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,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.

View File

@@ -0,0 +1,75 @@
# `stream-encoder`
## Purpose
`createStreamingEncoder` returns a `FfmpegStreamingEncoder` — the
`@suaveplan/audio-codecs` `StreamingEncoder` plus an additive Promise-returning
`flushAsync()` companion. It backs the encoder with a single long-running
ffmpeg subprocess that consumes interleaved f32le PCM on stdin and emits the
selected container format on stdout, lazily spawned on the first `start()` so
that an encoder constructed but never used pays no process cost.
## Features
- Single persistent ffmpeg subprocess per encoder.
- Lazy spawn on first `start()`; channel count chosen by the caller.
- Strict `validatePcm` on every `write` raises `INVALID_PCM` on bad input.
- Sync `start` / `write` / `flush` honour the base contract; additive
`flushAsync(): Promise<Uint8Array>` waits for ffmpeg to drain.
- `AbortSignal` and `timeoutMs` integrated — both raise canonical
`ABORTED` / `TIMEOUT` codes on the next `flushAsync`.
## Usage: Basic
```ts
import { FfmpegNativeCodec } from "@suaveplan/audio-codec-ffmpeg-native";
const encoder = new FfmpegNativeCodec().createEncoder({
sampleRate: 48_000,
format: "flac",
});
encoder.start(48_000, channelData.length);
encoder.write(channelData);
const tail = await encoder.flushAsync();
process.stdout.write(tail);
encoder.close();
```
## Usage: Advanced
```ts
import { FfmpegNativeCodec } from "@suaveplan/audio-codec-ffmpeg-native";
const codec = new FfmpegNativeCodec({ timeoutMs: 10_000 });
const encoder = codec.createEncoder({ format: "opus" });
encoder.start(48_000, 2);
for (const block of pcmBlocks()) encoder.write(block);
const finalBytes = await encoder.flushAsync();
encoder.close();
```
## API Reference
- `createStreamingEncoder(opts: StreamingEncoderOptions): FfmpegStreamingEncoder`
- `FfmpegStreamingEncoder` — extends `StreamingEncoder` with
`flushAsync(): Promise<Uint8Array>`.
- `StreamingEncoderOptions` — `binaryProvider`, `argsFor(channels)`,
optional `signal`, `timeoutMs`, `spawn`.
## Implementation Notes
The base `StreamingEncoder.start()` is documented as returning header bytes;
ffmpeg emits headers on stdout as part of the normal encode flow, so we
return an empty `Uint8Array` from `start()` and let the headers fall out of
the first `write` / `flushAsync` call. This is consistent with how all the
real-world container formats (FLAC, Opus, MP3) actually framing their headers
on the same byte stream as their data.
`write` returns synchronously with whatever bytes ffmpeg has already pushed
out of stdout. For most encoders the first synchronous return after a single
`write` is empty because ffmpeg buffers frames; callers MUST await
`flushAsync` to retrieve trailing bytes.
`chosenChannels` is captured in `start()` (or, defensively, the length of
the first `write` call's `channelData`) because the encode argv embeds the
`-ac <n>` flag — once ffmpeg is running, the channel count cannot change.