247 lines
7.5 KiB
Markdown
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.
|