Files
skills/docs/media/audio/audio-codec-ffmpeg-native/src/codec/codec.md
2026-05-29 08:20:56 +01:00

3.7 KiB

codec

(no submodules)

Purpose

FfmpegNativeCodec is the server-tier concrete AudioCodec implementation that backs @suaveplan/audio-codec-ffmpeg-native. It satisfies the @suaveplan/audio-codecs plugin contract — canDecode, decode, encode, createDecoder, createEncoder — by spawning a child ffmpeg process per operation, piping bytes through stdin and reading streamed PCM out of stdout without ever buffering the full input.

The class also owns the per-instance concurrency limit (a tiny FIFO semaphore) so that multi-tenant servers cannot launch unbounded ffmpeg children, and threads the caller's AbortSignal through to the runner module so that cancellation reliably reaps the subprocess.

Features

  • Full AudioCodec surface: canDecode, decode, encode, plus the optional createDecoder / createEncoder streaming factories.
  • Configurable concurrency (concurrencyLimit, default 4) enforced through an in-instance FIFO waiter queue.
  • Per-operation AbortSignal and per-instance timeoutMs plumbed through the runner; both escalate SIGTERM → SIGKILL.
  • Strict PCM validation on encode (validatePcm raises INVALID_PCM).
  • Strict encode-format allowlist (UNSUPPORTED_CODEC for anything outside wav | mp3 | ogg | opus | flac | aac | m4a | webm | matroska | wma).
  • f32le interleaving on encode + de-interleaving on decode (see pcm.ts).

Usage: Basic

import { FfmpegNativeCodec } from "@suaveplan/audio-codec-ffmpeg-native/codec/codec";

const codec = new FfmpegNativeCodec();
const decoded = await codec.decode(mp3Bytes);
const wav = await codec.encode(decoded.channelData, {
    sampleRate: decoded.sampleRate,
    format: "wav",
});

Usage: Advanced

import { FfmpegNativeCodec } from "@suaveplan/audio-codec-ffmpeg-native/codec/codec";

const codec = new FfmpegNativeCodec({
    binaryProvider: () => process.env.FFMPEG_PATH ?? "",
    concurrencyLimit: 8,
    timeoutMs: 15_000,
    channels: 1,
    sampleRate: 16_000,
    defaultEncodeFormat: "opus",
});

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
const encoded = await codec.encode([Float32Array.from([0.1, -0.1])], {
    signal: controller.signal,
    format: "flac",
});

API Reference

  • new FfmpegNativeCodec(options?, spawn?) — options matches FfmpegNativeCodecOptions; the second positional spawn argument is the test-only Spawn injector that bypasses node:child_process.spawn.
  • name, extensions, mimeTypes — read-only metadata for registry dispatch.
  • decode(bytes, opts?) / encode(channelData, opts?) — full-buffer paths.
  • createDecoder(opts?) / createEncoder(opts?) — streaming-factory returning the additive companion shapes from stream/*.ts.

Implementation Notes

The semaphore is intentionally inline (one private counter + one waiter array) rather than a separate module because it never leaves this class — extracting it would inflate the public surface for zero callers. The same reasoning applies to the singleton singleChunk async generator used by encode: it exists to feed runEncode's AsyncIterable<Uint8Array> contract from a single buffered byte string.

Decode and encode share argv templates (decodeArgs / encodeArgs) so the streaming factories can reuse the exact same ffmpeg argv as the full-buffer paths. This keeps the on-the-wire ffmpeg behaviour stable across the four public methods.

canDecode deliberately leans on the magic-byte sniffer in pcm.ts rather than running ffmpeg in a probe mode; we want canDecode to be cheap and synchronous because the CodecRegistry calls it on every dispatch.