first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user