# @suaveplan/audio-codec-ffmpeg-wasm - Browser-tier ffmpeg.wasm AudioCodec plugin for @suaveplan/audio-codecs. Lazy-loaded @ffmpeg/core wasm inside a Web Worker; decode/encode for WAV/MP3/OGG/Opus/FLAC/AAC/WebM; AbortSignal, timeout watchdog, COI-gated multi-thread, OTel spans+counters+histograms, structured AUDIO_CODEC_FFMPEG_WASM.* error codes. Browser-tier `AudioCodec` plugin for [`@suaveplan/audio-codecs`](../audio-codecs) backed by [`@ffmpeg/ffmpeg`](https://github.com/ffmpegwasm/ffmpeg.wasm) running inside a dedicated Web Worker. Decodes and encodes `wav`, `mp3`, `ogg`, `opus`, `flac`, `aac`, and `webm`. ## Documentation tree - [codec](./src/codec/ffmpeg-wasm-codec.md) — This module assembles the public `FfmpegWasmCodec` class — the browser-tier `AudioCodec` plugin for `@suaveplan/audio-c… - [ffmpeg-wasm-codec](./src/codec/ffmpeg-wasm-codec.md) — This module assembles the public `FfmpegWasmCodec` class — the browser-tier `AudioCodec` plugin for `@suaveplan/audio-c… - [core-loader](./src/core-loader/core-loader.md) — This module owns the lazy resolution of the ffmpeg.wasm core module. It picks the single-thread `@ffmpeg/core` or the m… - [core-loader](./src/core-loader/core-loader.md) — This module owns the lazy resolution of the ffmpeg.wasm core module. It picks the single-thread `@ffmpeg/core` or the m… - [decode](./src/decode/decode-pipeline.md) — This module owns the pure logic of the decode pipeline: format sniffing from magic bytes, the ffmpeg argument list that… - [decode-pipeline](./src/decode/decode-pipeline.md) — This module owns the pure logic of the decode pipeline: format sniffing from magic bytes, the ffmpeg argument list that… - [encode](./src/encode/encode-pipeline.md) — This module owns the pure logic of the encode pipeline: validating the requested format against the supported set *befo… - [encode-pipeline](./src/encode/encode-pipeline.md) — This module owns the pure logic of the encode pipeline: validating the requested format against the supported set *befo… - [error-codes](./src/error-codes/error-codes.md) — This module declares the canonical error-code registry for `@suaveplan/audio-codec-ffmpeg-wasm`. It exposes the nine `A… - [error-codes](./src/error-codes/error-codes.md) — This module declares the canonical error-code registry for `@suaveplan/audio-codec-ffmpeg-wasm`. It exposes the nine `A… - [register](./src/register/register.md) — This module exposes the `register(registry, options?)` install helper — a single function that callers use to plug `Ffm… - [register](./src/register/register.md) — This module exposes the `register(registry, options?)` install helper — a single function that callers use to plug `Ffm… - [streaming-decoder](./src/streaming/streaming-decoder.md) — This module implements the `StreamingDecoder` contract from `@suaveplan/audio-codecs` over ffmpeg.wasm. The implementat… - [streaming-encoder](./src/streaming/streaming-encoder.md) — This module implements the `StreamingEncoder` contract from `@suaveplan/audio-codecs` over ffmpeg.wasm. The implementat… - [worker](./src/worker/worker-host.md) — This module owns the lifecycle of the dedicated Web Worker that runs the ffmpeg.wasm core. It serialises every codec ca… - [worker-host](./src/worker/worker-host.md) — This module owns the lifecycle of the dedicated Web Worker that runs the ffmpeg.wasm core. It serialises every codec ca… ## Install ```bash bun add @suaveplan/audio-codec-ffmpeg-wasm @suaveplan/audio-codecs # The wasm core packages are optional dependencies — install whichever you # plan to serve: bun add @ffmpeg/ffmpeg @ffmpeg/core # Optional: multi-thread core (~30 MB) for cross-origin-isolated pages bun add @ffmpeg/core-mt ``` ## Quick start ```ts import { CodecRegistry } from "@suaveplan/audio-codecs"; import { register } from "@suaveplan/audio-codec-ffmpeg-wasm/register/register"; const registry = new CodecRegistry(); const codec = register(registry, { corePath: "/static/ffmpeg-core.js", }); const doc = await registry.decode(mp3Bytes); const opus = await registry.encode(doc, "opus"); await codec.dispose(); ``` ## The 30 MB lazy-load disclaimer The `@ffmpeg/core` wasm payload is roughly 30 MB. It is **not** loaded when this package is imported, nor when `new FfmpegWasmCodec()` is constructed — it is loaded on the first call to `decode`, `encode`, `createDecoder`, or `createEncoder`. Subsequent calls reuse the loaded core. `codec.dispose()` tears the worker down and releases the core. ## Multi-thread (cross-origin isolation) Pass `enableMultithread: true` to opt into the `@ffmpeg/core-mt` build. This requires the page to be cross-origin-isolated: - Serve the page with `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. - `globalThis.crossOriginIsolated` must be `true` at runtime. If `enableMultithread` is `true` without COI, the codec throws `AUDIO_CODEC_FFMPEG_WASM.CROSS_ORIGIN_ISOLATION_REQUIRED` on first use **before** attempting the dynamic `import()`. If COI is true but the optional `@ffmpeg/core-mt` package is missing, the codec logs a warning and silently falls back to the single-thread core. ## Streaming honesty note The `StreamingDecoder` / `StreamingEncoder` factories from this codec are **buffered emulation** — ffmpeg.wasm is whole-file in / whole-file out, so `write(chunk)` only appends to an internal buffer and never invokes the worker; the actual decode/encode happens once when `flushAsync()` is awaited. Use this for chunked-source UX (progress bars, abortable downloads), not for true low-latency streaming. ## Public API - `class FfmpegWasmCodec implements AudioCodec` - `class FfmpegWasmStreamingDecoder implements StreamingDecoder` - `class FfmpegWasmStreamingEncoder implements StreamingEncoder` - `class WorkerHost` — worker lifecycle abstraction - `loadCore(opts)` — lazy core loader - `register(registry, options?)` — one-line install helper - `AUDIO_CODEC_FFMPEG_WASM_CODES` + `AudioCodecFfmpegWasmError` ## Error codes | Code | Thrown when | |---|---| | `AUDIO_CODEC_FFMPEG_WASM.CORE_LOAD_FAILED` | wasm core fetch / import failed | | `AUDIO_CODEC_FFMPEG_WASM.WORKER_SPAWN_FAILED` | Worker construction or pre-load error | | `AUDIO_CODEC_FFMPEG_WASM.DECODE_FAILED` | ffmpeg non-zero exit on decode | | `AUDIO_CODEC_FFMPEG_WASM.ENCODE_FAILED` | ffmpeg non-zero exit on encode | | `AUDIO_CODEC_FFMPEG_WASM.TIMEOUT` | Per-call watchdog fired | | `AUDIO_CODEC_FFMPEG_WASM.ABORTED` | `AbortSignal` propagated | | `AUDIO_CODEC_FFMPEG_WASM.UNSUPPORTED_CODEC` | Format outside the supported set, or Zod boundary failure | | `AUDIO_CODEC_FFMPEG_WASM.MEMORY_LIMIT_EXCEEDED` | wasm 4 GB linear memory cap hit | | `AUDIO_CODEC_FFMPEG_WASM.CROSS_ORIGIN_ISOLATION_REQUIRED` | `enableMultithread` without COI | ## Test seams For testing without a real worker, supply: - `workerFactory: () => Worker` — returns a controllable shim. - `coreLoader: () => Promise` — overrides the dynamic `import("@ffmpeg/core")`. - `coreMtLoader: () => Promise` — overrides the dynamic `import("@ffmpeg/core-mt")`. These are dependency-injection seams, not mocks; production callers omit them and the real `new Worker(coreUrl)` / dynamic `import()` path runs unchanged.