5a75da1769
Seek now trims the lead-in so playback lands at the requested time, not the page start; decoder drain polls decodeQueueSize (bounded) instead of a single timeout. Minor cleanups.
64 lines
3.4 KiB
TypeScript
64 lines
3.4 KiB
TypeScript
/**
|
|
* IStreamingDecoder - the stateful streaming-decode seam, parallel to IFormatDecoder.
|
|
*
|
|
* Why a second seam. `IFormatDecoder` (WAV/MP3/FLAC) is a *wrap-and-decode-each-segment* strategy:
|
|
* `StreamDecoder` cuts the stream into independently-decodable segments, `wrapSegment` makes each a
|
|
* standalone file, and `decodeAudioData` decodes each in isolation. That model is correct for raw PCM
|
|
* (WAV) and independently-decodable frames (FLAC), but it is fundamentally wrong for Opus: Opus has
|
|
* pre-skip (encoder delay) and inter-frame state (MDCT overlap-add, SILK/CELT continuity), so decoding
|
|
* page-runs independently re-applies the pre-skip and starts from cold codec state at every boundary —
|
|
* audible glitching and a broken timeline.
|
|
*
|
|
* A WebCodecs `AudioDecoder` is the right tool: one stateful decoder fed packets sequentially, decoding
|
|
* continuously with correct pre-skip-once handling and full inter-frame continuity. But it does NOT fit
|
|
* `IFormatDecoder` — it is async/callback-driven and owns its own buffering. So Opus gets this seam
|
|
* instead. `AudioPlayer` dispatches by content-type: WAV/MP3/FLAC keep the `StreamDecoder` path
|
|
* byte-for-byte; Opus routes here. Both feed the SAME `PlaybackScheduler` — the change is the decode
|
|
* stage only, never the schedule/playback stage.
|
|
*
|
|
* The seam is intentionally minimal and mirrors the lifecycle `StreamDecoder` already exposes so
|
|
* `AudioPlayer` can treat the two uniformly: initialize -> push chunks -> mark complete, plus a
|
|
* range-continuation reinit for seek-beyond-buffer.
|
|
*/
|
|
|
|
export interface IStreamingDecoder {
|
|
/**
|
|
* Decoded buffers ready to schedule, drained by AudioPlayer after each push/flush. Each entry is a
|
|
* standard AudioBuffer at the AudioContext's sample rate, ready for PlaybackScheduler.addBuffer.
|
|
*/
|
|
readonly hasFatalError: boolean;
|
|
|
|
/** True once the decoder has enough to begin playback (header/config established). */
|
|
readonly ready: boolean;
|
|
|
|
/** Total stream duration in seconds if known up front (Opus knows it from the sidecar), else null. */
|
|
readonly totalDuration: number | null;
|
|
|
|
/**
|
|
* Push raw stream bytes. Returns decoded AudioBuffers that became ready (possibly empty — WebCodecs
|
|
* decode is async, so a push may return nothing and a later push returns several).
|
|
*/
|
|
push(chunk: Uint8Array): Promise<AudioBuffer[]>;
|
|
|
|
/**
|
|
* Signal end-of-stream. Flushes the decoder and returns any residual decoded buffers (including the
|
|
* end-trimmed final buffer).
|
|
*/
|
|
complete(): Promise<AudioBuffer[]>;
|
|
|
|
/**
|
|
* Reinitialize for a Range-continuation after seek-beyond-buffer. The 206 body begins on an Ogg page
|
|
* boundary and carries no setup pages — the decoder reuses the cached config and resets demux/codec
|
|
* state so inter-frame continuity restarts cleanly from the new offset.
|
|
*
|
|
* @param landingTimeSeconds The actual presentation time of the resolved seek page (t_page ≤ target).
|
|
* @param targetTimeSeconds The user-requested seek position. The decoder trims the leading
|
|
* `(target - landing) * sampleRate` frames so playback lands at target
|
|
* (AC9 fine re-sync, §3.4a step 4).
|
|
*/
|
|
reinitializeForRangeContinuation(landingTimeSeconds: number, targetTimeSeconds: number): void;
|
|
|
|
/** Tear down the underlying WebCodecs decoder and release resources. */
|
|
dispose(): void;
|
|
}
|