# FFmpeg WebAssembly build record

av-tools ships the locally rebuilt, reviewed core profile
`0.12.10-reviewed-stack5m.1` in single-thread and multithread forms. The
browser wrapper is `@ffmpeg/ffmpeg` **0.12.15** and helpers are
`@ffmpeg/util` **0.12.2**. All versions and build inputs are exact, not ranges.

## Underlying build

The base ffmpeg.wasm upstream release is `v12.15` at
`71aa99d37c02a7b4c435275ca9ef50e612f6efa1`. Its Dockerfile selects:

- FFmpeg **n5.1.4** (libavcodec 59.37.100);
- Emscripten SDK **3.1.40**;
- production `-O3 -msimd128`;
- pthread flags for the MT build and explicit thread disabling for ST.

The reviewed profile then applies upstream ffmpeg.wasm commit
`b409e36475bc21f0451b5b1e1d126fa82871439a` to select a 5 MiB Emscripten stack.
That upstream fix addresses the libopus zero-byte/out-of-bounds failures
reported in issues `#591` and `#775`. The local driver additionally makes the
upstream container-stage dependency isolation explicit, disables native x265
assembly for the wasm32 build, bounds parallel jobs and refuses output that
contains the maintainer's repository or SDK paths.

The build configuration embedded in both copied WASM files is:

```text
--target-os=none
--arch=x86_32
--enable-cross-compile
--disable-asm
--disable-stripping
--disable-programs
--disable-doc
--disable-debug
--disable-runtime-cpudetect
--disable-autodetect
--nm=emnm
--ar=emar
--ranlib=emranlib
--cc=emcc
--cxx=em++
--objcc=emcc
--dep-cc=emcc
--extra-cflags='-I../../../../install/include -O3 -msimd128'
--extra-cxxflags='-I../../../../install/include -O3 -msimd128'
--enable-gpl
--enable-libx264
--enable-libx265
--enable-libvpx
--enable-libmp3lame
--enable-libtheora
--enable-libvorbis
--enable-libopus
--enable-zlib
--enable-libwebp
--enable-libfreetype
--enable-libfribidi
--enable-libass
--enable-libzimg
```

The ST string also has `--disable-pthreads --disable-w32threads
--disable-os2threads`; the MT build supplies Emscripten pthread flags instead.
At runtime the application executes `-version`, `-buildconf`, `-formats`,
`-codecs`, `-encoders` and `-filters` sequentially. Presets are enabled only
when the active core reports every required muxer, encoder and filter. Build
flags alone are not treated as a capability guarantee.

## External libraries

The upstream build references x264 `4-cores`, x265 3.4, libvpx v1.13.1, LAME
`master`, Ogg v1.3.4, Theora v1.1.1, Opus v1.3.1, Vorbis v1.3.3, zlib v1.2.11,
libwebp v1.3.2, FreeType VER-2-10-4, FriBidi v1.0.9, HarfBuzz 5.2.0, libass
0.15.0 and zimg release-3.0.5. These resolve to x264
`33cac6b77d5b9259c552156013a817ab23119612`, x265
`2bb5520e9596f361bf0ed81b3b8da0d7fd999069` and LAME
`2badea1974ae36cb8312afe99cff1e6b3b5decee`. All three commits predate the
ffmpeg.wasm v12.15 release; the branch-based x264/LAME commits remain the
respective branch tips. `SOURCE.md` records retrieval commands.

## Shipped files

| Mode | File                       |    Bytes | SHA-256                                                            |
| ---- | -------------------------- | -------: | ------------------------------------------------------------------ |
| ST   | `st/ffmpeg-core.js`        |   111804 | `80c05d79d0e4e9434977b76cb851d10ddf2bfd378570178700d2c43b8afdcb24` |
| ST   | `st/ffmpeg-core.wasm`      | 32232580 | `fe41ddc77220cef6c04f5d48eeffcaaaef179ad270642e098f9945013aa0c9dc` |
| MT   | `mt/ffmpeg-core.js`        |   128953 | `4f2650099ab70cb2583951c0421147c62bea6c18dfbba5cfae4d7698b5d0ab62` |
| MT   | `mt/ffmpeg-core.wasm`      | 32718455 | `abbead010cb0448b26f01a36a3c8c03a6b04aca07812ddd4719a7f4b6bf4b645` |
| MT   | `mt/ffmpeg-core.worker.js` |     2115 | `f77898d631dc010b45c29c23cb4379c611a7d7b131bf591d08a656bb729a4ca3` |

The deterministic five-file repository artifact is 21,338,974 bytes with
SHA-256
`ebc068c6d096de55ae3335b04b7121130665f5b0ffa7a828067122733f55185f`.

`npm run vendor:ffmpeg` verifies and unpacks the deterministic tracked core
artifact to
`public/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/`;
`npm run vendor:verify-runtime` checks the build profile,
source/toolchain/patch identity, tracked artifact, generated paths, sizes and
digests without requiring a release-directory download. The stricter
`npm run vendor:verify` release gate also hashes the build-qualified
Corresponding Source archive. Vite then copies the verified bytes into `dist`.
A normal production build performs no network download and cannot fall back
to the older published npm core bytes.

## Multithread Chromium guardrail

The pinned multithread core can stall in Chromium when FFmpeg lets codecs use
the browser's full reported logical-core count. This is tracked upstream in
[ffmpeg.wasm issue 654](https://github.com/ffmpegwasm/ffmpeg.wasm/issues/654).
av-tools keeps the pthread core but adds `-threads 2` as an input and output
codec option for MT jobs. ST arguments remain unchanged. A regular browser
test performs an actual MT H.264/AAC conversion; loading the core alone is not
accepted as functional evidence.

Every execution also has a watchdog: an explicit shorter plan timeout wins;
otherwise the timeout is at least two minutes, scales to thirty times the
expected media duration, and is capped at the displayed four-hour product
limit. Timeout is still a failure, not a successful partial export.

## Long-session heap guardrail

The pinned Emscripten core was observed to end a later heterogeneous browser
workflow with a bare `Aborted()` even though the same command passed in a
fresh core and all per-job virtual files had been removed. Its `tile` filter
also aborted deterministically on a fourth consecutive contact sheet in local
stress runs. av-tools therefore refreshes an idle core before its seventeenth
media execution and before a fourth contact-sheet execution. Application queue
state, sources and completed results remain outside the worker and are not
discarded. A bare core abort also triggers recreation after cleanup so a
manual or queued retry starts on a fresh instance.

This is a conservative browser-memory guardrail, not a guarantee that sixteen
large jobs or three large contact sheets fit in memory. Cancellation and
failed filesystem-cleanup recovery continue to recreate the core immediately.

## Opus regression evidence

Published core 0.12.10 predates the upstream stack-size fix and stalled in the
real Opus preset path even though it advertised the `opus` muxer and
`libopus` encoder. Capability discovery alone was therefore not accepted as
enablement evidence.

On 2026-07-26 the Chromium harness used the checked
`tests/fixtures/generated/tone.wav` fixture (32,102 bytes, SHA-256
`0fd1f54d6be2f1cc7fd778775dd29cf184770d3716cf3394bf03ed54ac264c3c`)
and the real built-in `audio-opus` preset/command planner. After rebuilding
with the reviewed stack profile, the isolated ST and MT runs each:

- loaded the requested mode rather than a fallback;
- mapped PCM audio to `opus (libopus)`;
- opened the declared Opus output and reported its libopus encoder;
- returned non-empty bytes within its independent containment limit;
- re-probed the result as positive-duration Opus audio.

```sh
# Record JSON evidence.
npm run evidence:opus

# Positive Opus enablement/release gate.
npm run test:opus-regression
```

The Opus audio and VP9/Opus presets are enabled only when the reviewed-core
lock records both passing modes and runtime discovery finds every preset
requirement. This is exact Chromium fixture evidence, not a claim about every
browser, input or possible Opus build.

## Licence consequence

The actual core build enables GPL and links external codecs. The wrapper/util
are MIT. av-tools is GPL-3.0-or-later, and the distributed application must
not be described as MIT-only. Publish the reviewed Corresponding Source and
external notices with the binary release. See `LICENSE`, `LICENSES/`,
`THIRD_PARTY_NOTICES.md` and `SOURCE.md`.
