feat: install FFmpeg with one command, from a pinned LGPL build - #118
Merged
Conversation
Recording video needed an FFmpeg executable and the error told users to pick one of four mechanisms. `capture install-ffmpeg` now gets them one. The wheel and the sdist still carry no FFmpeg bytes, and nothing downloads unless the operator runs that command. It fetches one archive per platform from the openadapt-desktop ffmpeg-runtime-v8.1.2-r1 release, which openadapt-desktop builds from the upstream FFmpeg 8.1.2 tarball with --disable-gpl, --disable-nonfree and --disable-version3. That build is LGPL-2.1-or-later, and FFmpeg's own LICENSE.md is installed beside the binaries. Ordering, which the licensing and safety story depends on: the archive digest is compared before the archive is opened, every extracted member is written at mode 0600 and digest-checked as it is written, and only after all of them match does anything get an executable bit. A mismatch leaves nothing installed. The downloader refuses a non-HTTPS URL, an off-host redirect, an oversized body, an oversized member, and any executable member under bin/ that is not pinned. Resolution order is unchanged and the installed runtime is last, behind OPENADAPT_FFMPEG_PATH, Recorder(ffmpeg_path=...), OPENADAPT_DESKTOP_FFMPEG_PATH, Desktop's ffmpeg.json manifest, and PATH. Installing never displaces an FFmpeg the operator already chose, and the command says so when PATH wins. verify_distribution.py checked filenames, so a renamed binary passed. It now checks executable magic, container magic, and FFmpeg build strings for every member that is not text, and a member counts as text only if it is UTF-8 with no NUL byte, so documentation that names FFmpeg still passes while a renamed binary does not. That gate already runs on every pull request and on both release paths. check_ffmpeg_pin.py reads the live release assets and verifies the archive digest, every member digest, and the configure arguments recorded inside the archive, so the LGPL claim is checked against the artifact instead of asserted. The new workflow runs it weekly and on any pull request that touches the pin, plus an install-and-encode smoke test on Linux, macOS, and Windows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abrichr
force-pushed
the
feat/ffmpeg-provision
branch
from
August 28, 2026 17:48
15f43fb to
5dd93a9
Compare
abrichr
added a commit
that referenced
this pull request
Aug 28, 2026
test_install_writes_verified_files_and_a_receipt asserted st_mode & 0o100 on the installed ffmpeg. Windows has no execute bit: st_mode is 0o666 or 0o444 there and chmod only toggles read-only, so the assertion could never hold and test-windows went red on main after #118. The install-and-encode lane in ffmpeg-pin.yml already proves the installed runtime actually runs on windows-latest, which is the property this assertion was reaching for. Keep the mode checks on POSIX and check readability on Windows. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 28, 2026
#118 and #119 landed after the 1.3.0 section was written, so the notes did not mention them. #118 in particular adds a user-facing command and a network fetch, which the notes must state. The FFmpeg entry says plainly that neither artifact carries FFmpeg bytes and that nothing downloads unless the operator asks. Verified against the built wheel and source archive rather than taken from the commit message. The version stays 1.3.0. The highest bump implied since v1.2.2 is still minor, and 1.3.0 is already the minor. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
What this fixes
Recording video needs an FFmpeg executable. When one was missing, the error
listed four mechanisms and left you to pick. Now it names one command:
That downloads a single pinned archive for your platform, checks its SHA-256
against a digest compiled into the package, and installs it under your user
data directory.
--dry-runprints the URL, digest, and destination withoutfetching.
capture uninstall-ffmpegremoves it.What it does not change
The wheel and the source distribution still carry zero FFmpeg bytes, and
Capture downloads nothing unless you run that command. Bundling FFmpeg in an
MIT package would relicense the package, which is why the friction existed.
The build, and why it is LGPL
The archives come from the openadapt-desktop
ffmpeg-runtime-v8.1.2-r1release, which openadapt-desktop builds from the upstream FFmpeg 8.1.2 tarball
(
ffmpeg-8.1.2.tar.xz, sha256464beb5e...b524c, signed byFCF986EA15E6E293A5644F10B4322F04D67658D8). The configure arguments arerecorded inside each archive and include
--disable-gpl,--disable-nonfree,and
--disable-version3.FFmpeg's own
LICENSE.mdis the source for the licence claim: most files areLGPL v2.1 or later, and of the optional GPL parts it says "None of these parts
are used by default, you have to explicitly pass
--enable-gplto configure toactivate them." So the pinned build is LGPL-2.1-or-later. That licence text is
installed beside the binaries, and the install receipt records the archive, its
digest, and the matching upstream source tarball.
Reusing openadapt-desktop's release matters: Desktop already builds, signs, and
publishes these exact artifacts and its Rust provisioner already installs them.
A second downloader with its own pin would eventually disagree with the first.
Digest ordering
opened, extracted, made executable, or run.
bin/that is not pinned, and extracteach pinned member at mode
0600, digest-checked as it is written.tests/test_ffmpeg_provision.py::test_every_digest_is_checked_before_anything_becomes_executablerecords extraction and
chmodevents and asserts the last verificationprecedes the first executable bit. Reordering the two loops fails it.
The downloader also refuses a non-HTTPS URL, a redirect to an unpinned host, an
oversized body, an oversized member, and an archive whose
bin/carriesanything unpinned.
Precedence
Unchanged, with the installed runtime appended last:
Recorder(ffmpeg_path=...)orOPENADAPT_FFMPEG_PATHOPENADAPT_DESKTOP_FFMPEG_PATHffmpeg.jsonprovision manifestPATHcapture install-ffmpeginstalledInstalling never displaces an FFmpeg you already chose. When something earlier
wins, the command says so and tells you how to prefer the installed one.
Release gate
scripts/verify_distribution.pycompared filenames, so a renamed binary wouldhave passed. It now checks, for every member: executable magic (ELF, Mach-O,
PE), container magic (zip, xz, gzip, bzip2, zstd, ar), and FFmpeg build strings
ffmpeg version,libavcodec,--enable-gpland the rest. Those last twocategories apply to members that are not text, where text means UTF-8 with no
NUL byte. Documentation that names FFmpeg passes; a binary renamed
.pydoesnot. Dropping a real
bin/ffmpeginto the built wheel is caught under its ownname and after renaming to
codec_helper.bin.The gate already runs in
test.yml'spackage-contractjob on every pullrequest and in both
release.ymlpublication paths.scripts/check_ffmpeg_pin.pyis new and reads the live release assets: archivedigest, every member digest, the
bin/allowlist, and the configure argumentsrecorded in the archive. The LGPL claim is verified against the artifact rather
than asserted.
.github/workflows/ffmpeg-pin.ymlruns it weekly and on anypull request touching the pin, alongside an install-and-encode smoke test on
Linux, macOS, and Windows.
Evidence
member digest matches the committed pin, and all four report the LGPL
configuration (
scripts/check_ffmpeg_pin.py, full run).0700/0600, the receiptis written, and
require_video_encoder()encodes and decodes withh264_videotoolboxintomp4.PATHemptied, resolution picks the installed runtime and reportssource="capture install-ffmpeg". With an ffmpeg onPATH,PATHstillwins.
Note on imageio-ffmpeg
I considered depending on
imageio-ffmpeginstead and decided against it. Seethe review discussion; the short version is that it moves the same LGPL binary
into a transitive dependency where this repository controls neither the pin nor
the build configuration, and its bundled build is a different configuration
from the one Desktop already ships and probes.
🤖 Generated with Claude Code