import type { Duration } from "@moq/clock"; /** * Puts one subscription's frames in timestamp order for the audio decoder. * * Groups go out newest-first, so a subscription's head (the groups the publisher still holds) can * arrive after the live edge it was served alongside. The container's ordered read waits for it from * where the publisher's feed starts and hands groups over in sequence, so the head comes first here; * this keeps the order should it not. Chrome's AudioDecoder stamps every output from its first input, * so decoding the head after the live edge shifts the rest of the track, and the ring, anchored at the * first decoded frame, cannot place the head anyway. * * A buffered subscription holds its head and releases it sorted. The head is complete once a newer * group than the first arrives, or once the first group alone runs past the floor: the ring could not * start playing before holding that much, so waiting for the head costs no startup latency. It is * also released once it spans the subscription's max delay, since nothing older is served. After * that, and throughout when not holding, a frame older than one already decoded is not admitted. */ export class Order { // The current bounds in microseconds, while the head is still arriving. #hold: (() => { floor: number; maxDelay: number }) | undefined; // The head, in arrival order. #held: T[] = []; // The group the head started with, and its own first frame. #first?: { group: bigint; timestamp: number }; // The oldest and newest held timestamps. #span?: { oldest: number; newest: number }; // The newest frame admitted since the decoder was last reset. #newest?: number; /** Hold the head within the bounds `hold` reports, or pass each frame straight through. */ constructor(hold: (() => { floor: number; maxDelay: number }) | undefined) { this.#hold = hold; } /** Accept a frame, returning the frames released for decoding, oldest first. */ push(read: T): T[] { const hold = this.#hold?.(); if (!hold) return [read]; const { group, frame } = read; const timestamp = frame.timestamp.asMicros(); this.#first ??= { group, timestamp }; if (group > this.#first.group) return [...this.flush(), read]; this.#held.push(read); const oldest = Math.min(this.#span?.oldest ?? timestamp, timestamp); const newest = Math.max(this.#span?.newest ?? timestamp, timestamp); this.#span = { oldest, newest }; const ran = group === this.#first.group && timestamp - this.#first.timestamp > hold.floor; if (ran || newest - oldest >= hold.maxDelay) return this.flush(); return []; } /** The newest timestamp among the held head and `released`, the frames still to decode, in microseconds. */ newest(released: T[]): number | undefined { let newest = this.#held.length > 0 ? this.#span?.newest : undefined; for (const { frame } of released) { const timestamp = frame.timestamp.asMicros(); if (newest === undefined || timestamp > newest) newest = timestamp; } return newest; } /** Release the head as it stands, ending the hold: the track ended, or a gap or end cut it short. */ flush(): T[] { this.#hold = undefined; const held = this.#held; this.#held = []; // Stable, so frames sharing a timestamp keep their arrival order. return held.sort((a, b) => a.frame.timestamp.cmp(b.frame.timestamp)); } /** Whether a released frame is still in order for the decoder, recording it if so. */ admit(read: T): boolean { const timestamp = read.frame.timestamp.asMicros(); if (this.#newest !== undefined && timestamp < this.#newest) return false; this.#newest = timestamp; return true; } /** Start the order over after the decoder resets, which restarts its output timestamps. */ reset(): void { this.#newest = undefined; } }