Files
LibreMediaConverter/README.md
T
JMR-devandClaude Opus 5 bb969ece64 Enable R8 and add release, store and F-Droid infrastructure
Turning on R8 immediately surfaced a latent runtime bug: the ffmpeg-kit-next
wrapper references com.arthenica.smartexception.java.Exceptions from
AbstractSession.fail() in eighteen places, but a local .aar carries no
transitive dependencies, so nothing was pulling it in. Debug builds tolerate
this through lazy class loading -- the class is only touched on an error
path -- so it would have shipped as a crash the first time an FFmpeg
conversion failed. Declared explicitly now.

Keep rules cover the JNI boundary. The native library resolves classes and
methods by name, which R8 cannot see, so without them the FFmpeg calls fail
with NoSuchMethodError in release builds only. Workers are kept too, since
WorkManager reconstructs them reflectively from a class name persisted in its
database, and a rename breaks jobs enqueued before the update.

Verified on the produced artifacts rather than assumed: all 22 native
libraries survive minification and every one is still 16 KB aligned inside
the APK. Release is 82 MB against 115 MB for debug; the AAB is 40 MB and Play
splits it per ABI.

The privacy policy lists every permission, including the three WorkManager
adds automatically (WAKE_LOCK, RECEIVE_BOOT_COMPLETED, ACCESS_NETWORK_STATE).
Checking the merged manifest showed those, and a policy that omitted them
would look dishonest to anyone who inspected the app. INTERNET is genuinely
absent, so "files stay on the device" is enforced by the OS rather than a
promise.

CI runs unit tests on every push and builds the FFmpeg AAR only for release
tags, since that is a full cross-compile. Releases attach the FFmpeg
corresponding source next to the APK: GPL-3.0 requires it, and FFmpeg's
instruction to host it "on the same webserver" cannot be satisfied by a Play
listing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:45:40 -05:00

130 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: working, unreleased.** Both conversion engines, the router, the background
> job queue and the join flow are implemented and building. The FFmpeg format tests have
> been written but not yet executed on a device.
## 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`](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:
1. **Fixed-function video codec silicon** — reached through `MediaCodec`. This is what
"hardware accelerated" means for encode and decode. It is not the GPU.
2. **GPU shader cores** — genuinely used, but only for filters, scaling, and color
effects on already-decoded frames, via OpenGL ES. Never for entropy coding.
3. **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."
## Features
| | Formats |
|---|---|
| Video out | MP4 (H.264/H.265), MKV (H.264/H.265), WebM (VP9) |
| Audio out | MP3, AAC/M4A, FLAC, Opus, WAV |
| Images | GIF, PNG frame sequences |
| Other | Join several files into one |
Conversions run as durable background work, so they survive leaving the app and are
restored after a restart.
## Building
Requires JDK 17+ (AGP 9 will not run on older) and the Android SDK with API 37.
The FFmpeg AAR is **not committed** — it is a 35 MB binary, and F-Droid strips
checked-in native libraries. Build it first:
```sh
cd tools/ffmpeg
podman build -t ffmpeg-kit-builder:local -f Containerfile .
mkdir -p out
podman run --name ffmpeg-build -v "$PWD/out":/work/out:Z \
localhost/ffmpeg-kit-builder:local full
cp out/ffmpeg-kit-next-*.aar ../../app/libs/
```
Then:
```sh
./gradlew :app:assembleDebug # debug APK
./gradlew :app:testDebugUnitTest # JVM tests
./gradlew :app:connectedDebugAndroidTest # device tests, needs a running device
./gradlew :app:assembleRelease # R8-minified release
```
See [`tools/ffmpeg/README.md`](tools/ffmpeg/README.md) for why the build is
containerised and which flags matter.
## Testing
Unit tests cover the parts that decide correctness without needing hardware: the
routing matrix, the FFmpeg argument builder, and the stream-copy-versus-re-encode
planner. They run against fabricated device profiles, so branches like "this device
cannot encode HEVC" are reachable regardless of what the test machine is.
Instrumented tests cover the parts that only a device can prove: real hardware
transcoding, the foreground service type, and each FFmpeg output format asserted
against the produced file rather than the exit code.
## Privacy
The app has **no `INTERNET` permission**, so it cannot open a network connection at all.
Nothing is uploaded, and there is no analytics or advertising. See [PRIVACY.md](PRIVACY.md),
which also explains the permissions WorkManager adds automatically.
## 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`.