Initial Commit
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user