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

12 KiB

Tasks: @suaveplan/audio-codec-ffmpeg-wasm

Ordered implementation checklist for the enterprise-audio-codec-ffmpeg-wasm change. Every numbered task corresponds to scenarios in ./specs/audio-codec-ffmpeg-wasm/spec.md. Do not start a task whose prerequisites are still pending in STATUS.md.


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold the package

  • 1.1 Add @ffmpeg/ffmpeg and @ffmpeg/core to the root package.json catalog block at exact pinned versions. Note in the PR description that this is the only catalog addition required by this change.
  • 1.2 Create packages/media/audio/audio-codec-ffmpeg-wasm/ with package.json ("name": "@suaveplan/audio-codec-ffmpeg-wasm", "version": "0.1.0", "browser": true, "sideEffects": false, "type": "module"), workspace deps on @suaveplan/audio-codecs, audio-core, error, types, streams all at workspace:^, runtime deps on @ffmpeg/ffmpeg, @ffmpeg/core, zod catalog:forms, @opentelemetry/api catalog:observability, tslib catalog:build, and optionalDependencies: { "@ffmpeg/core-mt": "<pinned>" }.
  • 1.3 Declare one exports entry per feature module plus the root . barrel (see proposal "Module layout").
  • 1.4 Add tsconfig.json extending the shared base; add vitest.config.ts using createTestConfig() from @suaveplan/testing/config with pool: 'forks' and setupFiles: ['@suaveplan/testing/polyfills']. Configure happy-dom for any test that touches Worker.
  • 1.5 Add README.md at the package root only (no category READMEs).
  • 1.6 Run bun scripts/folder-reorg/generate-manifest.ts to refresh MANIFEST.md.

2. Types and error codes

  • 2.1 Author src/codec/ffmpeg-wasm-codec.types.ts with FfmpegWasmCodecOptions, LoadedCore, WorkerMessage union, and internal pipeline types.
  • 2.2 Author src/codec/ffmpeg-wasm-codec.schemas.ts exporting FfmpegWasmCodecOptionsSchema (Zod) — uses import z from "zod" per MEMORY.md. Reject coreUrl and corePath set simultaneously with .superRefine.
  • 2.3 Author src/error-codes.ts defining AudioCodecFfmpegWasmError, AudioCodecFfmpegWasmErrorCode, AUDIO_CODEC_FFMPEG_WASM_ERROR_METADATA, and registerAudioCodecFfmpegWasmErrorCodes() via @suaveplan/error's defineCodes. Cover all 9 codes from the spec.
  • 2.4 Author src/error-codes.test.ts asserting every code is reachable from a real call site (one assertion per code; cross-references the other test files).

3. Worker host (src/worker/)

  • 3.1 Implement worker-host.ts: wraps @ffmpeg/ffmpeg's FFmpeg instance, owns the Worker lifecycle, exposes submitDecode, submitEncode, terminate, and a progress event hook.
  • 3.2 AbortSignal plumbing: pre-aborted signal short-circuits before any worker message; mid-call abort calls worker.terminate() and rejects pending calls with AUDIO_CODEC_FFMPEG_WASM.ABORTED.
  • 3.3 Timeout watchdog using injectable Clock; defaults 60s decode / 120s encode; maps to AUDIO_CODEC_FFMPEG_WASM.TIMEOUT.
  • 3.4 Crash + OOM detection: parse stderr for Cannot enlarge memory arrays / OutOfMemory → AUDIO_CODEC_FFMPEG_WASM.MEMORY_LIMIT_EXCEEDED; pre-load worker error → AUDIO_CODEC_FFMPEG_WASM.WORKER_SPAWN_FAILED.
  • 3.5 Honour opts.workerFactory test seam.
  • 3.6 Tests in worker-host.test.ts covering scenarios from spec Requirements "Worker isolation", "AbortSignal propagation", "Timeout watchdog", and "Memory-limit detection".

4. Core loader (src/core-loader/)

  • 4.1 Implement core-loader.ts: resolve core URL (precedence corePath → coreUrl → bundled default), memoise the loaded core per codec instance.
  • 4.2 COI gate: when enableMultithread === true and globalThis.crossOriginIsolated !== true, throw AUDIO_CODEC_FFMPEG_WASM.CROSS_ORIGIN_ISOLATION_REQUIRED before attempting import("@ffmpeg/core-mt").
  • 4.3 Optional-dep fallback: when COI is true but import("@ffmpeg/core-mt") rejects, log a warning via opts.logger and fall back to @ffmpeg/core. Do not throw.
  • 4.4 Failure path: any load failure throws AUDIO_CODEC_FFMPEG_WASM.CORE_LOAD_FAILED with the resolved URL in context.
  • 4.5 Tests in core-loader.test.ts covering "Lazy core load on first use" and "Cross-origin-isolation gate" scenarios.

5. Decode pipeline (src/decode/)

  • 5.1 Implement decode-pipeline.ts: sniff format from magic bytes → ffmpeg.writeFile("input.<ext>", bytes) → run probe pass for sampleRate/channels → run -f f32le extraction → ffmpeg.readFile("output.raw") → deinterleave to Float32Array[].
  • 5.2 MEMFS cleanup: deleteFile both entries on success and on failure (try/finally).
  • 5.3 ffmpeg non-zero exit → AUDIO_CODEC_FFMPEG_WASM.DECODE_FAILED with stderr tail.
  • 5.4 Tests in decode-pipeline.test.ts covering "Decode pipeline" scenarios.

6. Encode pipeline (src/encode/)

  • 6.1 Implement encode-pipeline.ts: per-format command builders for wav, mp3, ogg, opus, flac, aac, webm, honouring bitrate/quality options.
  • 6.2 Interleave Float32Array[] → single Float32Array → Uint8Array view → MEMFS write.
  • 6.3 Unsupported format check before posting any worker message → AUDIO_CODEC_FFMPEG_WASM.UNSUPPORTED_CODEC.
  • 6.4 MEMFS cleanup (try/finally on both entries).
  • 6.5 ffmpeg non-zero exit → AUDIO_CODEC_FFMPEG_WASM.ENCODE_FAILED.
  • 6.6 Tests in encode-pipeline.test.ts covering "Encode pipeline" scenarios for at least wav, mp3, and opus.

7. Streaming emulation (src/streaming/)

  • 7.1 Implement streaming-decoder.ts: write(chunk) appends to internal buffer and returns null; flush() runs through the decode pipeline once.
  • 7.2 Implement streaming-encoder.ts as the symmetric mirror.
  • 7.3 Idempotent flush(): subsequent calls return null / empty bytes.
  • 7.4 Progress events fire only during flush().
  • 7.5 Tests in streaming-decoder.test.ts and streaming-encoder.test.ts covering "Buffered streaming emulation" scenarios.

8. Codec assembly (src/codec/)

  • 8.1 Implement ffmpeg-wasm-codec.ts: FfmpegWasmCodec class wiring constructor validation (Zod) → core loader → worker host → decode/encode pipelines → createDecoder / createEncoder → dispose.
  • 8.2 canDecode(bytes) uses @suaveplan/audio-codecs's magic sniff plus the probed encoder list to filter unsupported variants.
  • 8.3 Progress callback wiring with try/catch around the user callback; exceptions logged via opts.logger, never aborts the call.
  • 8.4 Tests in ffmpeg-wasm-codec.test.ts covering "AudioCodec contract conformance", "Progress callback wiring", "Zod boundary validation", and OTel scenarios.

9. Register helper (src/register/)

  • 9.1 Implement register.ts: validates options, constructs codec, calls registry.register(codec), returns the instance.
  • 9.2 Tests in register.test.ts covering the install scenario and the Zod rejection scenario.

10. OpenTelemetry instrumentation

  • 10.1 Wire audio-codec-ffmpeg-wasm.decode and audio-codec-ffmpeg-wasm.encode spans, with format and multithread attributes.
  • 10.2 Increment audio_codec_ffmpeg_wasm_decode_total / audio_codec_ffmpeg_wasm_encode_total with format and outcome labels.
  • 10.3 Record audio_codec_ffmpeg_wasm_decode_duration_ms / audio_codec_ffmpeg_wasm_encode_duration_ms driven by opts.clock.
  • 10.4 Verify no import-time side effects via tests/purity.test.ts.

11. Tests harness

  • 11.1 Every test file imports runner primitives from @suaveplan/testing/runner only — never vitest, never bun:test (rule 11).
  • 11.2 Use happy-dom for tests that touch Worker; supply a hand-rolled Worker shim that lets us script worker messages and error events.
  • 11.3 Use a workerFactory test seam to inject fakes for unit tests; reserve a real-worker smoke test for tests/integration.test.ts under Bun-runs-in-Node mode.
  • 11.4 Every it() / test() carries { timeout: 5000 } (30000 for the integration suite). No mocks of @ffmpeg/ffmpeg itself — only the worker boundary is faked.
  • 11.5 Property test: decode(encode(pcm)) ≈ pcm for lossless formats (wav, flac).
  • 11.6 tests/purity.test.ts greps built dist/ for forbidden Node API references and for static imports of @ffmpeg/core-mt.
  • 11.7 tests/exports.test.ts asserts each subpath export resolves and that no test file imports vitest or bun:test directly.

12. Benchmarks

  • 12.1 src/__benchmarks__/decode.bench.ts — decode a 5-minute WAV and a 5-minute MP3 with mitata.
  • 12.2 src/__benchmarks__/encode.bench.ts — encode 5-minute PCM to MP3, OPUS, and FLAC.
  • 12.3 Document the multi-thread vs single-thread delta in the benchmark output comments.

13. Documentation

  • 13.1 Every implementation .ts has a sibling .md (Purpose ≥2 sentences, Features, Usage: Basic, Usage: Advanced, API Reference, Implementation Notes; ≥200 words excl. code; both simple AND advanced examples).
  • 13.2 Excluded from the .md rule: .test.ts, .types.ts (inside a feature dir), barrel index.ts, *.schemas.ts (Zod schemas only).
  • 13.3 README.md covers: install, quick-start register(registry) usage, the COI requirement for enableMultithread, the 30 MB lazy-load disclaimer, and the buffered-streaming honesty note.
  • 13.4 bunx turbo docs:check --filter=@suaveplan/audio-codec-ffmpeg-wasm is green.

14. Pre-publish gates

  • 14.1 bunx turbo lint typecheck test --filter=@suaveplan/audio-codec-ffmpeg-wasm is green.
  • 14.2 bunx turbo test:coverage --filter=@suaveplan/audio-codec-ffmpeg-wasm reports 100% on all touched modules with provider: 'istanbul'.
  • 14.3 bun scripts/lint/check-test-runner-imports.ts passes for the package.
  • 14.4 No biome-ignore comments anywhere in src/ or tests/ (rule 9).
  • 14.5 No export * in any non-root barrel; root barrel uses explicit named exports or namespace re-exports (rule 10).
  • 14.6 bun scripts/folder-reorg/generate-manifest.ts re-run; resulting MANIFEST.md diff committed.

15. Fold the spec

  • 15.1 Fold specs/audio-codec-ffmpeg-wasm/spec.md into openspec/specs/audio-codec-ffmpeg-wasm/spec.md per §14 archival workflow. openspec/specs/ is untouched until this step.

16. Archive

  • 16.1 Move openspec/changes/enterprise-audio-codec-ffmpeg-wasm/ to openspec/archive/<yyyy-mm-dd>-enterprise-audio-codec-ffmpeg-wasm/ per §14.
  • 16.2 Run openspec validate after the move to confirm the canonical spec parses.