Files
2026-05-27 23:29:53 +01:00

7.5 KiB

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

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

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)

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).

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.