First working conversion: SAF input -> hardware transcode -> staged cache file -> SAF export, driven from a Compose screen. Media3 Transformer is the engine for this path rather than FFmpeg. It is Apache-2.0, needs no native build, consumes content:// URIs directly, and runs MediaCodec decode -> GL surface -> MediaCodec encode without frames round-tripping through the CPU. FFmpeg remains necessary for the long tail (MP3, GIF, MKV, exotic containers) but is not the right tool here. Two hazards are designed against rather than discovered later: Transformer must be driven from a single thread that has a Looper, and start()/cancel() throw IllegalStateException from anywhere else. The Looper it binds to is whichever the Builder saw, silently falling back to the main one. A WorkManager Worker runs on a Looper-less executor thread, so the naive arrangement builds against the main Looper and then throws on start. Media3Engine owns a dedicated HandlerThread, passes its Looper explicitly, and marshals every call onto it, so callers get a plain suspending function and cannot reintroduce the bug. Media3EngineTest covers this directly by driving a conversion from a Looper-less thread. Output never goes through a SAF file descriptor. MP4 faststart rewrites the moov atom at the end and needs to seek backwards, which a SAF fd does not reliably support. OutputPublisher stages to app-private cache, a real POSIX path, and copies out afterwards. That costs transient double disk usage, so it checks free space before starting. Input uses ACTION_OPEN_DOCUMENT rather than the photo picker: the picker is images and video only, offers no audio at all, and does not reliably surface .mkv/.flac/.webm. SAF needs no runtime permission. Tests run on an API 37 emulator and assert the output codec by reading the muxed file with MediaExtractor, so a silent fallback to H.264 fails rather than passing. Progress reporting is deliberately not asserted as non-empty: a 3 s fixture can finish inside one 250 ms poll tick, which would be an intermittent failure rather than a real defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Media Converter
A free and open-source media converter for Android — batch video transcoding and compression, audio extraction and conversion, GIF and frame export, and file merging.
Android 13+ (API 33). Built with Jetpack Compose and Material 3.
Status: early development. The project scaffold and UI shell exist; the conversion pipeline is being built out. Not yet usable.
Licensing at a glance
- Source code: MIT
- Distributed APK: GPL-3.0 — because it bundles FFmpeg built with x264/x265
That split is deliberate, not an oversight. See LICENSES/README.md
for the reasoning and the corresponding-source obligations.
Architecture
Two conversion engines behind an explicit router, because neither one covers the job alone.
AndroidX Media3 Transformer — the hardware path
Handles the common cases: MP4/MOV in and out, H.264/HEVC, resolution and frame-rate changes, rotation, overlays, audio to AAC, and stream-copy transmuxing. Fully hardware accelerated end to end — MediaCodec decodes to a GL surface and MediaCodec re-encodes, so frames never round-trip through the CPU. Roughly 7–8× realtime on 720p.
FFmpeg — the long tail
Everything Media3 structurally cannot do:
- Containers outside MP4/WebM/Ogg/WAV/AAC — MKV, AVI, FLV, MPEG-TS
- MP3 output — Android has no MP3 encoder at any version; this is a platform gap
- GIF and image sequences
- Input codecs with no platform decoder on the device
- CRF and 2-pass rate control, for the quality tier
Quality tiers
The router is surfaced to users as a quality choice rather than hidden:
| Tier | Engine | Rate control | Trade-off |
|---|---|---|---|
| Fast (default) | Media3 / MediaCodec | bitrate-targeted | ~7–8× realtime, low battery cost |
| Best quality | FFmpeg + x264/x265 | CRF or 2-pass | ~realtime or slower, better quality per byte |
A note on "GPU acceleration"
Android has no GPU video codec path. There are three distinct tiers, and conflating them causes a lot of confusion:
- Fixed-function video codec silicon — reached through
MediaCodec. This is what "hardware accelerated" means for encode and decode. It is not the GPU. - GPU shader cores — genuinely used, but only for filters, scaling, and color effects on already-decoded frames, via OpenGL ES. Never for entropy coding.
- CPU — x264, x265, and software decoders.
FFmpeg's -hwaccel is meaningful on Android only as mediacodec, and even then it
targets direct-to-Surface playback rather than file-to-file transcoding. Vulkan Video
exists in FFmpeg 8.0+ but no shipping Android GPU driver exposes it — no VK_KHR_video_*
extension appears in any Android Vulkan Profile tier.
So this app is hardware accelerated via MediaCodec, and GPU accelerated for effects via GL shaders. Both are real; neither is "the GPU decoding video."
Building
Requires JDK 17+ and the Android SDK with API 37.
./gradlew :app:assembleDebug
The FFmpeg native library is built separately from source; that build is not yet wired into this repository.
Contributing
Contributions are welcome. Note that contributions to the source are under MIT, while
the distributed binary remains GPL-3.0 for the reasons described in LICENSES/README.md.