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
AudioCodecsurface:canDecode,decode,encode, plus the optionalcreateDecoder/createEncoderstreaming factories. - Configurable concurrency (
concurrencyLimit, default 4) enforced through an in-instance FIFO waiter queue. - Per-operation
AbortSignaland per-instancetimeoutMsplumbed through the runner; both escalate SIGTERM → SIGKILL. - Strict PCM validation on encode (
validatePcmraisesINVALID_PCM). - Strict encode-format allowlist (
UNSUPPORTED_CODECfor anything outsidewav | 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?)—optionsmatchesFfmpegNativeCodecOptions; the second positionalspawnargument is the test-onlySpawninjector that bypassesnode: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 fromstream/*.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.