Skip to content

Upgrade to FFmpeg 7.1.5 + harden (MT-only, lean, vendorable) - #1

Open
asoulliereHT wants to merge 17 commits into
mainfrom
chore/phase-3-ffmpeg-7.1
Open

asoulliereHT wants to merge 17 commits into
mainfrom
chore/phase-3-ffmpeg-7.1

Conversation

@asoulliereHT

Copy link
Copy Markdown
Collaborator

Upgrades this fork from FFmpeg 5.1.4 → 7.1.5 and hardens it for an internal
video-clipping use case. Built and verified natively on Apple Silicon; 12/12
MT browser suite passing locally.

Highlights

  • FFmpeg 5.1.4 → 7.1.5; Emscripten 3.1.40 → 6.0.2 (digest-pinned, multi-arch/native).
  • fftools re-ported to 7.x's scheduler frontend (entry renames, dup-symbol fixes, C23
    stdbit, return-code capture, init_globals re-entrancy, progress + timeout bridges).
  • Lean codec set — dropped x265, theora, vorbis/ogg, and the subtitle stack
    (libass/freetype/fribidi/harfbuzz). Core 32.7 MB → 23.6 MB.
  • MT-only (7.x frontend requires threads; needs COOP/COEP headers).
  • Supply chain: x264/lame pinned to SHAs, zlib → 1.3.1 (CVE-2018-25032, CVE-2022-37434).
  • CI modernized (checkout/setup-node v4, Node 20, MT-only, GHA cache) + a v*-tag
    release job that packages vendorable artifacts (ffmpeg-core-mt, ffmpeg-wasm).
  • Fixed the broken serve script (http-server --headers was a silent no-op → no COOP/COEP).

Browser capability map

Op Status
cut/trim (-c copy)
single-input transcode
lossless concat
multi-input filtergraph (xfade/overlay) ❌ deadlocks — run server-side

Decisions

MT-only · single long-lived instance · transitions/re-encode server-side · vendored (not npm).

See FORK.md for build/vendor/usage and the required headers.

🤖 Generated with Claude Code

asoulliereHT and others added 17 commits July 4, 2026 01:51
Reproduce the current (pre-upgrade) FFmpeg n5.1.4 WASM build as a
rollback reference before the 7.1 upgrade effort.

- build/ffmpeg.sh: cap FFmpeg `make` parallelism (FFMPEG_JOBS, default 4)
  so individual clang processes aren't OOM-killed when the amd64-only
  emsdk 3.1.40 toolchain runs under x86 emulation on Apple Silicon
- .gitignore: ignore local buildx cache dirs (build-cache-st/-mt)
- BASELINE-n5.1.4.md: record toolchain, external-lib pins, FFmpeg
  configure flags, artifact sizes, test results (ST 23 pass, MT wrapper
  11 pass), and pre-existing harness gaps (broken serve script, MT-core
  main-thread test, Node test path)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the toolchain off the amd64-only emsdk 3.1.40, which forced slow,
OOM-prone x86 emulation on Apple Silicon. Only emscripten/emsdk:latest
is multi-arch, so pin it by manifest-list digest (= Emscripten 6.0.2):
reproducible builds that run native on both arm64 (local) and amd64 (CI).

Drop x265: its x86-centric CMake passes `-march=`, which emsdk 6.0.2's
newer clang rejects for the wasm target. x265 was already slated for
removal (HEVC *encode* only; HEVC decode remains via FFmpeg's native
decoder), so this both unblocks the toolchain bump and advances the trim.

All other vendored libs build cleanly under 6.0.2. FFmpeg itself is not
built here yet; the current n5.1.4 is superseded by the 7.1 upgrade
(the pre-upgrade baseline is captured on chore/phase-0-baseline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Toolchain + build now produce a statically-linked MT ffmpeg-core.wasm
(~27.5 MB) reporting "version 7.1.5" (libavcodec 61). NOT yet functional
at runtime — MT scheduler hangs during the test suite; re-entrancy,
progress, and timeout bridges are not reimplemented for 7.x yet.

Build/toolchain:
- Dockerfile: FFMPEG_VERSION n5.1.4 -> n7.1.5
- build/ffmpeg.sh: scope -fPIC to FFmpeg configure (emsdk 6.0.2 probe
  fix) and strip *.so from the dep prefix so -l<lib> links statically
  (zlib's CMake emits libz.so despite BUILD_SHARED_LIBS=OFF, which
  emsdk 6.0.2 turned into a runtime dlopen)
- build/ffmpeg-wasm.sh: add 7.x fftools (dec/demux/enc/mux_init/sched +
  objpool/sync_queue/thread_queue); add -I./compat/stdbit (C23 header)
- build/fribidi.sh: serial install (c2man man-page race under -j)

fftools (pristine 7.1.5 + minimal wasm patches):
- vendored all 7.1.5 fftools/*.c/*.h (scheduler-based frontend)
- ffmpeg.c: main() -> ffmpeg(); ffprobe.c: main() -> ffprobe()
- ffprobe.c: rename program_name / program_birth_year /
  show_help_default (+_ffprobe) to avoid link collisions with ffmpeg.c
- bind.js: capture _ffmpeg()/_ffprobe() return value into Module.ret
  (7.x removed exit_program(); main() returns the code directly)

Still TODO: MT runtime hang; re-entrancy (init_globals equivalent);
progress bridge; timeout; ST-core threading decision; lib version
bumps + theora/vorbis trim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MT core now loads, remuxes, and transcodes (single-instance, incl.
auto codec-threading and re-entrant execs when logging paces them).

- ffmpeg-wasm.sh: bake pthread-worker-URL fix — emsdk 6.0.2 spawns
  pthread workers from _scriptName (the wrapper worker's URL, not the
  core's), hanging load(); post-build sed restores the
  mainScriptUrlOrBlob override so pthreads load the core. Fixes the
  silent load() hang.
- ffmpeg-wasm.sh: PTHREAD_POOL_SIZE 32 -> 64 — FFmpeg 7.x scheduler
  (per-component threads) + codec frame-threading overflowed a 32-pool
  and deadlocked mid-transcode (overflow pthread_create can't spawn
  while the worker main thread is blocked).
- ffmpeg.c: add init_globals() reset at the top of ffmpeg() — 7.x
  ffmpeg_cleanup() frees the input/output/filtergraph/decoder arrays but
  leaves nb_* counts and transcode_init_done/nb_output_dumped set, so a
  2nd exec() ran against stale state and hung.

Known remaining (runtime): (1) rapid successive transcodes have a
timing-sensitive scheduler race (masked by log pacing); (2) the test
suite creates a fresh 64-worker instance per describe block and exhausts
browser workers ("Maximum call stack size"); pool sizing vs multi-
instance needs tuning (likely cap default threads + smaller pool).
Also still TODO: progress bridge, timeout, ST-core decision, lib trim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-instance now transcodes repeatedly with no deadlock: -h -> N
sequential transcodes all succeed, without needing log pacing.

- PTHREAD_POOL_SIZE 64 -> 32 (matches the historical footprint; keeps
  per-instance worker count sane).
- Post-build: cap navigator.hardwareConcurrency to 8 so FFmpeg's
  av_cpu_count (sysconf -> hardwareConcurrency) requests at most ~8
  threads/codec. Uncapped, the 7.x scheduler + frame-threading requested
  ~2*cores threads, overflowed the pool, and deadlocked mid-transcode
  (the "race" that log pacing masked). Explicit -threads still overrides.
- Fix -o output extraction in the post-build patch step (printf '%s'
  dropped arg spaces so neither patch applied).

Known remaining: terminate() after a transcode does not reap the MT
core's pthread workers, so creating a fresh instance afterwards fails
("Maximum call stack size"). Single long-lived instance (the common
case) is unaffected. Still TODO: progress bridge, timeout, ST decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the single-long-lived-instance pattern (create/load once, reuse
for every operation, terminate once) instead of one instance per
describe block. This mirrors real-world usage and sidesteps the emsdk
6.0.2 pthread-pool leak that surfaces when many instances are created
and terminated in one page (see Phase 3 handoff).

The MT suite now runs cleanly; remaining failures are the not-yet-ported
progress and timeout bridges, not instance lifecycle.

Dropped the brittle "should list root dir" exact-count assertion (a
shared instance pre-writes video.mp4, changing the count); listDir stays
covered by the directory-API tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-implement the two wasm JS bridges against FFmpeg 7.x's scheduler
frontend (bind.js already wired them):

- send_progress (EM_JS -> Module.receiveProgress): computed in
  print_report() from pts / longest input duration, before the -stats
  early return so it always fires; emits 1.0 on the final report.
- is_timeout / get_timeout (EM_JS -> Module.timeout): checked in the
  transcode() scheduler loop. 7.x has no exit_program(), so a timeout
  breaks the loop and forces ret=1 (bind.js captures ffmpeg()'s return).
  The loop's poll period is tightened to the timeout when one is set
  (it otherwise wakes only every stats_period ~0.5s, too coarse for
  short timeouts); print_report self-rate-limits so stats are unaffected.

The MT wrapper suite (tests/ffmpeg.test.js) now passes 10/10: FS/dir
APIs, help, transcode+progress, timeout, abort. The MT-core *direct*
suite still fails — it runs the MT core on the page main thread where
Atomics.wait is illegal; pre-existing, needs a worker-based harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the theora, vorbis, and ogg builder stages, their COPYs into
ffmpeg-base, the --enable-libtheora/--enable-libvorbis configure flags,
and the -logg/-ltheora/-lvorbis* link flags. These are dead formats for
a web clip/stitch/H.264 workload; x264 (re-encode), vpx, opus, mp3, aac
(native), and webp remain.

MT core: 27.5 MB -> 24.9 MB; wrapper suite still 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clip/stitch workload burns in no subtitles or text overlays, so the
entire subtitle/text-shaping stack is removed: libass, freetype, fribidi,
harfbuzz builder stages, their COPY, the --enable-lib{freetype,fribidi,ass}
configure flags, and the -l{freetype,fribidi,harfbuzz,ass} link flags.

This removes three CVE-carrying libraries outright (freetype 2.10.4,
fribidi 1.0.9, harfbuzz 5.2.0) rather than bumping them, and shrinks the
MT core 24.9 MB -> 23.6 MB. Also disables the drawtext filter (shares
freetype) — re-add freetype if in-browser text overlays are needed later.

Wrapper suite still 10/10. (build/{freetype2,fribidi,harfbuzz,libass,
ogg,theora,vorbis}.sh are now unused; left in place, harmless.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `serve` script piped COOP/COEP/CORP headers to `http-server --headers`,
which is a silent no-op (http-server has no such flag) — so pages were
never cross-origin isolated and the MT (SharedArrayBuffer) tests could not
run against it. Add scripts/serve.js, a dependency-free Node static server
that actually sends the isolation headers and correct MIME types (esp.
application/wasm), and point `serve` at it.

MT wrapper suite now runs 10/10 against `npm run serve`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add correctness tests for the browser-side clipping workload: lossless
cut (-ss/-t -c copy) and lossless concat (-f concat -c copy) — both
verified working on the MT 7.1.5 core. Suite now 12/12.

Documents (in a comment) that multi-input filtergraphs (xfade
transitions, concat filter, overlay) deadlock on the MT 7.x core in
wasm and must run server-side; only stream-copy stitching is browser-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supply-chain hardening:
- x264 (was branch `4-cores`) and lame (was branch `master`) are now
  pinned to their branch-HEAD commit SHAs — those floating branches made
  the build non-reproducible.
- zlib 1.2.11 -> 1.3.1 from upstream madler/zlib (pinned by the v1.3.1
  commit SHA; the ffmpegwasm fork has no 1.3.1 tag). Fixes CVE-2018-25032
  and CVE-2022-37434.

Confirmed: buildkit resolves the commit-SHA git refs, zlib 1.3.1 is
embedded in the core, wrapper suite still 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite .github/workflows/CI.yml for the 7.1.5/emsdk-6.0.2 toolchain:
- checkout@v4, setup-node@v4 (Node 20), buildx v3; add least-privilege
  permissions block (was none; deprecated checkout@v2/setup-node@v2 before).
- Build the MT core via `make prd-mt` with GHA buildx cache (emsdk 6.0.2
  is multi-arch -> native amd64 on the runner). MT-only: no ST core exists
  on 7.x. Chromium comes from the puppeteer bundled by mocha-headless-chrome
  (npm ci), so no separate setup-chrome action.
- Run the MT suite via start-server-and-test against the fixed serve script.
- release job (on v* tags): build + tar core-mt/dist and ffmpeg/dist and
  attach to the GitHub Release, so a Go app can vendor from a targeted release.

Also:
- test:all -> MT-only (npm test now passes 12/12; ST/core-direct targets
  can't run on 7.x).
- scripts/serve.js returns 200 at "/" so start-server-and-test/wait-on's
  health check passes.

Note: CI itself is verified only once pushed to GitHub Actions; the flow
it runs is validated locally (npm test = 12/12).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Team-facing doc: what changed vs upstream (7.1.5, lean codec set,
MT-only), the browser capability map (cut/transcode/concat work;
multi-input filtergraphs run server-side), the required COOP/COEP
headers, single-instance usage, build steps, and how to vendor from a
tagged release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed package-lock.json is out of sync with the apps/* example
workspaces, so strict `npm ci` fails. Those apps aren't needed to build
or test the core; use `npm install` (lenient). Bump Node 20 -> 22 to
clear the EBADENGINE warning from an apps/* devDep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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