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

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 single Uint8Array into stdin then ends it.
  • runEncode — drains an AsyncIterable<Uint8Array> into stdin with backpressure-aware drain waits.
  • onStdout(chunk) callback called per stdout chunk; nothing is buffered inside the runner.
  • Optional timeoutMs triggers 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 spawn for tests; defaults to node: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 always 0 on 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.