Files
LibreMediaConverter/tools/ffmpeg/build-ffmpeg.sh
JMR-devandClaude Opus 5 b082cea889 Add containerized FFmpeg build producing a 16 KB-aligned GPL AAR
There is no usable prebuilt FFmpeg for Android any more. arthenica/ffmpeg-kit
is archived and its binaries were deleted from Maven Central, so every
com.arthenica:ffmpeg-kit-* coordinate 404s and all of its release tags have
zero assets. Maven Central's search index still lists the old versions, which
misleads; the files behind those entries are gone. The successor,
ffmpeg-kit-next, is source-only by design. Building it ourselves is the only
remaining option, not a preference.

ffmpeg-kit-next is Nix-only -- there is no plain android.sh, only
nix-android.sh and a flake -- so the toolchain lives in a container rather
than on the developer's machine. The recipe doubles as the reproducibility
artifact F-Droid expects and as the GPL corresponding-source obligation.

Four problems this path hits, none of them documented upstream:

- The nixos/nix base image already ships bash, coreutils and git; installing
  them collides with the existing profile entries and fails the image build.
- Upstream scripts use #!/bin/bash but the image provides only /bin/sh, so
  start-android.sh dies with "cannot execute: required file not found" after
  the entire toolchain has been built.
- Gradle's AAPT2 comes from Maven as a prebuilt binary linked against FHS
  paths that do not exist under Nix, failing with "Daemon startup failed"
  after the whole native build succeeds. Nixpkgs' Android SDK ships an
  already-patched aapt2, so Gradle is pointed at that.
- A bare '*.aar' find also collects every AAR Gradle unpacked into its own
  caches, so the copy is scoped to the ffmpeg-kit outputs.

The NDK stays at r27d as the flake pins it. Do not "upgrade" to r28+:
android/jni/Android.mk applies -Wl,-z,max-page-size=16384 manually precisely
because r27 predates automatic alignment, and the result is verified 16 KB
compliant as-is.

Verified against the produced artifact: every .so on both ABIs reports LOAD
align 0x4000, libraries are separate rather than a static monolith as the
GPL relinking obligation requires, and the embedded configure line confirms
--enable-gpl --enable-version3 with x264, x265, SVT-AV1, LAME, libass and
the MediaCodec wrappers. Note that --enable-small and --enable-lto
internalize symbols, so absence from strings output proves nothing; check
the configure line instead.

The 35 MB AAR itself is gitignored. F-Droid strips checked-in prebuilt
native libraries, and the recipe is the artifact of record.

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

135 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Builds ffmpeg-kit-next into an Android AAR. Runs INSIDE the container image
# defined by the sibling Containerfile.
#
# Usage: build-ffmpeg.sh [spike|full]
#
# spike - minimal library set. Validates that the toolchain works and produces an
# AAR, without waiting on x264/x265/SVT-AV1. Use this first.
# full - the shipping configuration (GPL: x264 + x265).
#
set -euo pipefail
# The upstream scripts use `#!/bin/bash`, but the nixos/nix image ships only /bin/sh
# (itself bash, via the Nix store). Without this, start-android.sh dies with
# "cannot execute: required file not found" AFTER the whole toolchain has been built,
# which is an expensive way to discover a missing symlink.
if [[ ! -e /bin/bash ]]; then
ln -sf "$(command -v bash)" /bin/bash
fi
MODE="${1:-spike}"
TAG="${FFMPEG_KIT_TAG:-v8.1.1}"
SRC=/work/ffmpeg-kit-next
OUT=/work/out
# ---------------------------------------------------------------------------
# Library selection
# ---------------------------------------------------------------------------
# Flag names come from get_library_name() in scripts/function.sh — note it is
# --enable-lame, NOT --enable-libmp3lame.
#
# android-media-codec gives FFmpeg the h264_mediacodec / hevc_mediacodec wrappers.
# Those are the fallback-within-the-fallback: hardware encode from the FFmpeg side
# when a job has been routed away from Media3 for container reasons but still wants
# hardware encode.
COMMON_LIBS=(
--enable-android-media-codec
--enable-android-zlib
--enable-lame # MP3 encode. Android has NO MP3 encoder at any API level,
# so this is the only way the app can output MP3 at all.
--enable-opus
--enable-dav1d # fast AV1 decode
)
SUBTITLE_LIBS=(
--enable-libass # ISC licensed, NOT GPL - subtitle burn-in is LGPL-safe
--enable-fontconfig
--enable-freetype
--enable-fribidi
--enable-harfbuzz
)
# GPL. These are the reason the shipped binary is GPL-3.0 rather than LGPL: they are
# the only route to CRF and 2-pass rate control, which no Android hardware encoder
# exposes. See LICENSES/README.md.
GPL_LIBS=(
--enable-gpl
--enable-x264
--enable-x265
)
EXTRA_LIBS=(
--enable-libvpx # VP8/VP9
--enable-libsvtav1 # fast AV1 encode
)
case "$MODE" in
spike) LIBS=("${COMMON_LIBS[@]}") ;;
full) LIBS=("${COMMON_LIBS[@]}" "${SUBTITLE_LIBS[@]}" "${GPL_LIBS[@]}" "${EXTRA_LIBS[@]}") ;;
*) echo "unknown mode: $MODE (expected 'spike' or 'full')" >&2; exit 2 ;;
esac
echo "=============================================="
echo " ffmpeg-kit-next build"
echo " mode : $MODE"
echo " tag : $TAG"
echo " libraries : ${LIBS[*]}"
echo " started : $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "=============================================="
if [[ ! -d "$SRC" ]]; then
git clone --branch "$TAG" --depth 1 \
https://github.com/arthenica/ffmpeg-kit-next.git "$SRC"
fi
cd "$SRC"
# ---------------------------------------------------------------------------
# AAPT2 override
# ---------------------------------------------------------------------------
# The final step packages the .so files into an AAR with Gradle. Gradle's default
# AAPT2 comes from Maven as a prebuilt binary dynamically linked against normal FHS
# paths (/lib64/ld-linux-x86-64.so.2). Those do not exist in a Nix image, so it dies
# with "AAPT2 ... Daemon startup failed" AFTER the entire native build has succeeded.
#
# The Android SDK that Nix provides has an aapt2 that nixpkgs has already patchelf'd,
# so point Gradle at that one instead.
AAPT2="$(find /nix/store -maxdepth 6 -name aapt2 -type f 2>/dev/null | head -1)"
if [[ -n "$AAPT2" ]]; then
echo "using nix-provided aapt2: $AAPT2"
grep -v 'aapt2FromMavenOverride' android/gradle.properties > /tmp/gradle.properties.new || true
mv /tmp/gradle.properties.new android/gradle.properties
echo "android.aapt2FromMavenOverride=$AAPT2" >> android/gradle.properties
else
echo "WARNING: no nix aapt2 found; the AAR packaging step will probably fail." >&2
fi
# 64-bit only, matching the app's abiFilters. Dropping the 32-bit ABIs roughly halves
# build time and APK size, and Play does not require them.
# --api-level matches the app's minSdk 33 (default is 24), so the native code may use
# the newer NDK media APIs. ffmpeg-kit protocols (ffkitsaf, ffkitmem, ffkitstream) are
# left enabled: ffkitsaf is the SAF bridge that replaces the old getSafParameter trick.
# Output still stages through a real cache path rather than a SAF fd, because MP4
# faststart needs to seek back to rewrite the moov atom.
./nix-android.sh -p android-r27d \
--api-level=33 \
--disable-arm-v7a \
--disable-arm-v7a-neon \
--disable-x86 \
"${LIBS[@]}"
mkdir -p "$OUT"
# Only the ffmpeg-kit AAR. A bare '*.aar' find also sweeps up every AAR that Gradle
# happens to have unpacked into its own caches (junit, espresso, tracing...), which
# is confusing noise in the output directory.
find "$SRC/android/ffmpeg-kit-next-android-lib/build/outputs/aar" \
"$SRC/prebuilt" \
-name 'ffmpeg-kit-next*.aar' -exec cp -v {} "$OUT/" \; 2>/dev/null
echo "=============================================="
echo " finished : $(date -u +%Y-%m-%dT%H:%M:%SZ)"
ls -la "$OUT" || true