first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
# `runner`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```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<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.