`FFmpegCommandBuilder` has emitted `-c:a libvorbis` since the day it was written, and libvorbis was not in the AAR this app ships: the configure line omitted `--enable-libvorbis`, and `strings` on both ABIs' `libavcodec.so` named every other external encoder and not that one. The arm was unreachable from both ends, so nobody ever hit it -- but the first user to pick Ogg Vorbis would have got `Unknown encoder 'libvorbis'`. That is #238's shape again: two individually-correct facts, a builder arm and a configure line, that no test put together, and that no coverage number can see. So the binary is rebuilt rather than the arm rewritten. FFmpeg's in-tree `vorbis` encoder was already in there and was tried first; it is experimental, stereo-only, and its quality knob spans 2x its floor against libvorbis's 6x. Shipping it would have meant `-strict experimental`, a forced `-ac 2` that silently upmixes every mono source, and a slider with nowhere to go. What ships instead is the arm as originally written, `-c:a libvorbis -q:a 5`, with `OGG_VORBIS` added to the presets, `VORBIS` added to `ENCODABLE_AUDIO`, and Ogg's per-codec extension fixed so a Vorbis file is not named `.opus`. The flag is `--enable-libvorbis`, read out of ffmpeg-kit's `get_library_name()` rather than guessed: the `--enable-lame` / `--enable-opus` rule predicts `--enable-vorbis`, and that is not it. An unrecognised `--enable-*` is ignored silently, so the artifact was checked before `bin/README.md` was touched -- `libvorbis` present in both ABIs, the configure line otherwise identical, FFmpeg still n8.1.2, 10 shared libraries per ABI, every LOAD still `0x4000`. Both mutations were run on API 34 rather than predicted. Pointing the arm at `libopus` reddens the e2e test with `expected:<[audio/vorbis]> but was:<[audio/opus]>` while its `OggS` assertion still passes, which is why the track MIME is asserted and the container magic is not enough. Adding `-ac 2` back reddens it with `expected:<[1]> but was:<[2]>`: this class's own fixture is mono, so mono staying mono is an assertion rather than a claim. The unit test's load-bearing assertion inverts with this change and is rewritten to say so. It used to assert that `libvorbis` was *absent*; it now asserts the encoder name plus the two flags that must not be there. Nothing on the JVM can tell a real encoder name from a fictional one -- which is exactly how this survived four coverage waves -- so the e2e test is the only thing that proves the positive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FFmpeg build
Builds ffmpeg-kit-next into an Android
AAR that the app consumes.
Why this exists
arthenica/ffmpeg-kit — the library nearly every Android FFmpeg tutorial still points at
— was archived, and its binaries were deleted from Maven Central. Every
com.arthenica:ffmpeg-kit-* coordinate now returns 404, and all of its GitHub release
tags have zero attached assets. Maven Central's search index still lists the old
versions, which is misleading; the files behind those entries are gone.
Its successor, ffmpeg-kit-next, is source-only by design and publishes no
prebuilt packages. So building FFmpeg ourselves is not a preference, it is the only
remaining option.
Community forks publishing prebuilt 16 KB-aligned AARs do exist, but each fails on license, ABI coverage, or publisher credibility — and at least one redistributes a non-free FDK-AAC build, which FFmpeg states is unredistributable.
Why a container
ffmpeg-kit-next is Nix-only. There is no plain android.sh; the repository ships
nix-android.sh plus a flake, and the flake pins the entire toolchain including
Android NDK 27.3.13750724 (r27d).
Rather than install Nix on a developer machine, the toolchain lives in a container image. That keeps the host clean and doubles as the reproducibility artifact F-Droid wants.
Note the NDK version: do not "helpfully" upgrade to r28+. The flake pins r27d and
android/jni/Android.mk applies -Wl,-z,max-page-size=16384 manually for arm64-v8a
and x86_64 precisely because r27 predates automatic 16 KB alignment. The output is
16 KB compliant as-is.
Usage
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
Two modes:
| Mode | Libraries | Purpose |
|---|---|---|
spike |
minimal | Validates the toolchain end to end without waiting on x264/x265/SVT-AV1 |
full |
shipping set | The GPL configuration that ships |
The run deliberately omits --rm: the container's writable layer retains the several
gigabytes of Nix store contents (Android SDK and NDK), so subsequent builds skip the
download. Reuse it with podman start -a ffmpeg-build.
Only arm64-v8a and x86_64 are built, matching the app's abiFilters. Dropping the
32-bit ABIs roughly halves both build time and APK size, and Play does not require them.
Library selection
Flag names come from get_library_name() in the upstream scripts/function.sh. Three
that are easy to get wrong:
- It is
--enable-lame, not--enable-libmp3lame. - It is
--enable-libsvtav1for SVT-AV1. - It is
--enable-libvorbis— the rule above makes--enable-vorbisthe natural guess and it is wrong. Read the function rather than extrapolating from the first two; library 9 is namedlibvorbisthere. Enabling it also enables libogg, which ffmpeg-kit pulls in as its dependency without being asked.
An unrecognised --enable-* is ignored silently. ffmpeg-kit does not error on one, so a
build that quietly dropped a library looks exactly like one that worked, and forty minutes
later there is an AAR that is wrong in a way nothing in the log says. #254 is where that
was learned, from the other end: the builder carried -c:a libvorbis for months against a
binary with no libvorbis in it — unreachable, so no user ever hit it, and no build log ever
mentioned it. Check the artifact, not the log — strings jni/*/libavcodec.so | grep -x <name>.
MP3 deserves a note: Android has no MP3 encoder at any API level. That is a platform
gap, not a Media3 limitation, so --enable-lame is the only way the app can output MP3.
--enable-android-media-codec gives FFmpeg the h264_mediacodec / hevc_mediacodec
wrappers (added in FFmpeg 6.0). These act as a fallback-within-the-fallback: hardware
encode from the FFmpeg side when a job was routed away from Media3 for container reasons
but still wants hardware speed.
Licensing
The full build passes --enable-gpl with x264 and x265, which makes the
distributed binary GPL-3.0. This is deliberate — see ../../LICENSES/README.md.
Worth recording, because it is widely misunderstood: libass is ISC licensed, not GPL, so subtitle burn-in does not require the GPL flag. The only things GPL genuinely buys are x264/x265 software encode (and with them CRF and 2-pass rate control), vidstab, and the GPL filter set.
Never build with --enable-nonfree. FFmpeg states it renders the binary
unredistributable.
Verified build output (2026-08-19, ffmpeg-kit-next v8.1.1 / FFmpeg 8.1.2)
The full build produced a 35 MB AAR with 10 shared libraries per ABI for arm64-v8a
and x86_64. Confirmed against the artifact rather than assumed:
- 16 KB page alignment: every
.soon both ABIs reportsLOAD align 0x4000(readelf -lW). This is the hard Play gate. - Separate shared libraries, not a static monolith — which is what the LGPL/GPL relinking obligation requires.
- Embedded configure line (from
strings libavutil.so):--enable-gpl --enable-version3 --enable-libx264 --enable-libx265 --enable-libsvtav1 --enable-libvpx --enable-libmp3lame --enable-libopus --enable-libdav1d --enable-libass --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libharfbuzz --enable-mediacodec --enable-jni --enable-shared --enable-small --enable-lto. Since 2026-09-06 it also carries--enable-libvorbis(#254), which is the only difference between that build and the one inbin/today — same tag, same FFmpeg version, same 10 shared libraries per ABI, all stillLOAD align 0x4000. - Present and verified:
libx264(with an x264 core banner, so genuinely linked),libx265,libsvtav1,libmp3lame,h264_mediacodec,hevc_mediacodec,libopus,libdav1d,libvorbis(from 2026-09-06), the GIF encoder and muxer, libass internals (ass_shaper_new), and thesubtitles,scale,palettegen,paletteuseandconcatfilters.
Note --enable-version3: combined with --enable-gpl this makes the binary GPL-3.0,
which is what LICENSES/README.md states.
A caution on verifying this yourself: the build uses --enable-small and --enable-lto,
so internal symbols like ff_libx264_encoder do not appear in strings output.
Their absence proves nothing. Check the configure line and the registered codec names
instead.
Release checklist
GPL-3.0 requires corresponding source alongside the binary. For each release, attach to the GitHub Release next to the APK:
- the exact
ffmpeg-kit-nexttag and FFmpeg version used, - the full configure line (printed in this script's build log), and
- any patches applied.