Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
# `stream-decoder`
## Purpose
`createStreamingDecoder` returns a `FfmpegStreamingDecoder` — the
`@suaveplan/audio-codecs` `StreamingDecoder` plus an additive Promise-returning
`flushAsync()` companion. It is backed by a single long-running ffmpeg child
process per decoder instance, lazily spawned on the first `write` so that a
decoder constructed but never fed never spends a PID.
`write` and `flush` honour the underlying interface's synchronous contract by
returning whatever pages have already been buffered out of stdout; `flushAsync`
is the safe end-of-stream method that ends stdin, awaits the child to exit,
and yields any trailing pages.
## Features
- Single persistent ffmpeg subprocess per decoder.
- Lazy spawn — the child is only created once the first chunk arrives.
- Sync `write(chunk): DecodeResult[]` returning all pages buffered to date.
- Sync `flush(): DecodeResult[]` for callers that pump synchronously.
- Additive `flushAsync(): Promise<DecodeResult[]>` for safe end-of-stream
semantics (mandatory whenever the caller wants the trailing decoded frames).
- `AbortSignal` and `timeoutMs` integrated — both raise the canonical
`ABORTED` / `TIMEOUT` codes on the next `flushAsync` awaited.
## Usage: Basic
```ts
import { FfmpegNativeCodec } from "@suaveplan/audio-codec-ffmpeg-native";
const decoder = new FfmpegNativeCodec().createDecoder({ format: "ogg" });
for (const chunk of oggChunks) decoder.write(chunk);
const pages = await decoder.flushAsync();
decoder.close();
```
## Usage: Advanced
```ts
import { FfmpegNativeCodec } from "@suaveplan/audio-codec-ffmpeg-native";
const codec = new FfmpegNativeCodec({ channels: 2, sampleRate: 48_000 });
const controller = new AbortController();
const decoder = codec.createDecoder({ signal: controller.signal });
try {
for await (const chunk of network()) decoder.write(chunk);
const tail = await decoder.flushAsync();
process.stdout.write(`decoded ${tail.length} trailing pages\n`);
} finally {
decoder.close();
}
```
## API Reference
- `createStreamingDecoder(opts: StreamingDecoderOptions): FfmpegStreamingDecoder`
- `FfmpegStreamingDecoder` — extends `StreamingDecoder` with
`flushAsync(): Promise<readonly DecodeResult[]>`.
- `StreamingDecoderOptions` — `binaryProvider`, `args`, `channels`,
`sampleRate`, `format`, optional `signal`, `timeoutMs`, `spawn`.
## Implementation Notes
The `@suaveplan/audio-codecs` `StreamingDecoder` contract is synchronous —
`write(chunk): readonly DecodeResult[]`. Since the underlying ffmpeg
subprocess is fundamentally asynchronous, the synchronous `write` returns
only the pages already buffered from earlier `stdout data` events; the
new chunk is piped into stdin in the same call but its decoded output
arrives in subsequent ticks.
This is why `flushAsync` exists and why the package docs guide callers to
prefer it. The base `flush` is still useful for callers driving the decoder
from an event loop tick that already drained stdout (e.g. a `WritableStream`
pipe).
Frame alignment matters: ffmpeg's `-f f32le` output is a stream of
4-byte-per-sample, channels-interleaved frames. The sync drain only
de-interleaves a whole number of frames and re-buffers any trailing bytes
for the next call, so partial chunks never corrupt the channel split.