Files
2026-05-27 23:47:53 +01:00

128 lines
7.2 KiB
Markdown

# @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`.
<!-- begin:auto-docs -->
## 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…
<!-- end:auto-docs -->
## 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<unknown>` — overrides the dynamic
`import("@ffmpeg/core")`.
- `coreMtLoader: () => Promise<unknown>` — 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.