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

247 lines
7.5 KiB
Markdown

# OpenSpec Change: enterprise-audio-codec-ffmpeg-native
- **Change ID**: enterprise-audio-codec-ffmpeg-native
- **Package**: `@suaveplan/audio-codec-ffmpeg-native`
- **Category**: server
- **Target path**: `packages/media/audio/audio-codec-ffmpeg-native/`
- **Status**: PROPOSED
- **Date**: 2026-05-15
- **Design**: ./design.md
- **Spec delta**: ./specs/audio-codec-ffmpeg-native/spec.md
---
## Summary
Deliver `@suaveplan/audio-codec-ffmpeg-native` as a server-tier concrete codec plugin that
implements the `AudioCodec` contract from `@suaveplan/audio-codecs` by shelling out to the
system `ffmpeg` binary via the `fluent-ffmpeg` CLI wrapper. The package bundles a static
binary via `@ffmpeg-installer/ffmpeg` (declared as `optionalDependencies` so consumers may
override) and streams audio through ffmpeg's stdin/stdout to support decode, encode, and
true chunked streaming for wav, mp3, ogg, opus, flac, aac/m4a, webm/matroska, and wma —
without ever buffering the full file in memory. Concurrency is bounded by an in-package
semaphore, every spawn carries an `AbortSignal` that escalates `SIGTERM` → `SIGKILL`, and
the package exposes a one-line `register(registry)` helper plus `@suaveplan/streams`-based
`TransformStream` adapters for pipeline composition.
---
## 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. The package is genesis-original (no upstream parity
checklist required); the `AudioCodec` contract it plugs into is defined by the
in-flight `enterprise-audio-codecs` change.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/audio-codecs` (`workspace:^`) | `AudioCodec`, `CodecRegistry`, `DecodeResult`, `StreamingDecoder`, `StreamingEncoder` contracts |
| `@suaveplan/audio-core` (`workspace:^`) | `AudioPage`, `AudioFormat` aliases |
| `@suaveplan/streams` (`workspace:^`) | `TransformStream`, `ReadableStream`, `WritableStream` adapters with backpressure |
| `@suaveplan/error` (`workspace:^`) | `SuaveplanError` base class, `defineCodes`, structured error registry |
| `@suaveplan/types` (`workspace:^`) | `Clock`, `LoggerLike` injectables |
| `fluent-ffmpeg` (`catalog:audio`) | CLI wrapper used to compose ffmpeg invocations |
| `@ffmpeg-installer/ffmpeg` (`catalog:audio`, `optionalDependencies`) | Bundled static ffmpeg binary, default `binaryPath` |
| `zod` (`catalog:forms`) | Public options validation |
| `@opentelemetry/api` (`catalog:observability`) | Spans, counters, histograms, gauges |
| `tslib` (`catalog:build`) | TS runtime helpers |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`),
`@suaveplan/testing` (`workspace:^`). Note: catalog entries for `fluent-ffmpeg` and
`@ffmpeg-installer/ffmpeg` must be added to root `package.json` `catalog.audio` if not
already present — this is tracked in `tasks.md` step 1.
---
## Public API
### Core types
```ts
import type {
AudioCodec,
CodecRegistry,
DecodeResult,
EncodeOpts,
StreamingDecoder,
StreamingEncoder,
} from "@suaveplan/audio-codecs";
import type { Clock, LoggerLike } from "@suaveplan/types";
export interface FfmpegNativeCodecOptions {
binaryPath?: string;
binaryProvider?: () => string;
concurrencyLimit?: number;
timeoutMs?: number;
logger?: LoggerLike;
clock?: Clock;
}
export class FfmpegNativeCodec implements AudioCodec {
readonly name: string;
readonly extensions: readonly string[];
readonly mimeTypes: readonly string[];
constructor(options?: FfmpegNativeCodecOptions);
canDecode(bytes: Uint8Array): boolean;
decode(bytes: Uint8Array, opts?: { signal?: AbortSignal }): Promise<DecodeResult>;
encode(
pcm: Float32Array[],
opts: EncodeOpts & { signal?: AbortSignal },
): Promise<Uint8Array>;
createDecoder(opts?: { format: string; signal?: AbortSignal }): StreamingDecoder;
createEncoder(opts: EncodeOpts & { signal?: AbortSignal }): StreamingEncoder;
}
export function register(registry: CodecRegistry, options?: FfmpegNativeCodecOptions): void;
```
### Error codes
```ts
export type AudioCodecFfmpegNativeErrorCode =
| "AUDIO_CODEC_FFMPEG_NATIVE.BINARY_MISSING"
| "AUDIO_CODEC_FFMPEG_NATIVE.SPAWN_FAILED"
| "AUDIO_CODEC_FFMPEG_NATIVE.DECODE_FAILED"
| "AUDIO_CODEC_FFMPEG_NATIVE.ENCODE_FAILED"
| "AUDIO_CODEC_FFMPEG_NATIVE.TIMEOUT"
| "AUDIO_CODEC_FFMPEG_NATIVE.ABORTED"
| "AUDIO_CODEC_FFMPEG_NATIVE.UNSUPPORTED_CODEC"
| "AUDIO_CODEC_FFMPEG_NATIVE.INVALID_PCM";
export class AudioCodecFfmpegNativeError extends SuaveplanError<AudioCodecFfmpegNativeErrorCode> {}
export const AUDIO_CODEC_FFMPEG_NATIVE_ERROR_METADATA: Readonly<
Record<AudioCodecFfmpegNativeErrorCode, CodeMetadata>
>;
export function registerAudioCodecFfmpegNativeErrorCodes(): void;
```
### Zod schemas (sibling `*.schemas.ts`)
```ts
export const FfmpegNativeCodecOptionsSchema: z.ZodType<FfmpegNativeCodecOptions>;
export const EncodeOptsSchema: z.ZodType<EncodeOpts>;
```
---
## Module layout
Five-file pattern per feature module (`feature.ts`, `feature.test.ts`, `feature.types.ts`,
`feature.md`, `index.ts`); barrels use explicit named re-exports (rule 10 / §6a). Every
test file imports runner primitives from `@suaveplan/testing/runner` (rule 11 / §7).
```text
packages/media/audio/audio-codec-ffmpeg-native/
package.json
tsconfig.json
vitest.config.ts
README.md
src/
index.ts
error-codes.ts
error-codes.md
error-codes.test.ts
constants/
index.ts
codec/
codec.ts
codec.md
codec.test.ts
codec.types.ts
codec.schemas.ts
index.ts
binary/
binary.ts
binary.md
binary.test.ts
binary.types.ts
index.ts
runner/
runner.ts
runner.md
runner.test.ts
runner.types.ts
index.ts
semaphore/
semaphore.ts
semaphore.md
semaphore.test.ts
semaphore.types.ts
index.ts
decode/
decode.ts
decode.md
decode.test.ts
decode.types.ts
index.ts
encode/
encode.ts
encode.md
encode.test.ts
encode.types.ts
index.ts
stream/
stream-decoder.ts
stream-decoder.md
stream-decoder.test.ts
stream-decoder.types.ts
stream-encoder.ts
stream-encoder.md
stream-encoder.test.ts
stream-encoder.types.ts
index.ts
abort/
abort.ts
abort.md
abort.test.ts
abort.types.ts
index.ts
otel/
otel.ts
otel.md
otel.test.ts
otel.types.ts
index.ts
register/
register.ts
register.md
register.test.ts
register.types.ts
index.ts
__benchmarks__/
codec.bench.ts
tests/
purity.test.ts
integration.test.ts
exports.test.ts
```
---
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/audio-codec-ffmpeg-native` to enumerate.
Package does not exist yet:
- **No dependents — fresh package.** No downstream consumers exist on genesis at proposal
time; future consumers (`@suaveplan/audio-codecs` default-registry server bundle,
application-side `register(registry, ...)` call sites) will pick this up by direct
workspace dep when published.
Breaking changes: none — fresh build.
Prerequisites: `@suaveplan/audio-codecs`, `@suaveplan/audio-core`, `@suaveplan/streams`,
`@suaveplan/error`, `@suaveplan/types` published (or workspace-resolvable) on genesis;
catalog entries for `fluent-ffmpeg` and `@ffmpeg-installer/ffmpeg` declared in the root
`package.json` `catalog.audio` block.