# `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` 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 ```ts 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 ```ts import { runEncode } from "@suaveplan/audio-codec-ffmpeg-native/runner/runner"; async function* pcm(): AsyncIterable { 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` - `runEncode(opts: RunEncodeOptions): Promise` - `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.