/** * Web Audio output for the fallback strategy. * * Owns the **media-time clock** for fallback playback. Audio is the master: * decoded video frames are presented based on what `now()` returns here. * * State machine: * * ┌──────┐ schedule() ┌──────┐ ┌────────┐ * │ idle │ ───────────▶ │ idle │ ── start() ──▶│ playing│ * └──────┘ (queues) └──────┘ └────┬───┘ * ▲ │ * │ │ pause() * │ ▼ * │ ┌────────┐ * └────────────── reset(t) ─────────────── ── │ paused │ * └────────┘ * * - **idle**: AudioContext is suspended (no playback). `schedule()` queues * samples in `pendingQueue`; `now()` returns `mediaTimeOfAnchor`. * - **playing**: AudioContext is running. `schedule()` writes directly to * the audio graph at the right time. `now()` advances with `ctx.currentTime`. * - **paused**: AudioContext is suspended. `now()` returns the media time * captured at pause. `start()` resumes. * * Key invariant: between any two `start()` calls, `mediaTimeOfNext` (the * media time of the next sample to be scheduled) must equal the media time * the playback is at. This is what makes the cold-start race go away — we * never schedule audio with a stale wall-clock anchor. */ interface PendingChunk { samples: Float32Array; channels: number; sampleRate: number; frameCount: number; durationSec: number; /** Source-domain content PTS in seconds. `null` for legacy callers * that schedule sequentially without PTS information. */ ptsSec: number | null; } /** True when `globalThis.AVBRIDGE_DEBUG` is set. Used to gate [TRACE-AUD] * per-chunk logs that are useful for diagnosing scheduling drift but * unreadable in normal use. */ function isDebug(): boolean { return typeof globalThis !== "undefined" && !!(globalThis as Record).AVBRIDGE_DEBUG; } export interface ClockSource { /** Current media time in seconds. */ now(): number; /** True if media is currently playing (audio scheduler is running). */ isPlaying(): boolean; /** * Media time at which the current playback session was anchored — i.e. the * seek target after the most recent `reset()`, or 0 on cold start. Used by * the video renderer for post-flush PTS calibration: `now()` includes any * decode-stall lag accumulated since playback resumed, but the anchor is * a stable reference that maps directly to the user's intended position. */ anchorTime(): number; } export class AudioOutput implements ClockSource { private ctx: AudioContext; private gain: GainNode; private state: "idle" | "playing" | "paused" = "idle"; /** * Wall-clock fallback mode. When true, this output behaves as if audio * is unavailable — `now()` advances from `performance.now()` instead of * the audio context, `schedule()` is a no-op, and `bufferAhead()` returns * Infinity so the session's `waitForBuffer()` doesn't block on audio. * * Set by the decoder via {@link setNoAudio} when audio decode init fails. * This is what lets video play even when the audio codec isn't supported * by the loaded libav variant. */ private noAudio = false; /** Wall-clock anchor (ms from `performance.now()`) for noAudio mode. */ private wallAnchorMs = 0; /** Media time at which the next sample will be scheduled. */ private mediaTimeOfNext = 0; /** Anchor: media time `mediaTimeOfAnchor` corresponds to ctx time `ctxTimeAtAnchor`. */ private mediaTimeOfAnchor = 0; /** * Ctx time at which the first audible chunk will start playing. `-1` * before any chunk has been scheduled successfully (clock is frozen); * the actual ctx time once one has. The renderer's `clock.now()` uses * this to avoid advancing during the silent-gap window between * `audio.start()` and the first chunk that schedules without being * dropped — that gap is what produces the "audio-less fast-forward" * the user sees post-seek when the gate releases on video-only grace. */ private firstAudibleCtxStart = -1; private ctxTimeAtAnchor = 0; private pendingQueue: PendingChunk[] = []; private framesScheduled = 0; private destroyed = false; /** User-set volume (0..1). Applied to the gain node. */ private _volume = 1; /** User-set muted flag. When true, gain is forced to 0. */ private _muted = false; /** Playback rate. Scales the media clock and each AudioBufferSourceNode's * playbackRate so audio pitches up/down accordingly (same as native *