Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.2 KiB
@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 backed by
@ffmpeg/ffmpeg running inside
a dedicated Web Worker. Decodes and encodes wav, mp3, ogg, opus,
flac, aac, and webm.
Documentation tree
- codec — This module assembles the public
FfmpegWasmCodecclass — the browser-tierAudioCodecplugin for `@suaveplan/audio-c…- ffmpeg-wasm-codec — This module assembles the public
FfmpegWasmCodecclass — the browser-tierAudioCodecplugin for `@suaveplan/audio-c…
- ffmpeg-wasm-codec — This module assembles the public
- core-loader — This module owns the lazy resolution of the ffmpeg.wasm core module. It picks the single-thread
@ffmpeg/coreor the m…- core-loader — This module owns the lazy resolution of the ffmpeg.wasm core module. It picks the single-thread
@ffmpeg/coreor the m…
- core-loader — This module owns the lazy resolution of the ffmpeg.wasm core module. It picks the single-thread
- decode — This module owns the pure logic of the decode pipeline: format sniffing from magic bytes, the ffmpeg argument list that…
- decode-pipeline — This module owns the pure logic of the decode pipeline: format sniffing from magic bytes, the ffmpeg argument list that…
- encode — This module owns the pure logic of the encode pipeline: validating the requested format against the supported set *befo…
- encode-pipeline — This module owns the pure logic of the encode pipeline: validating the requested format against the supported set *befo…
- error-codes — This module declares the canonical error-code registry for
@suaveplan/audio-codec-ffmpeg-wasm. It exposes the nine `A…- error-codes — This module declares the canonical error-code registry for
@suaveplan/audio-codec-ffmpeg-wasm. It exposes the nine `A…
- error-codes — This module declares the canonical error-code registry for
- register — This module exposes the
register(registry, options?)install helper — a single function that callers use to plug `Ffm…- register — This module exposes the
register(registry, options?)install helper — a single function that callers use to plug `Ffm…
- register — This module exposes the
- streaming-decoder — This module implements the
StreamingDecodercontract from@suaveplan/audio-codecsover ffmpeg.wasm. The implementat… - streaming-encoder — This module implements the
StreamingEncodercontract from@suaveplan/audio-codecsover ffmpeg.wasm. The implementat… - worker — This module owns the lifecycle of the dedicated Web Worker that runs the ffmpeg.wasm core. It serialises every codec ca…
- worker-host — This module owns the lifecycle of the dedicated Web Worker that runs the ffmpeg.wasm core. It serialises every codec ca…
Install
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
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-originandCross-Origin-Embedder-Policy: require-corp. globalThis.crossOriginIsolatedmust betrueat 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 AudioCodecclass FfmpegWasmStreamingDecoder implements StreamingDecoderclass FfmpegWasmStreamingEncoder implements StreamingEncoderclass WorkerHost— worker lifecycle abstractionloadCore(opts)— lazy core loaderregister(registry, options?)— one-line install helperAUDIO_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 dynamicimport("@ffmpeg/core").coreMtLoader: () => Promise<unknown>— overrides the dynamicimport("@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.