/** * libav.js demux + decode loop for the fallback strategy. * * Design: * * - **Always software decode.** The fallback strategy is only entered when * classification has decided no browser decoder will handle the codec set, * so the WebCodecs hardware path is dead weight here. Going through libav * uniformly also avoids brittleness around `EncodedAudioChunk` framing for * codecs like MP3-in-AVI where the browser's AudioDecoder rejects libav's * raw demuxed packets. * * - **Cancellable pump loop.** Each pump iteration is gated on a token that * `seek()` increments. When the token changes mid-batch, the loop exits * and a fresh one starts at the new position. This is how seek interrupts * the decoder cleanly without having to await an arbitrarily long * `ff_decode_multi` call. * * - **Synthetic timestamps.** AVI demuxers report `AV_NOPTS_VALUE` for most * packets — they're frame-indexed, not time-indexed. We replace any * invalid pts with a per-stream synthetic counter (frame index × 1e6/fps * for video; sample-accurate for audio) so the bridge's chunk constructor * doesn't overflow int64. */ import { loadLibav, type LibavVariant } from "./libav-loader.js"; import { VideoRenderer } from "./video-renderer.js"; import { AudioOutput } from "./audio-output.js"; import type { MediaContext } from "../../types.js"; import { pickLibavVariant } from "./variant-routing.js"; import { dbg } from "../../util/debug.js"; /** True when `globalThis.AVBRIDGE_DEBUG` is set. Used to gate verbose * per-packet / per-frame trace lines that are useful for debugging * post-seek pts behavior but unreadable in normal use. */ function isDebug(): boolean { return typeof globalThis !== "undefined" && !!(globalThis as Record).AVBRIDGE_DEBUG; } import { libavFrameToInterleavedFloat32, packetPtsSec, } from "../../util/libav-demux.js"; export interface DecoderHandles { destroy(): Promise; /** Seek to the given time in seconds. Returns once the new pump has been kicked off. */ seek(timeSec: number): Promise; /** * Switch the active audio track. The decoder tears down the current audio * decoder, initializes one for the stream whose container id matches * `trackId` (== libav `stream.index`), seeks the demuxer to `timeSec`, and * restarts the pump. No-op if the track is already active. */ setAudioTrack(trackId: number, timeSec: number): Promise; stats(): Record; /** * The demuxer's read-ahead frontier in seconds. See * `HybridDecoderHandles.bufferedUntilSec` for the full contract — * same semantics, same consumer (`