recipe: av 18.1.0 (PyAV) + flet-libffmpeg 8.1.2 - #122
Merged
Merged
Conversation
Adds recipes for [av](https://pyav.basswood.io) 18.1.0 — FFmpeg's libraries behind a Python API: containers, streams, packets, codecs and frames as objects, with the pixel and sample data reachable in place — and the [FFmpeg](https://ffmpeg.org) 8.1.2 it links. Requested in [flet#6730](flet-dev/flet#6730). - [Docs](recipes/av/README.md) - [Example](recipes/av/examples/clip-roundtrip) An app cannot ship an `ffmpeg` binary or spawn a subprocess on either platform, so PyAV is the only route to media processing in Python on a phone. The wheel carries its own FFmpeg, which also means what an app can decode stops depending on the OS underneath it. ## Recipe shape A two-recipe chain, `flet-libarrow` -> `pyarrow` in shape: `flet-libffmpeg` builds the seven `libav*` libraries **shared**, and `av` compiles PyAV's 49 Cython extension modules against them. Shared is forced, not chosen — PyAV's `setup.py` rejects a static FFmpeg outright ("Building PyAV against static FFmpeg libraries is not supported"), and 49 modules each static-linking a 13 MB `libavcodec` is not a wheel anyone would ship. Delivery is the proven one: unversioned `lib<name>.so` with a matching SONAME into the APK's jniLibs on Android (FFmpeg's own `android` target already emits exactly that), and unversioned `lib<name>.dylib` with an `@rpath` install-id into per-slice `*.framework` bundles on iOS, plus an `__init__` preload shim. Two things were needed on top: - **`-Wl,-headerpad_max_install_names` on the iOS lanes of both recipes.** serious_python rewrites each `@rpath/libavformat.dylib` to `@rpath/opt.lib.libavformat.framework/opt.lib.libavformat`, 30 bytes longer, and `install_name_tool` cannot grow a load command with no padding. CMake passes the flag on Apple platforms itself, which is why pyarrow never hit this; setuptools and FFmpeg's `configure` do not. **The failure is silent:** `reconcile_framework_install_names` treats it as fatal and aborts the sync, but `flet build` still reports success — and since the plugin's `dist_ios` lives in the shared pub cache, the `.app` then ships whatever the last successful build of any other project left there. Observed as an app carrying another recipe's packages and no `av` at all. - **Relocatable `.pc` files.** `setup.py` finds FFmpeg *only* through `pkg-config`, and forge already puts `<site-packages>/opt/lib/pkgconfig` on `PKG_CONFIG_LIBDIR` — so once `prefix`/`libdir`/`includedir` are rewritten to `${pcfiledir}`-relative form the consumer needs no `script_env` for it at all. FFmpeg writes all three out as absolute staging paths. `--disable-autodetect` (plus an explicit `--enable-zlib`) is what makes the feature set identical on a laptop and in CI, and keeps the two platforms symmetric: no VideoToolbox on iOS without MediaCodec on Android, no `iconv` on one side only. ## Licensing No `--enable-gpl`, no `--enable-nonfree`, no `--enable-version3` — and two on-device tests assert that rather than leaving it to prose: every library must report LGPL, and no GPL/nonfree codec may appear in `codecs_available`. `about` declares **`LGPL-2.1-or-later AND IJG`**, not plain LGPL. libavcodec's `jrevdct.c`, `jfdctfst.c` and `jfdctint_template.c` are Independent JPEG Group code and are genuinely compiled in (`jrevdct.o`, `jfdctfst.o` and `jfdctint.o` are all in the link), and while FFmpeg's `LICENSE.md` concludes "in combination the LGPL v2.1+ applies", the IJG grant adds a condition that survives the combination and lands on whoever ships the binary: an app distributing only executables must state in its documentation that it is "based in part on the work of the Independent JPEG Group". The recipe README tells consumers so in a bullet of its own; the expression is what makes it discoverable from the wheel. Verified to canonicalise under strict `packaging`. The IJG grant lives in a header comment, not a file, so `build.sh` **extracts** it into `COPYING.IJG` from the source being built and fails the build if it is not there — a copy checked into the recipe would drift silently on the next bump. `license_file` is pinned to `COPYING.LGPLv2.1`, `LICENSE.md` and that extract; discovery would otherwise bundle `COPYING.GPLv2`/`GPLv3`/`LGPLv3`, notices for code this build does not contain. `av` needs no `about` — it is a Python recipe, so its own backend carries `License-Expression: BSD-3-Clause` and `LICENSE.txt` through, as #116 describes. ## Validation - 12/12 wheels green: both recipes x arm64-v8a, armeabi-v7a, x86_64, iphoneos.arm64, iphonesimulator.arm64, iphonesimulator.x86_64. - On-device 12/12 passed, EXIT 0, locally on an Android arm64 emulator and the iOS Simulator. - CI **12/12 jobs green** across 3.12 / 3.13 / 3.14 × android / iOS, twice: run 32852067992 for the recipes and run 32855217090 once the licence work landed, the latter on this exact tree. On-device `12 passed` / `EXIT 0` on the x86_64 emulator and simulator in both. - The `clip-roundtrip` example builds and runs on both: writes a 3 s MP4 with video and audio streams into app storage, reopens it, and puts four seeked frames on screen. ## Changes - `recipes/flet-libffmpeg/` — `meta.yaml` + `build.sh`, incl. the IJG notice extraction. - `recipes/av/` — `meta.yaml`, one patch, 12 on-device tests, `README.md`, and the example. - `.claude/skills/` — the headerpad failure class (catalogue + both sibling skills), the relocatable-`.pc` convention, and the `flet --version` / bundled-site-packages checks that would have caught the silent iOS build in one line. ## Consumer notes PyAV gives a Flet app the whole FFmpeg surface for reading and rewriting media: probing, trimming, remuxing, extracting stills and audio, filtering, resampling. Decoding is broad (H.264, HEVC, AV1, VP8/9, AAC, MP3, Opus, FLAC…); encoding is narrower because the modern video encoders are all GPL or unbuilt external libraries, and there is no `https` protocol — both documented, with the workarounds, in [the recipe README](recipes/av/README.md). [skip ci]
# Conflicts: # .claude/skills/local-recipe-testing/SKILL.md # .claude/skills/new-mobile-recipe/references/recipe-patterns.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds recipes for av 18.1.0 — FFmpeg's libraries behind a Python API: containers, streams, packets, codecs and frames as objects, with the pixel and sample data reachable in place — and the FFmpeg 8.1.2 it links. Requested in flet#6730.
An app cannot ship an
ffmpegbinary or spawn a subprocess on either platform, so PyAV is the only route to media processing in Python on a phone. The wheel carries its own FFmpeg, which also means what an app can decode stops depending on the OS underneath it.Recipe shape
A two-recipe chain,
flet-libarrow->pyarrowin shape:flet-libffmpegbuilds the sevenlibav*libraries shared, andavcompiles PyAV's 49 Cython extension modules against them. Shared is forced, not chosen — PyAV'ssetup.pyrejects a static FFmpeg outright ("Building PyAV against static FFmpeg libraries is not supported"), and 49 modules each static-linking a 13 MBlibavcodecis not a wheel anyone would ship.Delivery is the proven one: unversioned
lib<name>.sowith a matching SONAME into the APK's jniLibs on Android (FFmpeg's ownandroidtarget already emits exactly that), and unversionedlib<name>.dylibwith an@rpathinstall-id into per-slice*.frameworkbundles on iOS, plus an__init__preload shim. Two things were needed on top:-Wl,-headerpad_max_install_nameson the iOS lanes of both recipes. serious_python rewrites each@rpath/libavformat.dylibto@rpath/opt.lib.libavformat.framework/opt.lib.libavformat, 30 bytes longer, andinstall_name_toolcannot grow a load command with no padding. CMake passes the flag on Apple platforms itself, which is why pyarrow never hit this; setuptools and FFmpeg'sconfiguredo not. The failure is silent:reconcile_framework_install_namestreats it as fatal and aborts the sync, butflet buildstill reports success — and since the plugin'sdist_ioslives in the shared pub cache, the.appthen ships whatever the last successful build of any other project left there. Observed as an app carrying another recipe's packages and noavat all..pcfiles.setup.pyfinds FFmpeg only throughpkg-config, and forge already puts<site-packages>/opt/lib/pkgconfigonPKG_CONFIG_LIBDIR— so onceprefix/libdir/includedirare rewritten to${pcfiledir}-relative form the consumer needs noscript_envfor it at all. FFmpeg writes all three out as absolute staging paths.--disable-autodetect(plus an explicit--enable-zlib) is what makes the feature set identical on a laptop and in CI, and keeps the two platforms symmetric: no VideoToolbox on iOS without MediaCodec on Android, noiconvon one side only.Licensing
No
--enable-gpl, no--enable-nonfree, no--enable-version3— and two on-device tests assert that rather than leaving it to prose: every library must report LGPL, and no GPL/nonfree codec may appear incodecs_available.aboutdeclaresLGPL-2.1-or-later AND IJG, not plain LGPL. libavcodec'sjrevdct.c,jfdctfst.candjfdctint_template.care Independent JPEG Group code and are genuinely compiled in (jrevdct.o,jfdctfst.oandjfdctint.oare all in the link), and while FFmpeg'sLICENSE.mdconcludes "in combination the LGPL v2.1+ applies", the IJG grant adds a condition that survives the combination and lands on whoever ships the binary: an app distributing only executables must state in its documentation that it is "based in part on the work of the Independent JPEG Group". The recipe README tells consumers so in a bullet of its own; the expression is what makes it discoverable from the wheel. Verified to canonicalise under strictpackaging.The IJG grant lives in a header comment, not a file, so
build.shextracts it intoCOPYING.IJGfrom the source being built and fails the build if it is not there — a copy checked into the recipe would drift silently on the next bump.license_fileis pinned toCOPYING.LGPLv2.1,LICENSE.mdand that extract; discovery would otherwise bundleCOPYING.GPLv2/GPLv3/LGPLv3, notices for code this build does not contain.avneeds noabout— it is a Python recipe, so its own backend carriesLicense-Expression: BSD-3-ClauseandLICENSE.txtthrough, as #116 describes.Validation
iphonesimulator.arm64, iphonesimulator.x86_64.
12 passed/EXIT 0on the x86_64 emulator and simulator in both.clip-roundtripexample builds and runs on both: writes a 3 s MP4 with video and audio streams into app storage, reopens it, and puts four seeked frames on screen.Changes
recipes/flet-libffmpeg/—meta.yaml+build.sh, incl. the IJG notice extraction.recipes/av/—meta.yaml, one patch, 12 on-device tests,README.md, and the example..claude/skills/— the headerpad failure class (catalogue + both sibling skills), the relocatable-.pcconvention, and theflet --version/ bundled-site-packages checks that would have caught the silent iOS build in one line.Consumer notes
PyAV gives a Flet app the whole FFmpeg surface for reading and rewriting media: probing, trimming, remuxing, extracting stills and audio, filtering, resampling. Decoding is broad (H.264, HEVC, AV1, VP8/9, AAC, MP3, Opus, FLAC…); encoding is narrower because the modern video encoders are all GPL or unbuilt external libraries, and there is no
httpsprotocol — both documented, with the workarounds, in the recipe README.