/** * Low-Latency HLS awareness for the CDN balancer * (devs-committee cdn-balancer/2026-08-17_plugin-js-ll-hls-midstream-switch-smarttv). * * The balancer's mid-stream optimisation is built on seconds-scale assumptions: a 10 s connect * deadline, a 30 s trial cooldown, a background probe every second, a 100 KB floor before a * bandwidth sample counts. LL-HLS publishes parts every 200-500 ms, so on a low-latency stream * every one of those is longer than the window it is supposed to protect. FPT Play measured the * consequence as 2-5 s of loading whenever a CDN was throttled, on a stream whose whole latency * budget is under a second. * * Two things follow, and this module holds both: * * 1. DETECTION. A stream is low-latency when its playlist carries part-level tags, or when the * player asks for a blocking reload (`_HLS_msn` / `_HLS_part`). Integrators can also declare it * with `BalancerOptions.lowLatencyMode`. * 2. DERIVED DEADLINES. Once detected, per-request deadlines come from the part duration instead * of the fixed CDN defaults, so a stalled part fails inside its own window and the retry lands * on a healthy CDN — recovery within one part, rather than after a 10 s timeout. * * Everything here is scoped to detected low-latency sessions. A standard HLS or DASH stream never * reaches this code, so its behaviour is unchanged. */ /** * Delivery directives an LL-HLS player appends to a playlist request. They identify the reload, not * the content: two URLs differing only in these point at the same playlist. */ export declare const LL_HLS_QUERY_PARAMS: string[]; /** Used when a stream is known to be low-latency but its part duration is not known yet. */ export declare const DEFAULT_PART_TARGET_MS = 500; export type LowLatencyTimeouts = { connectTimeoutMilliseconds: number; readTimeoutMilliseconds: number; callTimeoutMilliseconds: number; totalTimeoutMilliseconds: number; }; /** True when the URL carries an LL-HLS blocking-reload directive. */ export declare function urlSignalsLowLatency(url: string): boolean; /** True when the playlist body carries part-level tags. */ export declare function manifestSignalsLowLatency(body: string): boolean; /** * `PART-TARGET` from `#EXT-X-PART-INF`, in milliseconds. This is the publishing cadence the origin * promises, which is what the deadlines below should track. */ export declare function partTargetMsFromManifest(body: string): number | undefined; /** * Per-request deadlines derived from the part duration, in the same shape as the four-timeout CDN * model. Proportions follow the established derived-timing pattern (see `P2PDerivedTiming`): a * short first-byte window, a read gap under half a part, and a whole-attempt budget of a couple of * parts. Floors keep them survivable on an embedded TV browser, where a cold TLS handshake alone * can cost a hundred milliseconds or more. */ export declare function derivedTimeoutsFromPartTarget(partTargetMs?: number): LowLatencyTimeouts; /** * Deadlines for a request the playlist announced with `#EXT-X-PRELOAD-HINT`. The origin holds that * response — first byte included — until the part exists, so a part-scaled first-byte deadline would * abort a healthy CDN every time. The budget is the hold-back the spec allows the origin (three * parts), with the same floors and caps philosophy as the media deadlines. */ export declare function preloadHintTimeoutsFromPartTarget(partTargetMs?: number): LowLatencyTimeouts; /** Resolved, query-free URLs of every `#EXT-X-PRELOAD-HINT` in a playlist body. */ export declare function preloadHintUrlsFromManifest(body: string, playlistUrl?: string): string[]; /** * A manifest body as text, whatever `responseType` the player set. hls.js reads playlists as text, * but a player using `responseType='arraybuffer'` (or a typed array) hands us bytes — and detection * needs a string, so without this every hinted part on such a player would hit the fail-fast * deadline. A manifest is small text; decoding it is cheap. Anything we cannot decode synchronously * (a Blob) returns empty, and detection simply falls back to the URL signal. */ export declare function manifestBodyToString(body: unknown): string; /** * Session-scoped low-latency state. * * Reached as a singleton, matching `TestProfilesBusinessObject` — the state is consulted from the * XHR intercept, the loader's timeout arming and the CDN selector, and threading it through four * constructors would widen the change for no benefit. `CdnBalancer.destroy()` resets it, so a page * that plays a low-latency live stream and then a standard VOD does not keep the LL profile for * the VOD. */ export default class LowLatencyMode { private static instance; static getInstance(): LowLatencyMode; private active; private partTargetMs?; /** Set from `BalancerOptions.lowLatencyMode`; forces the profile on regardless of detection. */ private forced; /** * Current preload-hint target per playlist path. Keyed by playlist so each rendition keeps its * own; a playlist's hint is replaced on every reload, so the map stays as small as the number * of playlists in play. */ private preloadHints; setForced(forced: boolean): void; /** Detection from an intercepted request URL. Returns true when this call turned the mode on. */ noteUrl(url: string): boolean; /** * Detection from a fetched playlist body (string or the raw bytes a player set `responseType` * for), which also carries the part duration and the current preload hint. `playlistUrl` resolves * relative hint URIs; without it a relative hint is dropped. */ noteManifest(body: unknown, playlistUrl?: string): boolean; /** * True when the request targets a part the playlist has only hinted at. Compared by path: the * hint resolves against the playlist's (possibly CDN-rewritten) host while the request still * carries the player's origin host, and a CDN path prefix may sit in front of either. */ isPreloadHint(url: string): boolean; /** * The ONE place that decides which deadlines a request gets. Both the attempt timers and the * read timer go through here, so the exemptions cannot drift apart: * - inactive, or a playlist: null → the caller keeps the standard CDN budget. A blocking * playlist reload withholds its first byte by design (measured: 4 of 16 lab requests failed * when part-scaled deadlines were applied to playlists). * - a preload-hinted part: the hold-back budget, since the origin holds it until it exists. * - any other media fetch: the part-scaled budget — the fail-fast this profile exists for. */ deadlinesFor(url: string, isManifest: boolean): LowLatencyTimeouts | null; /** * The cold-start first-chunk probe arms its own deadlines (1500 ms connect) whenever a CDN has * no bandwidth sample yet, and those win over the profile's. On a low-latency stream that is * three part windows, and it is exactly what a fallback CDN looks like the moment it is first * used — measured: the fallback after a fail-fast abort hung for 1502 ms on the probe deadline. * LL sessions never take the probe path. */ suppressFirstChunkProbe(): boolean; /** * Manifest host rotation on a low-latency stream sends each blocking reload to whichever CDN is * current, so consecutive reloads of one playlist can land on different origins. LL sessions keep * playlists on one host; a configured `false` is unchanged. */ effectiveBalanceManifests(configured?: boolean, allowOnLowLatency?: boolean): boolean; isActive(): boolean; getPartTargetMs(): number | undefined; timeouts(): LowLatencyTimeouts; /** * Floor for the dynamic trial deadline. The stock 500 ms floor is fine for 1 s parts and too * generous for very short ones — a trial should never be allowed longer than roughly one part, * or a failed trial costs more than the window it was meant to protect. */ trialTimeoutFloorMs(stockFloorMs: number): number; /** * Bytes a response must carry before it updates a CDN's bandwidth estimate. The standard 100 KB * floor is designed for multi-second segments; LL parts are a fraction of that, so on a * low-latency stream almost nothing qualifies and the ranking goes stale — which is how a * throttled CDN keeps being chosen and playback stays pinned to a low rendition. */ minRequestSizeForBwEstimateKB(configured: number): number; reset(): void; }