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