Skip to content

recipe: av 18.1.0 (PyAV) + flet-libffmpeg 8.1.2 - #122

Merged
ndonkoHenri merged 4 commits into
mainfrom
pyav
Sep 15, 2026
Merged

ndonkoHenri merged 4 commits into
mainfrom
pyav

Conversation

@ndonkoHenri

Copy link
Copy Markdown

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 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.

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
@ndonkoHenri
ndonkoHenri merged commit 19733e9 into main Sep 15, 2026
14 of 26 checks passed
@ndonkoHenri
ndonkoHenri deleted the pyav branch September 15, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant