import type { AudioTrack, HdrOnSdrFallback, QualityLevel, SubtitleTrack } from '@nomercy-entertainment/nomercy-player-core'; import type { HtmlPreloadMode } from '../../types.js'; import type { BackendEventPayload, BackendState, IVideoBackend } from './IVideoBackend.js'; import { MediaElementBackend } from '@nomercy-entertainment/nomercy-player-core'; /** * Default video backend. Wraps an `` for transport. * * HLS support: native pass-through when `canPlayType` reports support * (Safari / iOS), otherwise dynamically imports `hls.js` (peer dep — not * bundled; IIFE build externalises it as `window.Hls`, ESM build resolves * it from the consumer's node_modules) and attaches it. MSE / WebCodecs * backends ship later. */ export declare class Html5VideoBackend extends MediaElementBackend implements IVideoBackend { readonly kind: "html5"; private hls; private currentUrl; private _state; private _hadError; private _ended; /** * Listener attached to the active `TextTrack` so we can detach on track * switch / dispose without rebuilding the rest of the listeners map. */ private cueChangeHandler; /** * Codec → supported flag. Populated asynchronously after MANIFEST_PARSED. * Keys are CODECS attribute strings from HLS level attributes. */ private _capabilityCache; /** * Retry count for the current fatal network-error sequence. Reset on * successful playback resume. */ private _netRetryCount; /** * Timestamp (ms) when the first media-error recovery was attempted. * Used to detect a second media error within the 5-second escalation window. */ private _mediaRecoveryStartMs; /** Timer handle for exponential back-off retries. Cleared on unload/dispose. */ private _retryTimer; /** * Whether this outage already failed below the HTTP layer, which is what * tells a restarting server's 404 apart from a video that is genuinely gone. */ private _sawConnectionFailure; /** * True while the ladder is riding out a source the server stopped serving. * * The backend is the first thing a consumer has that learns the origin is * gone: it asks for bytes constantly, while a realtime socket through the * same tunnel can stay up long after the server behind it died. An app * watches this to raise its own server-offline screen on the player's * evidence rather than waiting for a socket that may never drop. */ private _recoveringFromOutage; /** Retries the moment the device has a route again, rather than waiting out a rung. */ private _onlineHandler; private _sourceNode?; private _sourceCtx?; private _analyserNode?; private _outputGain?; /** * Dynamic-range constraint strategy for HLS.js ABR. * * On manifests that carry both SDR and HDR (PQ/HLG) level variants — * e.g. Cosmos Laundromat (7 resolution tiers × SDR + HDR = 14 levels) — * HLS.js ABR is oblivious to display capability and can freely pick a PQ * variant on an SDR display (colours look washed out). This block wires * three responses: * * 1. Constraint on load — after MANIFEST_PARSED, if the display is SDR * the highest-SDR-level index is stored in `autoLevelCapping` so ABR * never selects above it. For interleaved manifests (HDR index < SDR * index at the same resolution) `nextLevel` force-switches away from * any currently-playing HDR variant to its SDR peer. * * 2. Live display flip — matchMedia `change` listener (wired in the * constructor, torn down in dispose). On SDR→HDR: lift the cap and * optionally prefer a PQ peer at the same resolution. On HDR→SDR: * cap + force-switch the playing level if it is a PQ variant. * * 3. Audio continuity — switching levels via `nextLevel` is seamless * when the new level shares the same audio group ID, which HLS.js * ensures for well-formed manifests (same resolution tier). No extra * audio continuity guard is needed here. */ /** Whether the active display reported HDR capability at the last check. */ private _displayHdr; /** * Per-level SDR/HDR classification. Populated after each MANIFEST_PARSED. * Index matches `hls.levels[i]`. `true` = HDR (PQ or HLG), `false` = SDR. */ private _levelIsHdr; /** * matchMedia query for display HDR capability. Tracked so we can remove * the listener on dispose without leaking the closure. */ private _hdrMql; /** * Bound listener for `_hdrMql` change events. Stored so we can call * `removeEventListener` with the same reference on dispose. */ private _hdrMqlListener; /** * Consumer policy for an all-HDR item on an SDR display, pushed in by * `NMVideoPlayer` via `setHdrOnSdrFallback`. `undefined` until then — * `hdrDecision`'s own default (`'play'`) applies at read time. */ private _hdrOnSdrFallback; /** * Observer on the media element, so the ceiling follows a window drag, a * fullscreen toggle and an orientation change. The facade's own observer * feeds the `videoRect` event on `NMVideoPlayer` and the backend holds no * reference to the facade, so the size signal is re-derived here. */ private _paneObserver; /** * Pending coalesced re-apply. A drag fires the observer on every frame and * every `autoLevelCapping` write is a decision ABR has to react to, so the * burst collapses into one apply. */ private _paneReapplyTimer; constructor(container: HTMLElement); /** * `startTime` is consumed natively (declared via `canStartAt`): the hls.js * path maps it to `startPosition` so the FIRST fragment fetched is the one * containing the offset — fragment 0 is never requested. The native / * progressive path sets `element.currentTime` once metadata arrives. */ readonly canStartAt: boolean; load(url: string, opts?: { preload?: HtmlPreloadMode; startTime?: number; }): Promise; unload(): void; dispose(): void; private _teardownAudioGraph; buffered(): number; videoWidth(): number; videoHeight(): number; audioTracks(): AudioTrack[]; setAudioTrack(idx: number): void; subtitleTracks(): SubtitleTrack[]; setSubtitleTrack(idx: number | null): void; /** * Match the kit-facing subtitle index back to the underlying * `TextTrack` instance. * * For HLS-managed tracks (`hls.subtitleTracks[idx]`), match against * `element.textTracks` by `language + label`. The catch: HLS streams * with CEA-608 / CEA-708 closed captions embedded in the MPEG-TS * video (e.g. Apple's bipbop-advanced) get auto-extracted by HLS.js * into a `kind: 'captions'` track that often shares the same * `language` and `label` as the WebVTT subtitle track. Match-by- * lang+label alone ambiguously picks the FIRST one — usually the * CEA captions, which are NOT the same as the WebVTT track the * user selected. Prefer `kind: 'subtitles'` (WebVTT) over * `kind: 'captions'` (CEA) when both match. * * Native (non-HLS) sources index the textTrack list directly. */ private resolveSubtitleTextTrack; private disableAllSubtitleTextTracks; private detachActiveTextTrack; /** * Read the active cues off a `TextTrack` and emit them through the * backend's `subtitleCue` channel. Each `VTTCue` is normalised into * the backend-agnostic `SubtitleCue` shape so renderers don't have * to know whether the source was an HLS-fed VTT or a native track. */ private emitEmptySubtitleCue; private emitActiveCues; qualityLevels(): QualityLevel[]; qualityLevels(opts: { includeUnsupported: true; }): QualityLevel[]; private _probeCodecCapabilities; setQuality(idx: number | 'auto'): void; currentLevel(): number; /** * Live throughput estimate in bits per second, read straight off the * active hls.js instance's own EWMA estimator. `0` when no HLS instance * is bound (native/progressive playback, or before `load()` has attached * one) — the kit's public `bandwidth()` treats `0` as "no estimate yet", * so this never reports a stale or fabricated number. */ bandwidthEstimate(): number; state(): BackendState; outputProtectionState(): 'unrestricted' | 'restricted' | 'unsupported'; /** * Returns the tail of the backend's Web Audio graph (a `GainNode` whose * output flows to `ctx.destination` by default). Mirrors * `AudioElementBackend.outputNode` exactly. The graph is built lazily on * first call so backends that are never tapped pay zero Web Audio cost and * the autoplay policy is never triggered ahead of user interaction. * * `AudioGraphPlugin` disconnects the baseline `outputGain → destination` * routing and rewires through its effect chain on `use()`. */ outputNode(ctx: AudioContext): AudioNode; /** * Returns the raw `MediaElementAudioSourceNode` — the pre-volume source — * so that `AudioGraphPlugin` can tap the `AnalyserNode` upstream of the * volume fader and produce volume-independent spectrum/FFT magnitudes. */ analysisNode(ctx: AudioContext): AudioNode; /** * Build the minimal Web Audio signal chain lazily: * `MediaElementAudioSourceNode → AnalyserNode → GainNode → destination`. * * Idempotent for the same `ctx`. If called with a different context the old * graph is disconnected and rebuilt (the consumer swapped AudioContext). * The graph mirrors `AudioElementBackend.ensureSourceGraph` so `AudioGraphPlugin` * can treat both backends identically. */ private _ensureAudioGraph; /** * Wire video-specific DOM events onto the element, tracking each handler * in `this.domHandlers` (inherited from `MediaElementBackend`) so * `detachDomBridges()` removes them cleanly on dispose. * * Video adds `resize`, `emptied`, `loadeddata`, and error-code mapping * on top of the standard 13 events handled by `attachDomBridgesTo`. * Rather than calling the base helper (which would add state-mutation * handlers appropriate for the audio path), video wires its own full * event set here to keep the custom `ended`/`error` side-effects intact. */ private wireElementEvents; /** Reset all recovery counters. Call on successful playback resume or unload. */ private _resetRecoveryState; /** * True while the ladder is riding out a source the server stopped serving — * see `_recoveringFromOutage`. A consumer probes server reachability on this * so its own offline screen rises from the evidence that arrives first. */ get isRecoveringFromOutage(): boolean; /** * A server that went away is an outage to ride out, not a dead file. * * Escalating after three tries over seven seconds is what made a host * restart read to a viewer as a broken film: the ladder was spent before * the server had finished booting. The budget now matches how long a real * restart takes, and how many rungs a failure earns depends on what it was * — an origin-down status or a refused connection gets all of them, a cold * 4xx gets five, because a URL that is genuinely gone answers the same way * every time and a viewer should be told so rather than shown a spinner. */ private _rideOutNetworkError; /** * Retries the moment the device has a route again, so an outage longer than * the ladder still ends in playback. Without it a Wi-Fi drop that outlasts * the budget leaves the session unrecoverable until the viewer reloads. */ private _watchForConnectivityReturn; /** * Escalate a fatal HLS error to a player-level error event. * * @param details - `HlsErrorData.details` string from hls.js * @param message - Human-readable summary for the error payload */ private _escalateHlsError; /** * Subscribe to `Hls.Events.ERROR` on the current `this.hls` instance. * Must be called after every HLS instance creation (including the CEA * fallback reload) because hls.js event listeners are per-instance. * * Decision tree per hls.js error semantics: * - Non-fatal → emit `stream:error` with `fatal: false`, no escalation. * - Fatal NETWORK → ride the outage out on `SOURCE_OUTAGE_BACKOFF_MS`, * for as many rungs as the failure earns. If exhausted, escalate. * - Fatal MEDIA → call `hls.recoverMediaError()`. If a second media * error fires within 5 s, escalate. * - Fatal MUX / other → destroy + reload via `load()`. If that fails, escalate. * * @param Hls - The hls.js constructor (carries static `Events` + `ErrorTypes`) * @param url - The manifest URL, needed for MUX destroy-reload. */ private _attachHlsErrorHandler; /** * Emit `levels` and `audioTracks` backend events from the current HLS * instance's live lists. Called after every `MANIFEST_PARSED` event so * overlay plugins can update button visibility without polling. * * Also builds the per-level HDR classification table and immediately * applies the ABR constraints for the current display and pane size. */ private _emitHlsTrackLists; /** * Consumer policy for an all-HDR item on an SDR display with no converter. * Pushed once by `NMVideoPlayer` right after construction — see `IVideoBackend`. */ setHdrOnSdrFallback(fallback: HdrOnSdrFallback): void; /** * Wire a combined-query matchMedia listener once. The comma joins the two * queries as an OR at the CSS level, so one listener catches a change in * either signal; the boolean answer itself always comes from * `detectDisplayHdr`, which asks them in priority order (video plane * before page) rather than trusting whichever one the event fired for. */ private _wireHdrMatchMedia; /** * Watch the media element's box so the pane ceiling follows the layout. * * The element rather than the container: `object-fit` letterboxing means the * container can be wider than the picture, and the picture is what a * rendition has to cover. */ private _wirePaneResizeObserver; private _schedulePaneReapply; /** Disconnect the pane observer and drop a pending re-apply. Called from `dispose()`. */ private _teardownPaneResizeObserver; /** Remove the matchMedia listener. Called from `dispose()`. */ private _teardownHdrMatchMedia; /** * Build `_levelIsHdr[]` from the current HLS level list. * A level is HDR when its VIDEO-RANGE attribute is `PQ` or `HLG`. */ private _classifyHdrLevels; /** * Constrain HLS.js ABR to what this display and this pane can actually show, * per `hdrDecision` and `sizeAbrCeiling` — the oracles both native ports * mirror. The only writer of `autoLevelCapping`. * * The pane ceiling applies on every branch, including the ones with no * dynamic-range cap to make: an SDR item in a 740px player is the ordinary * case, and it is the one that was pulling a 4K rendition. * * `as-is` (HDR display, or an ordinary SDR item with no HDR rungs to cap): * - `autoLevelCapping` falls back to the pane ceiling, or `-1` when the * pane is unmeasured or already as large as the ladder's best rung. If * the current level is SDR and an HDR peer exists at the same * resolution, prefer it via `nextLevel` (soft switch — the next fragment * boundary picks it up, no stutter). The peer shares the current level's * resolution, so it cannot violate the pane ceiling. A no-op on the * SDR-only case: there is no HDR peer to find. * * `cap-to` (SDR display, an SDR rung exists): * - Caps `autoLevelCapping` to the more restrictive of the dynamic-range * rung and the pane rung, by raw manifest index. For interleaved * manifests (HDR index < SDR index at the same resolution) the cap alone * is insufficient — `nextLevel` also forces off any currently-playing * HDR variant to its SDR peer. * * `tone-map` — never returned here; see `backendCanToneMap` below. * * `play-unconverted` — no SDR rung, no converter, consumer opted in: the * dynamic range goes unconstrained on purpose, the pane ceiling still * holds, no error. * * `refuse` — no SDR rung, no converter, consumer opted out: escalated * through the same fatal-error channel HLS stream errors use. * * Always re-emits `levels` so overlay plugins can update their menus. */ private _applyAbrConstraints; /** Called by the matchMedia listener when the display's HDR capability changes. */ private _onDisplayHdrChange; /** * Subscribe to HLS level-change signals on the current `this.hls` instance * and forward them as the backend's `level-switched` event. Must be called * after every HLS instance creation alongside `_attachHlsErrorHandler`. * * Listens to two HLS events for robustness: * - `LEVEL_SWITCHED` — the "official" level change. Fires reliably for * the initial pick and explicit setQuality() calls, but can go silent * for ABR-driven sub-switches that happen at fragment boundaries. * - `FRAG_CHANGED` — every fragment swap. Carries `frag.level`, so when * a fragment lands on a different level than the previous one, that's * effectively a level switch. Catches the cases LEVEL_SWITCHED misses. * * Both paths dedupe through `lastLevel` so consumers never see two * `level-switched` emissions for the same actual level. */ private _attachHlsLevelSwitchedHandler; private waitForLoadedMetadata; }