fframes-android-demo-project: The Android App That Treats Video Like Code

A tiny Kotlin front end, a Rust render engine, and a JNI bridge that turns a phone into a local video factory.

7 min read • View on GitHub • More from dmtrKovalenko

A modern Android phone sits on a workbench while a Kotlin app interface is visible on the screen. Behind the phone, a cutaway workshop reveals gears, a JNI bridge, a cargo-ndk crate, and a MediaCodec encoder driving film frames out the other side. The image explains that the app is only the control surface, while the actual rendering happens natively on the device.
The whole demo in one frame: Kotlin as control plane, Rust as rendering engine, Android hardware as the output machine.
Key Takeaways

The surprise in dmtrKovalenko/fframes-android-demo-project is not that it uses Rust. It is that the Android app stays small while the actual video work happens natively, on the device, with the phone doing real rendering instead of shuttling jobs to a server.

The surprising part is not Compose. It is the native renderer behind it.

A Compose button click becomes a native render job, then returns as a finished file through Android’s media stack.

The app side is deliberately plain. A Compose screen tracks state, launches the native call off the main thread, and hands the renderer a destination path plus a scratch directory. That is the entire trick: Kotlin owns interaction, Rust owns the expensive work.

private external fun render(outputPath: String, tempDir: String, slug: String)

scope.launch(Dispatchers.IO) {
    render(outputPath, cacheDir.absolutePath, slug)
}

when (state) {
    RenderState.IDLE -> renderButtonEnabled = true
    RenderState.RENDERING -> renderButtonEnabled = false
    RenderState.COMPLETED -> showSuccess()
    RenderState.FAILED -> showError()
}

That contract matters because it makes the app feel native instead of glued together. The UI never blocks, the native layer gets real filesystem paths, and the bridge has just enough surface area to stay understandable.

How the Kotlin app hands off work without freezing the UI

Shows how you can build fframes rust binary for android

Dmitriy Kovalenko, Author/Maintainer · dmtrKovalenko/fframes-android-demo-project

On the Rust side, the mental model changes. You are not editing video in the usual sense. You are describing it. The repo’s `hello_world.rs` implements a video object, generates SVG frames, and lets `fframes` flatten that sequence into something encodable.

A close-up cross-section shows a timeline curve feeding text and color into an SVG frame generator. The frame is then flattened and passed into a hardware encoder that outputs a finished video segment. The image explains that the core abstraction is programmable frames, not a traditional editing timeline.
Video becomes a data structure first, then a rendered frame, then an encoded file.
impl Video for HelloWorldVideo {
    fn render_frame(&self, frame: FrameContext) -> Frame {
        let svg = svgr!(
            "<svg>... {slug} ...</svg>"
        );
        Frame::from_svg(svg)
    }
}

let options = EncoderOptions {
    preferred_video_codec: Some("h264_mediacodec".into()),
    ..Default::default()
};

fframes::render(video, options, output_path, temp_dir)?

That last line is the tell. The renderer is not just producing pixels. It is choosing an Android-native path, which means the demo is interested in throughput and battery life, not just in making a proof of concept compile.

Why this demo bothers with Gradle, cargo-ndk, and jniLibs

This is where the repo stops looking like a stunt. Gradle does not just “include” a Rust library. It coordinates the Rust build, copies the compiled shared objects into Android’s native library folder, and wires the whole thing into the app lifecycle so the binary stays current before packaging.

That glue is boring in the best way. Once it works, the app developer can ignore the mechanics and think in terms of output path, cache directory, and render state instead of toolchain rituals.

Why hardware encoding changes the economics

ApproachWhere the work happensStrengthTrade-off
fframes on Android with JNI and hardware encodingOn-device, with Kotlin as control plane and Rust plus MediaCodec as the engineOffline capable, battery-aware, and tightly integrated with the appRequires a custom bridge and cross-compilation setup
FFmpeg-style native integrationUsually native C or C++ wrapped into AndroidBattle-tested codec breadth and mature toolingHeavier integration surface and more manual plumbing
Cloud renderingServer-side infrastructureSimpler client app and centralized computeNetwork dependency, higher latency, and ongoing server cost

The important part is not that one approach is universally better. It is that this repo shows a different economic shape. If the app already owns the workflow, local rendering can remove latency, avoid upload cycles, and keep the whole experience offline.

What this demo is really competing with

The real comparison is between owning the media pipeline and outsourcing it. FFmpeg gives you enormous reach, but usually through a lower-level integration story. Cloud rendering gives you scale, but the app becomes a client to someone else’s compute.

OptionBest forWhat you give up
fframes on AndroidProgrammatic video generation inside a mobile appYou accept a newer ecosystem and a more specialized abstraction
FFmpeg integrationBroad codec support and legacy compatibilityYou often spend more time at the plumbing layer
Cloud renderingHeavy jobs and centralized processingYou lose offline operation and add network and server dependence

The repo does not claim to replace every existing video workflow. It shows that there is room for a higher-level Rust-native path when the app itself is the product and video creation is part of the product loop, not a separate backend service.

The tiny repo that proves the larger idea

That is why the project matters. It is minimal by design, but it demonstrates a real boundary crossing: Android UI on top, Rust rendering below, Android hardware encoding at the end. Once that loop exists, the shape of future tooling changes.

The repo is not trying to be a framework, an SDK, or a polished product. It is a feasibility proof with teeth. A phone can be a video factory, and the boundary between app logic and media engine can be much thinner than most mobile stacks assume.