import P2PLoader from './P2PLoader'; import CDNLoader from './CDNLoader'; import BalancerOptions from '../Utils/Options'; import SegmentStorage from '../Storage/SegmentStorage'; import { VideoSegment } from '../Storage/VideoSegment'; import JoinGate from '../Utils/JoinGate'; import type ShadowXhrBridge from '../ShadowXhrBridge'; import CdnBalancer from '../CdnBalancer'; import ResourceIdentifier from '../manifest/ResourceIdentifier'; import P2PManifestRegistry from './P2PManifestRegistry'; import { P2PSegmentIdResolver } from './P2PSegmentIdResolver'; import DiskSegmentStore from '../Storage/DiskSegmentStore'; import { BalancerProvider, ExternalProvidersOptions } from '../Utils/ExternalProviders'; /** * @class * @description Request load main class, it manages and chooses between CDN and P2P requests and evaluates the requests. * @exports Loader */ /** * Client-side probe settings. The backend value (`customData.latencyProbe`) is applied later by * CDNLoader.setSettings and wins when present. */ export declare function applyLatencyProbeOptions(options: BalancerOptions): void; export default class Loader { private _accountCode; private _resource; private readonly resourceIdentifier; /** * V2 manifest registry: parses HLS/DASH manifests as they arrive, tracks * per-rendition bandwidth and the canonical SwarmIdentity used to join the * same P2P swarm as iOS / Android peers. */ private readonly p2pManifestRegistry; /** * V2 segment identity resolver: delegates to the registry when available, * otherwise falls back to a URL-based canonical key. Produces the 32-hex * segmentIds exchanged over the wire with native peers. */ private readonly p2pSegmentIdResolver; /** * V2 segment cache with ACQUIRING -> READY state machine and (where * supported) IndexedDB-backed persistence for cross-session seeding. */ readonly diskSegmentStore: DiskSegmentStore; private readonly _options; private _balancerBusinessObject; private readonly statsReportBusinessObject; P2PLoader: P2PLoader; CDNLoader: CDNLoader; storage: SegmentStorage; private _segments; segmentsMap: Map; uuid: string; private isEnabled; private apiHost; private manifestModelHits; private manifestFallbacks; private readonly externalProvidersManager; /** * Pre-join abort guard (Utils/JoinGate.ts). While closed, `timeout()` will * not abort the player's XHR. Public so `CdnBalancer` can share the one gate * with the peak-bandwidth sampler and the first-chunk eligibility check. */ readonly joinGate: JoinGate; /** * Transparent retry (Switch 2): starts the next attempt of a segment on a fresh shadow bridge. * Set by `CdnBalancer.send` when it routes a media request through the bridge; `retryOnNextCdn` * calls it after picking the next CDN. Undefined = the request was not bridged by the balancer. */ transparentRetryStarter?: (xhr: XMLHttpRequest, segment: VideoSegment) => void; /** * Constructs loader. * @param {BalancerOptions} options Options object. */ constructor(accountCode: string, options: BalancerOptions, apiHost: string); getReportInterval(): number; private camelToSnake; loadCDNBalancerData(firstRequest?: boolean): void; /** * Chooses to request to CDN or P2P and manages the response calling the callback. * @param {URL} url URL object for the request. * @param {callback} callback Callback method to call back when loaded.F * @param {Object} headers Optional headers of the request. * @param {number} retries Number of retries before giving up to load a segment if it fails, optional. * @param {boolean} forceArrayBuffer False by default, set as true if is expected to get the manifest response * in ArrayBuffer format. * @public */ processSegment(url: URL, download: boolean, callbacks: callback[], headers: { [key: string]: string; }, retries?: number, forceArrayBuffer?: boolean, stats?: fragStats): VideoSegment; /** * Android `P2pProvider.fetch` / iOS `P2PProvider.fetch` port: * 1. Local disk cache hit -> serve immediately, no network. * 2. Swarm access denied -> fall back to CDN. * 3. Leader election -> if a peer owns the segment, request from them. * 4. Election window elapses with no candidate -> fall back to CDN. * * Runs as a detached Promise so `processSegment` keeps its sync contract * with the player's interceptor. */ private _dispatchP2pV2Async; /** * V2 gave up on peers for this segment. When the player's own XHR carries the request, its P2P * branch (CdnBalancer send hook) waits on `p2pDispatch` and makes the one CDN fallback itself, so * it is only told; otherwise the fallback happens here. */ private _p2pGaveUp; /** * Single entry point for P2P -> CDN transitions. Flips `useP2P` off, * re-asks CDNLoader for a URL and registers the segment as an active CDN * request so the existing XHR retry machinery picks it up. The switch * counter is incremented at most once per segment (idempotent via * `segment.p2pSwitchRecorded`), so repeated V2/V1 fallbacks for the same * segment never double-count. Call sites are expected to set * `segment.p2pFailureReason` before calling so the reason is consumed * here rather than passed as a parameter; unset defaults to `'errors'`. * Returns `true` when a CDN URL was assigned and the caller can proceed * with the HTTP request, `false` otherwise. */ _fallBackP2pToCdn(segment: VideoSegment): boolean; onProcessSegmentFail(segment: VideoSegment, retry: boolean): void; onProcessSegmentSuccess(segment: VideoSegment): void; /** * Returns the map of unfinished/active CDN requests. * @returns {Map} The map with the requests. * @public */ enableCDN(name: string, enabled: boolean): boolean; setActive(name: string, enabled: boolean): boolean; setMaxBandwith(name: string, bandwidth: number): boolean; onAbort(xhr: XMLHttpRequest, segment: VideoSegment): void; /** * Arm the connect and total timers for one CDN attempt. Each provider hop * uses a fresh XHR so both timers naturally reset per provider (matches * Android CdnTimeoutSettings: totalTimeout is per-attempt, not per-session). * Values <= 0 mean "no deadline" — the corresponding timer is not armed. * * When `probeOverrides` is supplied (PR6 first-chunk probe), the connect * and total timers use those stricter values instead of the defaults. */ armAttemptTimers(xhr: XMLHttpRequest, segment: VideoSegment, probeOverrides?: { connectTimeoutMs: number; totalDownloadTimeoutMs: number; }): void; /** * Re-arm the read timeout. Read timeout is "between socket reads" — armed at * HEADERS_RECEIVED and re-armed on every progress tick. <= 0 disables. */ armReadTimeout(xhr: XMLHttpRequest, segment: VideoSegment): void; clearConnectTimer(xhr: XMLHttpRequest): void; /** * Recompute the trial call deadline from the announced Content-Length, the * current good CDN's bandwidth, and that CDN's idle ratio (L0). Replaces * the active total-attempt timer ONLY when the new deadline is stricter * than the baseline already armed (Android parity: * TrialDeadlineEventListener.responseHeadersEnd, "stricter only" guard). * * NOTE: the Android impl reads L0 from a single global StatsCollector * shared across all providers; the JS impl tracks concurrency per-Cdn. * For the trial decision the current CDN's idle fraction is the relevant * signal (it's the CDN we're protecting from underrun) so the divergence * is benign in `cdnPriority`/single-CDN-active modes — and a closer match * to the writeup's intent ("CDN idle") than a global aggregate would be. */ private _recomputeTrialDeadline; /** * A trial that missed its tightened deadline but will end sooner than a fresh download from the * current CDN would is left alone: the timer moves to the projected end (once per attempt). * Returns true when the trial was kept. */ private _keepTrialIfFastEnough; /** A cut attempt still teaches: its body bytes over its transfer time become a bandwidth sample of the cut CDN. */ private _recordPartialSample; clearAttemptTimers(xhr: XMLHttpRequest, segment: VideoSegment): void; loadEM(xhr: XMLHttpRequest, url: URL, e: ProgressEvent): void; loadStartEM(xhr: XMLHttpRequest, segment: VideoSegment, e: ProgressEvent): void; progressEM(xhr: XMLHttpRequest, segment: VideoSegment, e: ProgressEvent): void; readyStateChangeEM(xhr: XMLHttpRequest, segment: VideoSegment, cdnBalancer: CdnBalancer): boolean; /** * A status-0 end that arrives once the player's own request time limit has run out (readyState 4 * is delivered before the browser's `timeout` event, so the limit itself is the tell). Balancer * deadlines and player cancels are told apart before this is consulted. */ private _endedByPlayerTimeout; /** * A request that ended without being a CDN failure (the player cancelled it, or its own timeout ran * out) still leaves the loader's bookkeeping: its record, and with P2P on the ACQUIRING announcement * peers may be waiting on (review of PR #300, 2026-09-25). */ private _endedWithoutFailure; /** Milliseconds left of the player's own timeout, or Infinity when the player set none. */ private _playerTimeLeftMs; /** Marks the loader disabled ahead of teardown: an in-flight end after this is teardown, not a failure. */ markDisabled(): void; /** Seconds the player has buffered ahead of its playhead, or null when unknown (set by NpawPlugin). */ bufferAheadProvider?: () => number | null; timeout(xhr: XMLHttpRequest, segment: VideoSegment): void; /** * One step of the transparent retry loop (Android `ProviderLoader.intercept` parity: iterate the * providers inside one player request; only the final outcome reaches the player). * * 1. Book the cut attempt as a failure — no BAN, no failure reason (it was our deadline). * 2. Abandon the shadow silently; the player's XHR keeps its mirrored state and gets no event. * 3. Pick the next CDN: `updateUrl` skips `segment.triedCdns` and falls through to the origin * once every CDN was tried. When the origin was the attempt that just failed, the round is * spent and a NEW round starts (`triedCdns` cleared): the balancer never hands the player a * failure of its own making. What ends a request that finds no working host is the player's * own timeout (hls.js 120 s `maxLoadTimeMs`, Shaka's retry timeout, ...) — exactly what * happens with no balancer at all — or, as a leak guard, TRANSPARENT_RETRY_MAX_MS. A host * that comes back mid-outage is picked up on the next attempt, within one deadline. * 4. Reset the segment's per-attempt fields, re-arm the deadlines, start the next bridge. */ retryOnNextCdn(xhr: XMLHttpRequest, segment: VideoSegment, bridge: ShadowXhrBridge): void; /** * Canonical XHR exit point. `loadend` fires after every termination path — * success, HTTP error, network error, abort, browser timeout — so it is the * one place where teardown is guaranteed to run. Android parity * (`plugin-android@02c4f055` "cancel timeout deadline on every body-source * error path"): without this, a network-level failure that never reaches * `readyState=4` (DNS fail, CORS reject, connection refused) leaves the * connect/total deadlines armed until they fire and `abort()` an already- * dead XHR. `clearAttemptTimers` is idempotent so the double-clear on the * normal success path (which also clears at `readyState=4`) is harmless. */ loadEndEM(xhr: XMLHttpRequest, segment: VideoSegment, _e: ProgressEvent): void; errorEM(xhr: XMLHttpRequest, segment: VideoSegment, e: ProgressEvent): void; getStats(): LoaderStats; destroy(): void; getExternalProviders(): BalancerProvider[]; setExternalProviders(providers: BalancerProvider[], options?: ExternalProvidersOptions): void; updateExternalProviders(providers: BalancerProvider[], options?: ExternalProvidersOptions): void; clearExternalProviders(): void; appendExternalProviders(providers: BalancerProvider[]): void; prependExternalProviders(providers: BalancerProvider[]): void; insertExternalProviders(providers: BalancerProvider[], index: number): void; /** * Applied on every successful /decision: remembers the raw response and returns * the overlay-merged response when a runtime overlay is set. */ applyDecisionResponse(decision: balancerResponse, originalHost: string): balancerResponse; enable(): void; disable(): void; /** * The key under which two manifest URLs are the same content, for every "same manifest" check: the * decision throttle, new-video detection and the balancer analytics view. Ignores the LL-HLS directives, * `ephemeralQueryParams` (the default cache-busters when unset) and every CDN credential name the token * registry knows, built-in or learned from a decision, as Android's ManifestKeyNormalizer does. Only a * comparison key: no URL sent anywhere is changed. */ manifestKey(url: string): string; /** `url` without the query params the token registry knows as CDN credentials; fragment and order kept. */ withoutCredentials(url: string): string; getIsEnabled(): boolean; getResourceIdentifier(): ResourceIdentifier; getP2PManifestRegistry(): P2PManifestRegistry; getP2PSegmentIdResolver(): P2PSegmentIdResolver; getDiskSegmentStore(): DiskSegmentStore; getManifestResolutionStats(): { parseCount: number; resolveCount: number; resolveHits: number; resolveMisses: number; lookaheadHits: number; lastHitFastHits: number; lastHitForceHits: number; multiPassHits: number; subManifestAttachments: number; manifestCount: number; lookaheadSize: number; modelHits: number; fallbackCount: number; }; private getByteRangeOffset; }