import { VideoSegment } from '../Storage/VideoSegment'; import { CdnRequestStatus } from '../Utils/Constants'; import type { SyntheticCdnStats } from '../Utils/Constants'; import BalancerOptions from '../Utils/Options'; import Cdn from './Cdn'; import Emitter from '../Utils/Emitter'; import Resource from '../Resource/Resource'; import CdnTokenRegistry from '../BusinessObjects/CdnTokenRegistry'; import P2PLoader from './P2PLoader'; export default class CDNLoader extends Emitter { private _p2pLoader; private _secure?; private _method?; private _activeSwitching?; private _probeOnBanned?; private _noProbing?; private _bandwidthThreshold; private _responseUUID?; private _options; private _cdnList; /** * Per-CDN credential bookkeeping (PGN-4826). Owns which query params are * auth material and which token each host last issued, so a retargeted * request never carries a sibling CDN's credential. */ tokenRegistry: CdnTokenRegistry; /** * Every host a decision has listed as a CDN for the content now playing, with the path prefix that CDN * adds. They stay balanced hosts after a profile save drops them from /decision: the player keeps * requesting them (a manifest reload, a segment it resolved before the save), so those requests must * still reach selection, and the remaining CDNs, instead of going out unsigned as a bypass. Forgotten * only when the content changes; an empty decision for the same content keeps them. */ private _contentCdnHosts; /** The resource those hosts belong to. Its content key is taken when compared, never stored (see `_isSameContent`). */ private _contentResource; private _activeCdnRequests; private _activeRequests; private _failedRequests; private _syntheticCdnStats; private _chunkCount; private _probingInfo; private _highestChunkBandwidth; private _highestRenditionBandwidth; private _cdnLastInstantBandwith; private _cdnInstantBandwith; private _cdnPeakBandwidth; private _retries; private _totalDownloadedBytes; private _firstRequestTime; private _lastChunkTime; private _lastChunksLapse; private _resource; currentRenditionBandwidth: number; currentRenditionPos: number; renditionCount: number; initializated: boolean; monitoringStarted: boolean; lastUsedCdn?: string; private _lastSuccessfulVideoProvider?; codavelAllBanned: boolean; codavelBanCount: number; codavelUnbanCount: number; codavelBanTime: number; codavelUnbanTime: number; codavelSwitches: number; codavelSwitchesDueToConnectivity: number; codavelSwitchesDueToErrors: number; codavelSwitchesDueToQuality: number; codavelBanRefTicToc: number; lastCodavelRequestStatus: CdnRequestStatus; private codavelProvidersState; processedApiCall: boolean; avgApiCallResponseTime?: number; minApiCallResponseTime?: number; maxApiCallResponseTime?: number; apiCallErrors?: number; apiCallResponses?: number; p2pAvailable: boolean; bolinaAvailable: boolean; activeSwitchingPriority: string; priorityCdn: string; announcedCdns: number; /** * Long-lived "last failed trial" marker (PR9 / Android * `ActiveSwitcher.lastFailedTrialProvider`, `plugin-android@56bf3e20`). * Set by `Loader.onProcessSegmentFail` when a tagged trial segment ends in * failure; consumed and cleared by the next `_getCdnToProbe` sort. Lives * on CDNLoader because `CDNSelectorBusinessObject` is reinstantiated per * `updateUrl` call and would otherwise lose the marker between segments. */ private _lastFailedTrialCdn; segmentDuration?: number; private apiHost; /** * The CDN choice cannot change between two requests unless something was learnt in between: a * download finished or failed, a probe or a trial answered, a new decision arrived. Every method that * records one of those bumps `_statsVersion`, and a plain choice (no trial, no P2P, no origin * fallback) is kept and handed to the next request while the version is the same and the choice is * younger than CHOICE_REUSE_MS. A request that could start a trial always runs the full selection, * so trial timing is untouched. FPT's Samsung TV spends 25-90 ms per choice and the player opens the * first four requests of a play back to back with nothing learnt in between: the fourth left 280 ms * after the first (2026-09-25, QA-4438). */ private _statsVersion; private _lastChoice?; /** * Constructs CDNLoader * @param {BalancerOptions} options Options object. */ constructor(options: BalancerOptions, resource: Resource, loader: P2PLoader, apiHost: string); getSecure(): boolean; getActiveRequests(): Map; gotEmptyResponse(): void; /** Something that can change the CDN choice was learnt: the kept choice is dropped. */ noteStatsChanged(): void; private _reusableChoice; /** * For initial workflow and resets, asks to the API for the list of available * CDNs with the content we want to reproduce. * It creates the CDN list object and fills it with the CDNs received. * Also calculates the relative score. * @public */ setSettings(resp: balancerResponse, originalHost: string): void; private updateSettings; /** * Resolve the four-timeout values from /decision and push them into * CDNDefaultSettings so the XHR layer reads a single source. Priority: * 1. customData.cdnTimeout.X (Android-shape nested response) * 2. BalancerOptions.cdnTimeout.X (constructor or dotted customData.options) * 3. customData.cdnTimeout.requestTimeoutMilliseconds (legacy alias for callTimeout) * 4. previous CDNDefaultSettings value (default or last decision) */ private _applyCdnTimeoutSettings; startMonitoring(): void; /** * `cancelledByBalancer`: the attempt was cut by the balancer's own deadline and a transparent * retry takes over inside the same player request. It still counts as a failed attempt, but the * host is not BANNED and no failure reason is tagged — Android does the same on its own cancel * (`CdnProvider.kt:249-262`). Without the retry loop the ban IS the switch, so this flag is only * set from `Loader.retryOnNextCdn` (devs-committee 2026-09-02_plugin-js-no-player-request-abort-after-join). */ updateCDNStatsOnFailure(cdnName: string, segment: VideoSegment, cancelledByBalancer?: boolean): void; removeActiveSegment(segment: VideoSegment): void; updateCDNStatsOnSuccess(segment: VideoSegment): void; /** * Android parity (ProviderLoader.trackSwitch): count P2P->CDN switches only * when the previously successful video provider was actually P2P. The state * is consumed on first fallback so concurrent retries do not inflate * switches before a new successful provider is established. */ consumeLastSuccessfulP2PProvider(): boolean; getCdnList(): Map; /** * Record that the last trial fetch on `cdnName` ended in failure so the * next `_getCdnToProbe` selection skips that CDN. Android parity (PR9 / * `plugin-android@56bf3e20`): set by `Loader.onProcessSegmentFail` when the * failing segment carried a `trialTag`; the next sort consumes and clears * the marker, so it represents "the most recent failed trial" only — not * a persistent blacklist. */ /** * Bandwidth sample from a trial that was cut before it finished (bytes after the first byte over * the transfer time). Returns false when the CDN is unknown or the sample is below the size floor * that also applies to complete downloads. */ recordPartialTrialSample(cdnName: string, bytes: number, ms: number): boolean; markLastFailedTrial(cdnName: string): void; /** Test hook — production callers should not need to read this. */ getLastFailedTrialCdn(): Cdn | null; getSyntheticCdnStats(): { BYPASS: SyntheticCdnStats; BLACKLIST: SyntheticCdnStats; ORIGIN: SyntheticCdnStats; }; /** * Snapshot + reset. Kept the legacy name so external callers (tests, other * packages) keep compiling; returns the full SyntheticCdnStats shape now. */ consumeSyntheticCdnCounts(): { BYPASS: SyntheticCdnStats; BLACKLIST: SyntheticCdnStats; ORIGIN: SyntheticCdnStats; }; /** * Android parity (StatsCollector.kt:549, wrapSyntheticResponse). Accumulate * the response metrics from a completed XHR into the synthetic CDN bucket * tagged on the segment. Only records when bytes > 0 on success (Android * Balancer.kt:358) — no response body means nothing to report. */ private _recordSyntheticResponse; private _bumpHttpBucket; updateProgressStats(): void; private getSignedUrlFromApi; updateUrl(segment: VideoSegment, isProbe?: boolean, cdnName?: string): string | undefined; setResourceInfo(url: string): void; getRecommendedManifest(): Promise; /** * Returns an object with the CDN data stats of the current content/view. * @returns {CdnLoaderStats} CDN info object. * @public */ getStats(): CdnLoaderStats; /** * CAUTION: if you try to disable many CDN you may break the view! * Enables/disables the use of a specific CDN. * You can get the name of the CDNs using getStats().cdn * @param {string} name Cdn name to be disabled/enabled. * @param {boolean} mode True to enable, False to disable. * @returns {boolean} True if change was applied, false if not. * @public */ changeStatusCDN(name: string, enable: boolean): boolean; setMaxBandwidth(name: string, bandwidth: number): boolean; getMaxBandwidth(cdnName: string): number; updateCodavelBanTimes(): void; resetOnPing(): void; onCodavelSwitched(): void; onCodavelSucessfullResponse(): void; onCodavelFailedResponse(status: CdnRequestStatus): void; onFailedApiResponse(): void; onSucessfullApiResponse(responseTime: number): void; getBucket(): string | undefined; getIsLive(): boolean | undefined; getForceDecision(): boolean | undefined; getSegmentDuration(): number | undefined; /** * Host[:port] a request goes to on `cdn`. A provider with no host keeps the request's own host, and * its port too unless it names one — Android `CdnProvider.redirectedToCdnOrNull` parity. Such an * entry (e.g. {"name":"MediaTailor","host":""}) is the stream's own CDN; joining an empty host gave * "https:///path", every request failed and the CDN was banned. */ private cdnAuthority; /** * Notes every host this decision lists as a CDN for the content now playing, and asks the token registry to * keep their credentials past a decision that drops them. Call before `learnFromDecision`. */ private _noteContentCdnHosts; /** * `url` is the content now playing, at most moved to another CDN: the same content key, on the resource's own * host or on a host a decision listed for this content. */ isSameContent(url: string): boolean; private _isSameContent; /** The resource as content: path without the CDN's own prefix, query without credentials or cache-busters. */ private _contentKeyOf; private _forgetContentCdnHosts; /** * The origin fallback for a request on a host that was a CDN of this content carries that host's last known * credential, the only one valid there. Any other origin URL goes out as the player wrote it. */ private _signedOriginUrl; private isNotOnCdnList; private isNotOnWhitelist; private isOnUrlBlacklist; }