3.7 KiB
runner
(no submodules)
Purpose
This module is the single place in @suaveplan/audio-codec-ffmpeg-native that
imports node:child_process. It owns spawning ffmpeg, piping bytes through
stdin, streaming stdout chunks to a callback (never buffering the full
output), enforcing optional timeouts with a SIGTERM-then-SIGKILL escalation,
honouring caller-supplied AbortSignals with the same escalation path, and
mapping all post-spawn failure modes onto the typed
AudioCodecFfmpegNativeError codes.
Centralising spawn lifecycle here means decode, encode and the streaming adapters never re-implement timeout/abort/exit handling — they only describe the ffmpeg argv they want to run.
Features
runDecode— feeds a singleUint8Arrayinto stdin then ends it.runEncode— drains anAsyncIterable<Uint8Array>into stdin with backpressure-awaredrainwaits.onStdout(chunk)callback called per stdout chunk; nothing is buffered inside the runner.- Optional
timeoutMstriggers SIGTERM with a 250 ms grace window before SIGKILL. - AbortSignal listeners use
{ once: true }and are removed on settle. - Rolling stderr ring buffer (default 4 KiB) preserved on
error.context.stderr. - Injectable
spawnfor tests; defaults tonode:child_process.spawn.
Usage: Basic
import { runDecode } from "@suaveplan/audio-codec-ffmpeg-native/runner/runner";
const chunks: Uint8Array[] = [];
await runDecode({
binaryPath: "/usr/bin/ffmpeg",
args: ["-i", "pipe:0", "-f", "f32le", "-ac", "1", "pipe:1"],
input: compressedBytes,
onStdout: (chunk) => chunks.push(chunk),
});
Usage: Advanced
import { runEncode } from "@suaveplan/audio-codec-ffmpeg-native/runner/runner";
async function* pcm(): AsyncIterable<Uint8Array> {
yield interleaveFloat32(channelDataA);
yield interleaveFloat32(channelDataB);
}
const out: Uint8Array[] = [];
const controller = new AbortController();
const result = await runEncode({
binaryPath: "/opt/ffmpeg",
args: ["-f", "f32le", "-ar", "48000", "-ac", "2", "-i", "pipe:0", "-f", "flac", "pipe:1"],
inputs: pcm(),
onStdout: (chunk) => out.push(chunk),
signal: controller.signal,
timeoutMs: 30_000,
stderrCap: 8192,
});
console.log("stderr tail:", result.stderr);
API Reference
runDecode(opts: RunDecodeOptions): Promise<RunResult>runEncode(opts: RunEncodeOptions): Promise<RunResult>RunnerCommon—binaryPath,args,signal?,timeoutMs?,spawn?,stderrCap?.RunResult—{ stderr, exitCode }(exitCode is always0on resolve).- Error codes raised:
SPAWN_FAILED,DECODE_FAILED,ENCODE_FAILED,TIMEOUT,ABORTED.
Implementation Notes
The single shared settle guard guarantees the returned promise resolves or
rejects exactly once even when stdin pump, child error and close events
all fire in close succession. child.on("error", ...) is the spawn-time
failure path (ENOENT/EACCES); child.on("close", ...) is the post-exit
discriminator that funnels exit codes into DECODE_FAILED / ENCODE_FAILED
depending on which entry point caller used.
Timeout and abort both go through the same killEscalate helper. The 250 ms
grace window is deliberately short — ffmpeg honours SIGTERM cleanly in the
common case, and on misbehaving builds SIGKILL guarantees process death
without leaking PIDs. timeout?.unref?.() keeps the Node event loop from
being kept alive by the timer.
The stderr ring buffer keeps the tail of stderr because ffmpeg emits its informative diagnostics (codec banner, then per-frame progress) at the start of a successful run but the end of a failing run.