Skip to content

marbou92/FrameForge

Repository files navigation

FrameForge

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.

Project status

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.

Tech stack

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

Module structure

: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.

Building

Prerequisites

  • 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)

Setup

  1. Clone the repository.

  2. (Optional) For signed release builds, copy keystore.properties.example to keystore.properties at 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.

  3. Open in Android Studio or build from the command line:

    ./gradlew :app:assembleDebug

ABI filters

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.

Architecture

MVVM with Clean Architecture influences

  • View — Compose Screen composables in feature/*/ui/
  • ViewModel@HiltViewModel classes in feature/*/viewmodel/, exposing state via StateFlow and accepting user intents via command methods
  • Domain — Pure Kotlin data classes in :core:timeline/model/ (no Android dependencies, fully testable)
  • DataProjectRepository in :core:database, wrapping Room DAOs
  • Native — C++ engine in :core:engine/src/main/cpp/, exposed via JNI bridges in NativeRenderer / NativeAudioEngine / NativeEffects / NativeOpticalFlow

Native engine

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.

Export flow

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 MediaCodecMediaMuxer

ExportViewModel enqueues the worker via WorkManager.enqueueUniqueWork and observes WorkInfo flow to drive the on-screen progress UI.

Persistence

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).

Testing

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.

ProGuard / R8

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").

Known limitations

  • AudioProjectRepositoryImpl's effect parameter mapping samples the BezierCurve at t=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.

Changelog (Phases 1–3)

A condensed view of the multi-phase hardening pass; see worklog.md for the full record.

Phase 1 — Critical bug fixes (8 P0s):

  • prefab = true in core/engine → Oboe now compiles + links when the AAR is present (audio playback revived)
  • EditorViewModel.play() increment formula corrected (was double-speed)
  • ProjectBrowserViewModel wired to the Room repository (home screen works)
  • ProjectSettingsScreen loads the actual aspect ratio before enabling Apply
  • Clip.effects mapped 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)
  • ExportWorker now calls executeExport() → 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: BezierGraphEditor density bug fixed, MixerStrip pan label, TimelineTrack MOVE interaction, TransportControls accessibility
  • core:codec: Muxer thread-safe + pts dedup fixed, AudioEncoder no longer drops/truncates, writeAudioChunk volume 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 guards
  • feature:audio: full timeline visible (was hardcoded 30s), playback- based metering, beat-cut tail loss fixed, TIME_STRETCH for BPM match
  • app: CrashLog O(1) NDJSON append, LifecycleLogger release-gated

Phase 3 — Architecture & code quality (all P2s):

  • feature:export: 6 selector components wired into ExportScreen (was ~1100 LOC dead code), state/race/path/format bugs fixed, @Previews added
  • feature:editor: PropertiesPanel reactive + correctly positioned, ClipView density-correct, fullscreen works, @Previews added
  • core:timeline+database: silent error swallowing replaced with logging, Waveform peaks immutable
  • core:ui: PropertyItem immutability fixed, Locale.US everywhere
  • feature:audio: dead state removed, save failures surfaced, critical TIME_STRETCH → TimeStretchEffect mapping added

License

See LICENSE if present, otherwise all rights reserved.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages