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

8.6 KiB

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

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

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

FfmpegWasmCodecOptionsSchema

Module layout

Five files per feature module, co-located with .md siblings per CLAUDE.md "Package Structure":

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.