Commit Graph
4 Commits
Author SHA1 Message Date
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
JMR-devandClaude Opus 5 e4efa8a74a Add Media3 hardware conversion path with end-to-end tests
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>
2026-08-19 21:18:44 -05:00
JMR-devandClaude Opus 5 48f65f0941 Replace scaffold with Compose/Material 3 base and project identity
The generated scaffold was a Views-based Material 2 shell with no activity,
no Kotlin sources, and a placeholder package. Replace it with the real base
the conversion work builds on.

Build configuration, with three AGP 9 specifics that contradict most
tutorials still in circulation:

- AGP 9 has built-in Kotlin. Applying org.jetbrains.kotlin.android now fails
  the build, so the absence of that plugin is deliberate, not an oversight.
- The Compose compiler plugin is still separate and must be applied, pinned
  to 2.2.10 to match the kotlin-gradle-plugin AGP 9.3.1 brings transitively.
  Pinning it to the newest Kotlin release instead would mismatch.
- android.kotlinOptions {} was removed; jvm configuration moves to a
  top-level kotlin { compilerOptions {} }.

Java compatibility goes 11 -> 17 (AGP 9 requires JDK 17), abiFilters
restrict packaging to arm64-v8a and x86_64, and jniLibs packaging is set
uncompressed so the APK zip-aligns native libraries on 16 KB boundaries.

Every dependency version in the catalog was checked to resolve against
Google Maven rather than copied from documentation. Note that KSP has moved
to standalone versioning (2.3.11) and no longer uses the old
<kotlin>-<ksp> scheme; it is catalogued but left unapplied until Room lands.

Set applicationId to dev.jasonmross.mediaconverter. com.example.* is
rejected by the Play Console, and the application ID is permanent once
published, so it has to be right before the first upload. The display name
is just a string resource and stays changeable.

Document the split license posture: source is MIT, but the distributed
binary will be GPL-3.0 because it bundles FFmpeg built with x264/x265.
LICENSES/README.md records why, including that libass is ISC rather than
GPL, so subtitle burn-in is not what forces the GPL choice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:18:28 -05:00
JMR-dev 18ae2cff80 initial commit 2026-08-19 17:29:15 -05:00