A timeline-based Android video editor built with Jetpack Compose, a native
C++/OpenGL ES 3.1 rendering engine, hardware MediaCodec + AAC encoding
pipeline, Room persistence, an audio mixer with beat detection, optical-flow
frame interpolation, and a WorkManager-driven export flow.
Active development. The module skeleton, native engine, codec pipeline, and Compose UI are in place; many features are functional, others are still being wired up. See each module's package docs for details.
| Concern | Choice |
|---|---|
| Language | Kotlin 2.0.21, C++17 (NDK) |
| Build | Gradle 8.9, AGP 8.7.3, version catalog (gradle/libs.versions.toml) |
| UI | Jetpack Compose (BOM 2025.10.00 — material3 1.4.0 stable), Material 3, dynamic color |
| DI | Hilt 2.52 (KSP) |
| Persistence | Room 2.6.1, DataStore Preferences |
| Background work | WorkManager 2.9.0 with Hilt (HiltWorkerFactory) |
| Async | Coroutines 1.7.3 |
| Navigation | Navigation Compose 2.7.6 |
| Native rendering | OpenGL ES 3.1 + compute shaders, EGL, self-contained (no external native deps) |
| Codec | Hardware MediaCodec for H.264/H.265/VP9/AV1 decode + encode, MediaMuxer for MP4/WebM/OGG |
| Audio | MediaCodec AAC encode, custom beat detector (FFT + autocorrelation); Oboe 1.9.0 when present |
| minSdk / targetSdk | 28 / 34 |
| compileSdk | 35 |
| Java / JVM target | 17 |
:app Application entry, DI wiring, navigation, project browser
:core:timeline Pure domain models (Project, Track, Clip, Effect, BezierCurve, ...)
:core:ui Shared Compose theme + components (TimelineTrack, BezierGraphEditor, ...)
:core:database Room database, DAOs, ProjectRepository
:core:codec MediaCodec decoder/encoder, AAC encoder, MediaMuxer wrapper, CodecPipeline
:core:engine Native C++ engine (GL renderer, audio, effects, optical flow) + JNI
:feature:editor Timeline editor screen, properties panel, motion/speed/time-remap editors
:feature:audio Audio mixer, waveform, beat detection, audio-reactive keyframe setup
:feature:export Export screen, codec/resolution/quality selectors, ExportWorker
Dependency layering: :core:timeline is the foundation. :core:database,
:core:codec, and :core:engine depend on it. Feature modules depend on
the relevant core modules. :app wires everything together via Hilt.
- Android Studio Koala or Ladybug (2024.x) or newer — AGP 8.7.x requires a 2024.x IDE
- JDK 17
- Android SDK with compileSdk 35
- NDK r25+ (bundled with Android Studio is fine)
- CMake 3.22.1+ (bundled with Android SDK is fine)
-
Clone the repository.
-
(Optional) For signed release builds, copy
keystore.properties.exampletokeystore.propertiesat the repo root and fill in your keystore details. Without this file, release builds fall back to the debug signing config so builds still work. -
Open in Android Studio or build from the command line:
./gradlew :app:assembleDebug
The native engine builds for armeabi-v7a, arm64-v8a, and x86_64.
32-bit ARM (armeabi-v7a) is included for older devices but lacks some
NEON-optimized paths; arm64-v8a is recommended for production.
- View — Compose
Screencomposables infeature/*/ui/ - ViewModel —
@HiltViewModelclasses infeature/*/viewmodel/, exposing state viaStateFlowand accepting user intents via command methods - Domain — Pure Kotlin data classes in
:core:timeline/model/(no Android dependencies, fully testable) - Data —
ProjectRepositoryin:core:database, wrapping Room DAOs - Native — C++ engine in
:core:engine/src/main/cpp/, exposed via JNI bridges inNativeRenderer/NativeAudioEngine/NativeEffects/NativeOpticalFlow
The C++ engine is self-contained — no external native libraries (no FFmpeg,
no libyuv). It links only against GLESv3, EGL, log, and android.
Oboe is optionally supported via find_package(oboe CONFIG QUIET) in
CMakeLists.txt; AGP's prefab mechanism is enabled in core/engine's
build.gradle.kts (buildFeatures { prefab = true }), so when the Oboe
AAR is on the classpath its headers are exported to CMake and the
__has_include(<oboe/Oboe.h>) guard in AudioEngine.cpp compiles in
real low-latency playback. When the AAR is absent, the audio engine
degrades cleanly to a stub.
All native wrapper classes implement AutoCloseable and use
java.lang.ref.Cleaner for deterministic-enough native peer cleanup.
The GL renderer must be called from a single thread (the GL thread) —
this is documented in NativeRenderer's KDoc.
Exports run via ExportWorker (a @HiltWorker CoroutineWorker in
:feature:export). The worker:
- Promotes itself to a foreground service with a progress notification
- Reports progress via
setProgress(throttled to every 5 frames) - Cancellation is honored via
coroutineContext.ensureActive()per frame - Writes the encoded output via
MediaCodec→MediaMuxer
ExportViewModel enqueues the worker via WorkManager.enqueueUniqueWork
and observes WorkInfo flow to drive the on-screen progress UI.
Room database frameforge.db (version 2) with four entities:
ProjectEntity, MediaItemEntity, EffectPresetEntity, UndoStackEntity.
Timeline content is serialized to JSON (full clip/effect/keyframe graph)
and stored as a timeline_json column on ProjectEntity. Schema export is
enabled (exportSchema = true); migration test helpers can be added.
MIGRATION_1_2 is registered automatically when consumers use
FrameForgeDatabase.builder(context) (the recommended entry point).
Unit and instrumented test dependencies (JUnit 4.13.2,
kotlinx-coroutines-test, Compose ui-test-junit4) are declared in
every module. The test suite currently has 265 @Test methods across
16 test files spanning:
:core:timeline(JVM) —BezierCurveTest,RationalTest,TimeRangeTest,TimelineSerializationTest,VideoClipMotionPathTest(Phase 1):core:codec(JVM) —ExportConfigTest,VideoCodecTest:core:database(JVM) —ConvertersTest:core:database(instrumented) —ProjectDaoTest,MediaItemDaoTest,EffectPresetDaoTest,UndoStackDaoTest,ConvertersInstrumentedTest:feature:editor(JVM) —MotionPathTimeRemapTest(Phase 1):app(instrumented) —FrameForgeDatabaseTest,HiltInjectionTest
Room migration tests should be added once the schema stabilizes further.
Release builds have isMinifyEnabled = true and isShrinkResources = true.
ProGuard rules are in app/proguard-rules.pro, organized by library.
Native methods are kept. Consumer rules are bundled with each library
module via consumerProguardFiles("consumer-rules.pro").
AudioProjectRepositoryImpl's effect parameter mapping samples theBezierCurveatt=0(loses time-animation); a proper mapping that preserves the curve is a TODO.- The optical flow GPU path is currently a stub (returns
false); CPU single-level Lucas-Kanade is the only working path. GPU interpolation is a future improvement.
A condensed view of the multi-phase hardening pass; see worklog.md for
the full record.
Phase 1 — Critical bug fixes (8 P0s):
prefab = trueincore/engine→ Oboe now compiles + links when the AAR is present (audio playback revived)EditorViewModel.play()increment formula corrected (was double-speed)ProjectBrowserViewModelwired to the Room repository (home screen works)ProjectSettingsScreenloads the actual aspect ratio before enabling ApplyClip.effectsmapped in both directions → effects survive save/load- Motion-path / speed-curve / time-remap persisted on
Clip CodecPipeline.cancel()captures the coroutine Job; multipass disabled (was crashing)ExportWorkernow callsexecuteExport()→ export produces a real (currently black-frame) video file- 11 new JVM tests (motion-path / time-remap JSON round-trip)
Phase 2 — High-severity correctness & data safety (5 P1s):
core:ui:BezierGraphEditordensity bug fixed,MixerStrippan label,TimelineTrackMOVE interaction,TransportControlsaccessibilitycore:codec: Muxer thread-safe + pts dedup fixed,AudioEncoderno longer drops/truncates,writeAudioChunkvolume scaling removed (was corrupting AAC)core:engine: AudioEngine atomics + mono→stereo upmix, transform effects (skew/crop/rotate90) fixed, scratch FBO cached, GL_BLEND state managed correctly, OpticalFlow GPU path honestly stubbed, JNI guardsfeature:audio: full timeline visible (was hardcoded 30s), playback- based metering, beat-cut tail loss fixed,TIME_STRETCHfor BPM matchapp:CrashLogO(1) NDJSON append,LifecycleLoggerrelease-gated
Phase 3 — Architecture & code quality (all P2s):
feature:export: 6 selector components wired intoExportScreen(was ~1100 LOC dead code), state/race/path/format bugs fixed,@Previews addedfeature:editor: PropertiesPanel reactive + correctly positioned, ClipView density-correct, fullscreen works,@Previews addedcore:timeline+database: silent error swallowing replaced with logging,Waveformpeaks immutablecore:ui:PropertyItemimmutability fixed,Locale.USeverywherefeature:audio: dead state removed, save failures surfaced, criticalTIME_STRETCH → TimeStretchEffectmapping added
See LICENSE if present, otherwise all rights reserved.