# OpenSpec Change: enterprise-audio-codec-ffmpeg-wasm - **Change ID**: enterprise-audio-codec-ffmpeg-wasm - **Package**: `@suaveplan/audio-codec-ffmpeg-wasm` - **Category**: browser - **Target path**: `packages/media/audio/audio-codec-ffmpeg-wasm/` - **Status**: PROPOSED - **Date**: 2026-05-15 - **Design**: `./design.md` - **Spec delta**: `./specs/audio-codec-ffmpeg-wasm/spec.md` --- ## Summary Deliver `@suaveplan/audio-codec-ffmpeg-wasm` on the genesis branch from scratch at enterprise quality. The package is the browser-tier concrete codec plugin that implements the `AudioCodec` plugin contract from `@suaveplan/audio-codecs` by wrapping `@ffmpeg/ffmpeg` (ffmpeg.wasm) and `@ffmpeg/core` running inside a Web Worker. It exposes a `FfmpegWasmCodec` class covering decode and encode for `wav`, `mp3`, `ogg`, `opus`, `flac`, `aac`, and `webm` — the same format surface as the native server-tier counterpart so the two are interchangeable at the `CodecRegistry` level. A single-line `register(registry)` helper installs the codec into a shared registry. The wasm core (~30 MB) is lazy-loaded on the first `decode`/`encode` call, never at module import. When the host page is cross-origin-isolated (`crossOriginIsolated === true`) and `enableMultithread` is set, the package loads the multi-threaded `@ffmpeg/core-mt` variant via `optionalDependencies`; otherwise it falls back to the single-thread core. The codec is genesis-original — there is no upstream counterpart to migrate. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. It is genesis-original: no upstream `../suaveplan/packages/audio-codec-ffmpeg-wasm` exists and no entry in `STATUS.md`'s `Pending` list — the change is authored under §0b's third clause ("does not exist in `Pending` or `Completed`"). --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/audio-codecs` (`workspace:^`) | `AudioCodec` interface, `CodecRegistry`, `DecodeResult`, streaming factory types | | `@suaveplan/audio-core` (`workspace:^`) | `AudioDocument`, `AudioPage` (consumed indirectly via the codec contract) | | `@suaveplan/streams` (`workspace:^`) | WHATWG `ReadableStream` / `TransformStream` primitives for chunked source feeding | | `@suaveplan/error` (`workspace:^`) | Base class for `AudioCodecFfmpegWasmError`; `AUDIO_CODEC_FFMPEG_WASM.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` shared abstractions | | `@ffmpeg/ffmpeg` (catalog entry — add) | Main-thread façade over ffmpeg.wasm; spawns the worker and drives MEMFS | | `@ffmpeg/core` (catalog entry — add) | Single-threaded wasm core (~30 MB) | | `@ffmpeg/core-mt` (`optionalDependencies`) | Multi-threaded wasm core — only loaded when `crossOriginIsolated && enableMultithread` | | `zod` (`catalog:forms`) | Schemas for `FfmpegWasmCodecOptions` and the `register` argument | | `@opentelemetry/api` (`catalog:observability`) | Spans + counter + histogram on decode and encode | | `tslib` (`catalog:build`) | TypeScript helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `happy-dom`, `fast-check`, `mitata` (all `catalog:testing`). The `@suaveplan/testing/runner` wrapper is the only path to test-runner primitives. > **Catalog action item:** `@ffmpeg/ffmpeg` and `@ffmpeg/core` are not currently in the root `package.json` `catalog` block. Adding both at exact pinned versions is the first task — see `tasks.md §1`. --- ## Public API ### Core types ```ts class AudioCodecFfmpegWasmError extends SuaveplanError interface FfmpegWasmCodecOptions { /** CDN URL for `@ffmpeg/core` wasm; mutually exclusive with `corePath`. */ coreUrl?: string /** Local URL for `@ffmpeg/core` wasm; mutually exclusive with `coreUrl`. */ corePath?: string /** Test seam — overrides the default `new Worker(...)` construction. */ workerFactory?: () => Worker /** * Opt into the multi-threaded `@ffmpeg/core-mt` variant. * Requires `crossOriginIsolated === true`; otherwise throws on first use. * Default `false` (auto-detect: if `crossOriginIsolated` is true, mt is preferred only when this flag is also true). */ enableMultithread?: boolean /** Receives progress in `[0, 1]` from ffmpeg's `progress` event. */ progressCallback?: (progress: number) => void /** Structured logger; defaults to a no-op. */ logger?: LoggerLike /** Injectable clock for OTel histograms and timeouts; defaults to `Date.now`-based. */ clock?: Clock } class FfmpegWasmCodec implements AudioCodec { readonly name: 'ffmpeg-wasm' readonly extensions: ['wav', 'mp3', 'ogg', 'opus', 'flac', 'aac', 'webm'] readonly mimeTypes: string[] constructor(options?: FfmpegWasmCodecOptions) canDecode(bytes: Uint8Array): boolean decode(bytes: Uint8Array | ArrayBuffer): Promise encode(pcm: Float32Array[], opts: EncodeOpts): Promise createDecoder(): StreamingDecoder createEncoder(): StreamingEncoder /** Terminates the worker and releases the loaded core. */ dispose(): Promise } /** One-line install helper. Constructs the codec with `options` and registers it. */ function register(registry: CodecRegistry, options?: FfmpegWasmCodecOptions): FfmpegWasmCodec ``` ### Error codes ```ts type AudioCodecFfmpegWasmErrorCode = | 'AUDIO_CODEC_FFMPEG_WASM.CORE_LOAD_FAILED' | 'AUDIO_CODEC_FFMPEG_WASM.WORKER_SPAWN_FAILED' | 'AUDIO_CODEC_FFMPEG_WASM.DECODE_FAILED' | 'AUDIO_CODEC_FFMPEG_WASM.ENCODE_FAILED' | 'AUDIO_CODEC_FFMPEG_WASM.TIMEOUT' | 'AUDIO_CODEC_FFMPEG_WASM.ABORTED' | 'AUDIO_CODEC_FFMPEG_WASM.UNSUPPORTED_CODEC' | 'AUDIO_CODEC_FFMPEG_WASM.MEMORY_LIMIT_EXCEEDED' | 'AUDIO_CODEC_FFMPEG_WASM.CROSS_ORIGIN_ISOLATION_REQUIRED' const AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA: Readonly> function registerAudioCodecFfmpegWasmErrorCodes(): void ``` ### Zod schemas ```ts FfmpegWasmCodecOptionsSchema ``` --- ## Module layout Five files per feature module, co-located with `.md` siblings per `CLAUDE.md` "Package Structure": ```text packages/media/audio/audio-codec-ffmpeg-wasm/ package.json tsconfig.json vitest.config.ts README.md src/ index.ts error-codes.ts error-codes.md error-codes.test.ts codec/ ffmpeg-wasm-codec.ts ffmpeg-wasm-codec.md ffmpeg-wasm-codec.types.ts ffmpeg-wasm-codec.schemas.ts ffmpeg-wasm-codec.test.ts index.ts worker/ worker-host.ts worker-host.md worker-host.types.ts worker-host.test.ts index.ts core-loader/ core-loader.ts core-loader.md core-loader.types.ts core-loader.test.ts index.ts decode/ decode-pipeline.ts decode-pipeline.md decode-pipeline.types.ts decode-pipeline.test.ts index.ts encode/ encode-pipeline.ts encode-pipeline.md encode-pipeline.types.ts encode-pipeline.test.ts index.ts streaming/ streaming-decoder.ts streaming-decoder.md streaming-decoder.test.ts streaming-encoder.ts streaming-encoder.md streaming-encoder.test.ts index.ts register/ register.ts register.md register.test.ts index.ts __benchmarks__/ decode.bench.ts encode.bench.ts tests/ purity.test.ts exports.test.ts integration.test.ts ``` Every feature module's `index.ts` is an explicit named-export barrel per `CLAUDE.md` rule 10; no `export *`. --- ## Impact **Breaking changes**: none — fresh build, package does not exist on genesis. **Prerequisites**: `@suaveplan/audio-codecs`, `@suaveplan/audio-core`, `@suaveplan/streams`, `@suaveplan/error`, `@suaveplan/types` published to Verdaccio. All five are scheduled in the same `enterprise-audio-*` batch and must land before this package builds. ### Affected dependents `@suaveplan/audio-codec-ffmpeg-wasm` is a leaf codec plugin — nothing in the workspace currently depends on it. It is installed at runtime by browser-tier applications via the `register(registry)` helper; those applications are out-of-tree consumers and do not appear in `bunx turbo ls --filter=...@suaveplan/audio-codec-ffmpeg-wasm`. There is therefore no in-repo dependent surface to sweep. - _(no in-repo dependents)_ — [x] no action needed (reason: leaf plugin; consumers are out-of-tree browser applications) Once landed, any subsequent change to this package's public surface MUST run `bunx turbo test --filter=...@suaveplan/audio-codec-ffmpeg-wasm` per `openspec/AGENTS.md` §13 step 10 so that any future in-repo dependent (e.g. a browser audio engine that bundles a default codec set) is covered automatically.