Files
suaveplan/openspec/archive/2026-05-23-enterprise-audio-codec-ffmpeg-wasm/proposal.md
2026-05-27 23:29:53 +01:00

210 lines
8.6 KiB
Markdown

# 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<DecodeResult>
encode(pcm: Float32Array[], opts: EncodeOpts): Promise<Uint8Array>
createDecoder(): StreamingDecoder
createEncoder(): StreamingEncoder
/** Terminates the worker and releases the loaded core. */
dispose(): Promise<void>
}
/** 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<Record<AudioCodecFfmpegWasmErrorCode, CodeMetadata>>
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.