210 lines
8.6 KiB
Markdown
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.
|