video
This commit is contained in:
348
node_modules/hls.js/src/controller/base-playlist-controller.ts
generated
vendored
Normal file
348
node_modules/hls.js/src/controller/base-playlist-controller.ts
generated
vendored
Normal file
@@ -0,0 +1,348 @@
|
||||
import type Hls from '../hls';
|
||||
import type { NetworkComponentAPI } from '../types/component-api';
|
||||
import { getSkipValue, HlsSkip, HlsUrlParameters, Level } from '../types/level';
|
||||
import { computeReloadInterval, mergeDetails } from '../utils/level-helper';
|
||||
import { ErrorData } from '../types/events';
|
||||
import { getRetryDelay, isTimeoutError } from '../utils/error-helper';
|
||||
import { NetworkErrorAction } from './error-controller';
|
||||
import { logger } from '../utils/logger';
|
||||
import type { LevelDetails } from '../loader/level-details';
|
||||
import type { MediaPlaylist } from '../types/media-playlist';
|
||||
import type {
|
||||
AudioTrackLoadedData,
|
||||
LevelLoadedData,
|
||||
TrackLoadedData,
|
||||
} from '../types/events';
|
||||
|
||||
export default class BasePlaylistController implements NetworkComponentAPI {
|
||||
protected hls: Hls;
|
||||
protected timer: number = -1;
|
||||
protected requestScheduled: number = -1;
|
||||
protected canLoad: boolean = false;
|
||||
protected log: (msg: any) => void;
|
||||
protected warn: (msg: any) => void;
|
||||
|
||||
constructor(hls: Hls, logPrefix: string) {
|
||||
this.log = logger.log.bind(logger, `${logPrefix}:`);
|
||||
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
|
||||
this.hls = hls;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.clearTimer();
|
||||
// @ts-ignore
|
||||
this.hls = this.log = this.warn = null;
|
||||
}
|
||||
|
||||
protected clearTimer(): void {
|
||||
if (this.timer !== -1) {
|
||||
self.clearTimeout(this.timer);
|
||||
this.timer = -1;
|
||||
}
|
||||
}
|
||||
|
||||
public startLoad(): void {
|
||||
this.canLoad = true;
|
||||
this.requestScheduled = -1;
|
||||
this.loadPlaylist();
|
||||
}
|
||||
|
||||
public stopLoad(): void {
|
||||
this.canLoad = false;
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
protected switchParams(
|
||||
playlistUri: string,
|
||||
previous: LevelDetails | undefined,
|
||||
current: LevelDetails | undefined,
|
||||
): HlsUrlParameters | undefined {
|
||||
const renditionReports = previous?.renditionReports;
|
||||
if (renditionReports) {
|
||||
let foundIndex = -1;
|
||||
for (let i = 0; i < renditionReports.length; i++) {
|
||||
const attr = renditionReports[i];
|
||||
let uri: string;
|
||||
try {
|
||||
uri = new self.URL(attr.URI, previous.url).href;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Could not construct new URL for Rendition Report: ${error}`,
|
||||
);
|
||||
uri = attr.URI || '';
|
||||
}
|
||||
// Use exact match. Otherwise, the last partial match, if any, will be used
|
||||
// (Playlist URI includes a query string that the Rendition Report does not)
|
||||
if (uri === playlistUri) {
|
||||
foundIndex = i;
|
||||
break;
|
||||
} else if (uri === playlistUri.substring(0, uri.length)) {
|
||||
foundIndex = i;
|
||||
}
|
||||
}
|
||||
if (foundIndex !== -1) {
|
||||
const attr = renditionReports[foundIndex];
|
||||
const msn = parseInt(attr['LAST-MSN']) || previous?.lastPartSn;
|
||||
let part = parseInt(attr['LAST-PART']) || previous?.lastPartIndex;
|
||||
if (this.hls.config.lowLatencyMode) {
|
||||
const currentGoal = Math.min(
|
||||
previous.age - previous.partTarget,
|
||||
previous.targetduration,
|
||||
);
|
||||
if (part >= 0 && currentGoal > previous.partTarget) {
|
||||
part += 1;
|
||||
}
|
||||
}
|
||||
const skip = current && getSkipValue(current);
|
||||
return new HlsUrlParameters(msn, part >= 0 ? part : undefined, skip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
|
||||
if (this.requestScheduled === -1) {
|
||||
this.requestScheduled = self.performance.now();
|
||||
}
|
||||
// Loading is handled by the subclasses
|
||||
}
|
||||
|
||||
protected shouldLoadPlaylist(
|
||||
playlist: Level | MediaPlaylist | null | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
this.canLoad &&
|
||||
!!playlist &&
|
||||
!!playlist.url &&
|
||||
(!playlist.details || playlist.details.live)
|
||||
);
|
||||
}
|
||||
|
||||
protected shouldReloadPlaylist(
|
||||
playlist: Level | MediaPlaylist | null | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
this.timer === -1 &&
|
||||
this.requestScheduled === -1 &&
|
||||
this.shouldLoadPlaylist(playlist)
|
||||
);
|
||||
}
|
||||
|
||||
protected playlistLoaded(
|
||||
index: number,
|
||||
data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData,
|
||||
previousDetails?: LevelDetails,
|
||||
) {
|
||||
const { details, stats } = data;
|
||||
|
||||
// Set last updated date-time
|
||||
const now = self.performance.now();
|
||||
const elapsed = stats.loading.first
|
||||
? Math.max(0, now - stats.loading.first)
|
||||
: 0;
|
||||
details.advancedDateTime = Date.now() - elapsed;
|
||||
|
||||
// if current playlist is a live playlist, arm a timer to reload it
|
||||
if (details.live || previousDetails?.live) {
|
||||
details.reloaded(previousDetails);
|
||||
if (previousDetails) {
|
||||
this.log(
|
||||
`live playlist ${index} ${
|
||||
details.advanced
|
||||
? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex
|
||||
: details.updated
|
||||
? 'UPDATED'
|
||||
: 'MISSED'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
// Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
|
||||
if (previousDetails && details.fragments.length > 0) {
|
||||
mergeDetails(previousDetails, details);
|
||||
}
|
||||
if (!this.canLoad || !details.live) {
|
||||
return;
|
||||
}
|
||||
let deliveryDirectives: HlsUrlParameters | undefined;
|
||||
let msn: number | undefined = undefined;
|
||||
let part: number | undefined = undefined;
|
||||
if (details.canBlockReload && details.endSN && details.advanced) {
|
||||
// Load level with LL-HLS delivery directives
|
||||
const lowLatencyMode = this.hls.config.lowLatencyMode;
|
||||
const lastPartSn = details.lastPartSn;
|
||||
const endSn = details.endSN;
|
||||
const lastPartIndex = details.lastPartIndex;
|
||||
const hasParts = lastPartIndex !== -1;
|
||||
const lastPart = lastPartSn === endSn;
|
||||
// When low latency mode is disabled, we'll skip part requests once the last part index is found
|
||||
const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex;
|
||||
if (hasParts) {
|
||||
msn = lastPart ? endSn + 1 : lastPartSn;
|
||||
part = lastPart ? nextSnStartIndex : lastPartIndex + 1;
|
||||
} else {
|
||||
msn = endSn + 1;
|
||||
}
|
||||
// Low-Latency CDN Tune-in: "age" header and time since load indicates we're behind by more than one part
|
||||
// Update directives to obtain the Playlist that has the estimated additional duration of media
|
||||
const lastAdvanced = details.age;
|
||||
const cdnAge = lastAdvanced + details.ageHeader;
|
||||
let currentGoal = Math.min(
|
||||
cdnAge - details.partTarget,
|
||||
details.targetduration * 1.5,
|
||||
);
|
||||
if (currentGoal > 0) {
|
||||
if (previousDetails && currentGoal > previousDetails.tuneInGoal) {
|
||||
// If we attempted to get the next or latest playlist update, but currentGoal increased,
|
||||
// then we either can't catchup, or the "age" header cannot be trusted.
|
||||
this.warn(
|
||||
`CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`,
|
||||
);
|
||||
currentGoal = 0;
|
||||
} else {
|
||||
const segments = Math.floor(currentGoal / details.targetduration);
|
||||
msn += segments;
|
||||
if (part !== undefined) {
|
||||
const parts = Math.round(
|
||||
(currentGoal % details.targetduration) / details.partTarget,
|
||||
);
|
||||
part += parts;
|
||||
}
|
||||
this.log(
|
||||
`CDN Tune-in age: ${
|
||||
details.ageHeader
|
||||
}s last advanced ${lastAdvanced.toFixed(
|
||||
2,
|
||||
)}s goal: ${currentGoal} skip sn ${segments} to part ${part}`,
|
||||
);
|
||||
}
|
||||
details.tuneInGoal = currentGoal;
|
||||
}
|
||||
deliveryDirectives = this.getDeliveryDirectives(
|
||||
details,
|
||||
data.deliveryDirectives,
|
||||
msn,
|
||||
part,
|
||||
);
|
||||
if (lowLatencyMode || !lastPart) {
|
||||
this.loadPlaylist(deliveryDirectives);
|
||||
return;
|
||||
}
|
||||
} else if (details.canBlockReload || details.canSkipUntil) {
|
||||
deliveryDirectives = this.getDeliveryDirectives(
|
||||
details,
|
||||
data.deliveryDirectives,
|
||||
msn,
|
||||
part,
|
||||
);
|
||||
}
|
||||
const bufferInfo = this.hls.mainForwardBufferInfo;
|
||||
const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
|
||||
const distanceToLiveEdgeMs = (details.edge - position) * 1000;
|
||||
const reloadInterval = computeReloadInterval(
|
||||
details,
|
||||
distanceToLiveEdgeMs,
|
||||
);
|
||||
if (details.updated && now > this.requestScheduled + reloadInterval) {
|
||||
this.requestScheduled = stats.loading.start;
|
||||
}
|
||||
|
||||
if (msn !== undefined && details.canBlockReload) {
|
||||
this.requestScheduled =
|
||||
stats.loading.first +
|
||||
reloadInterval -
|
||||
(details.partTarget * 1000 || 1000);
|
||||
} else if (
|
||||
this.requestScheduled === -1 ||
|
||||
this.requestScheduled + reloadInterval < now
|
||||
) {
|
||||
this.requestScheduled = now;
|
||||
} else if (this.requestScheduled - now <= 0) {
|
||||
this.requestScheduled += reloadInterval;
|
||||
}
|
||||
let estimatedTimeUntilUpdate = this.requestScheduled - now;
|
||||
estimatedTimeUntilUpdate = Math.max(0, estimatedTimeUntilUpdate);
|
||||
this.log(
|
||||
`reload live playlist ${index} in ${Math.round(
|
||||
estimatedTimeUntilUpdate,
|
||||
)} ms`,
|
||||
);
|
||||
// this.log(
|
||||
// `live reload ${details.updated ? 'REFRESHED' : 'MISSED'}
|
||||
// reload in ${estimatedTimeUntilUpdate / 1000}
|
||||
// round trip ${(stats.loading.end - stats.loading.start) / 1000}
|
||||
// diff ${
|
||||
// (reloadInterval -
|
||||
// (estimatedTimeUntilUpdate +
|
||||
// stats.loading.end -
|
||||
// stats.loading.start)) /
|
||||
// 1000
|
||||
// }
|
||||
// reload interval ${reloadInterval / 1000}
|
||||
// target duration ${details.targetduration}
|
||||
// distance to edge ${distanceToLiveEdgeMs / 1000}`
|
||||
// );
|
||||
|
||||
this.timer = self.setTimeout(
|
||||
() => this.loadPlaylist(deliveryDirectives),
|
||||
estimatedTimeUntilUpdate,
|
||||
);
|
||||
} else {
|
||||
this.clearTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private getDeliveryDirectives(
|
||||
details: LevelDetails,
|
||||
previousDeliveryDirectives: HlsUrlParameters | null,
|
||||
msn?: number,
|
||||
part?: number,
|
||||
): HlsUrlParameters {
|
||||
let skip = getSkipValue(details);
|
||||
if (previousDeliveryDirectives?.skip && details.deltaUpdateFailed) {
|
||||
msn = previousDeliveryDirectives.msn;
|
||||
part = previousDeliveryDirectives.part;
|
||||
skip = HlsSkip.No;
|
||||
}
|
||||
return new HlsUrlParameters(msn, part, skip);
|
||||
}
|
||||
|
||||
protected checkRetry(errorEvent: ErrorData): boolean {
|
||||
const errorDetails = errorEvent.details;
|
||||
const isTimeout = isTimeoutError(errorEvent);
|
||||
const errorAction = errorEvent.errorAction;
|
||||
const { action, retryCount = 0, retryConfig } = errorAction || {};
|
||||
const retry =
|
||||
!!errorAction &&
|
||||
!!retryConfig &&
|
||||
(action === NetworkErrorAction.RetryRequest ||
|
||||
(!errorAction.resolved &&
|
||||
action === NetworkErrorAction.SendAlternateToPenaltyBox));
|
||||
if (retry) {
|
||||
this.requestScheduled = -1;
|
||||
if (retryCount >= retryConfig.maxNumRetry) {
|
||||
return false;
|
||||
}
|
||||
if (isTimeout && errorEvent.context?.deliveryDirectives) {
|
||||
// The LL-HLS request already timed out so retry immediately
|
||||
this.warn(
|
||||
`Retrying playlist loading ${retryCount + 1}/${
|
||||
retryConfig.maxNumRetry
|
||||
} after "${errorDetails}" without delivery-directives`,
|
||||
);
|
||||
this.loadPlaylist();
|
||||
} else {
|
||||
const delay = getRetryDelay(retryConfig, retryCount);
|
||||
// Schedule level/track reload
|
||||
this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
|
||||
this.warn(
|
||||
`Retrying playlist loading ${retryCount + 1}/${
|
||||
retryConfig.maxNumRetry
|
||||
} after "${errorDetails}" in ${delay}ms`,
|
||||
);
|
||||
}
|
||||
// `levelRetry = true` used to inform other controllers that a retry is happening
|
||||
errorEvent.levelRetry = true;
|
||||
errorAction.resolved = true;
|
||||
}
|
||||
return retry;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user