Skip to content

Commit

Permalink
Implement ErrorActions and Pathway Switching
Browse files Browse the repository at this point in the history
  • Loading branch information
robwalch committed Feb 22, 2023
1 parent 9bb6060 commit 024db11
Show file tree
Hide file tree
Showing 29 changed files with 742 additions and 370 deletions.
67 changes: 60 additions & 7 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class AbrController implements AbrComponentAPI {
// (undocumented)
protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData): void;
// (undocumented)
protected onLevelSwitching(event: Events.LEVEL_SWITCHING, data: LevelSwitchingData): void;
// (undocumented)
protected registerListeners(): void;
// (undocumented)
protected unregisterListeners(): void;
Expand Down Expand Up @@ -232,8 +234,6 @@ export class BasePlaylistController implements NetworkComponentAPI {
// (undocumented)
protected requestScheduled: number;
// (undocumented)
protected retryCount: number;
// (undocumented)
protected shouldLoadPlaylist(playlist: Level | MediaPlaylist): boolean;
// (undocumented)
startLoad(): void;
Expand Down Expand Up @@ -879,15 +879,35 @@ export type EMEControllerConfig = {
requestMediaKeySystemAccessFunc: MediaKeyFunc | null;
};

// Warning: (ae-missing-release-tag) "ErrorActionFlags" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum ErrorActionFlags {
// (undocumented)
MoveAllAlternatesMatchingHDCP = 2,
// (undocumented)
MoveAllAlternatesMatchingHost = 1,
// (undocumented)
None = 0,
// (undocumented)
SwitchToSDR = 4
}

// Warning: (ae-missing-release-tag) "ErrorController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class ErrorController {
export class ErrorController implements NetworkComponentAPI {
constructor(hls: Hls);
// (undocumented)
destroy(): void;
// (undocumented)
onErrorOut(event: Events.ERROR, data: ErrorData): void;
// (undocumented)
sendAlternateToPenaltyBox(data: ErrorData): void;
// (undocumented)
startLoad(startPosition: number): void;
// (undocumented)
stopLoad(): void;
}

// Warning: (ae-missing-release-tag) "ErrorData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand All @@ -904,13 +924,15 @@ export interface ErrorData {
context?: PlaylistLoaderContext;
// (undocumented)
details: ErrorDetails;
// (undocumented)
// @deprecated (undocumented)
err?: {
message: string;
};
// (undocumented)
error: Error;
// (undocumented)
errorAction?: IErrorAction;
// (undocumented)
event?: keyof HlsListeners | 'demuxerWorker';
// (undocumented)
fatal: boolean;
Expand Down Expand Up @@ -1446,7 +1468,7 @@ export type HdcpLevel = (typeof HdcpLevels)[number];
// Warning: (ae-missing-release-tag) "HdcpLevels" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const HdcpLevels: readonly ["NONE", "TYPE-0", "TYPE-1", "TYPE-2", null];
export const HdcpLevels: readonly ["NONE", "TYPE-0", "TYPE-1", null];

// @public
class Hls implements HlsEventEmitter {
Expand Down Expand Up @@ -1793,6 +1815,19 @@ export class HlsUrlParameters {
skip?: HlsSkip;
}

// Warning: (ae-missing-release-tag) "IErrorAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type IErrorAction = {
action: NetworkErrorAction;
flags: ErrorActionFlags;
retryCount?: number;
retryConfig?: RetryConfig;
hdcpLevel?: HdcpLevel;
nextAutoLevel?: number;
resolved?: boolean;
};

// Warning: (ae-missing-release-tag) "ILogger" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -2261,7 +2296,7 @@ export interface LevelUpdatedData {

// Warning: (ae-missing-release-tag) "LiveBackBufferData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
// @public @deprecated (undocumented)
export interface LiveBackBufferData extends BackBufferData {
}

Expand Down Expand Up @@ -2381,7 +2416,7 @@ export interface LoaderResponse {
// (undocumented)
code?: number;
// (undocumented)
data: string | ArrayBuffer | Object;
data?: string | ArrayBuffer | Object;
// (undocumented)
text?: string;
// (undocumented)
Expand Down Expand Up @@ -2673,6 +2708,24 @@ export interface NetworkComponentAPI extends ComponentAPI {
stopLoad(): void;
}

// Warning: (ae-missing-release-tag) "NetworkErrorAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export enum NetworkErrorAction {
// (undocumented)
DoNothing = 0,
// (undocumented)
InsertDiscontinuity = 4,
// (undocumented)
RemoveAlternatePermanently = 3,
// (undocumented)
RetryRequest = 5,
// (undocumented)
SendAlternateToPenaltyBox = 2,
// (undocumented)
SendEndCallback = 1
}

// Warning: (ae-missing-release-tag) "NonNativeTextTrack" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down
44 changes: 24 additions & 20 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ export type HlsConfig = {
FragmentLoaderConfig &
PlaylistLoaderConfig;

const defaultLoadPolicy: LoaderConfig = {
maxTimeToFirstByteMs: 8000,
maxLoadTimeMs: 20000,
timeoutRetry: null,
errorRetry: null,
};

/**
* @ignore
* If possible, keep hlsDefaultConfig shallow
Expand Down Expand Up @@ -374,12 +381,7 @@ export const hlsDefaultConfig: HlsConfig = {
enableID3MetadataCues: true,

certLoadPolicy: {
default: {
maxTimeToFirstByteMs: 8000,
maxLoadTimeMs: 20000,
timeoutRetry: null,
errorRetry: null,
},
default: defaultLoadPolicy,
},
keyLoadPolicy: {
default: {
Expand Down Expand Up @@ -448,20 +450,22 @@ export const hlsDefaultConfig: HlsConfig = {
},
},
steeringManifestLoadPolicy: {
default: {
maxTimeToFirstByteMs: 10000,
maxLoadTimeMs: 20000,
timeoutRetry: {
maxNumRetry: 2,
retryDelayMs: 0,
maxRetryDelayMs: 0,
},
errorRetry: {
maxNumRetry: 1,
retryDelayMs: 1000,
maxRetryDelayMs: 8000,
},
},
default: __USE_CONTENT_STEERING__
? {
maxTimeToFirstByteMs: 10000,
maxLoadTimeMs: 20000,
timeoutRetry: {
maxNumRetry: 2,
retryDelayMs: 0,
maxRetryDelayMs: 0,
},
errorRetry: {
maxNumRetry: 1,
retryDelayMs: 1000,
maxRetryDelayMs: 8000,
},
}
: defaultLoadPolicy,
},

// These default settings are deprecated in favor of the above policies
Expand Down
10 changes: 10 additions & 0 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
FragLoadedData,
FragBufferedData,
LevelLoadedData,
LevelSwitchingData,
} from '../types/events';
import type { AbrComponentAPI } from '../types/component-api';

Expand Down Expand Up @@ -44,6 +45,7 @@ class AbrController implements AbrComponentAPI {
hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
}

Expand All @@ -52,6 +54,7 @@ class AbrController implements AbrComponentAPI {
hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
}

Expand All @@ -74,6 +77,13 @@ class AbrController implements AbrComponentAPI {
this.timer = self.setInterval(this.onCheck, 100);
}

protected onLevelSwitching(
event: Events.LEVEL_SWITCHING,
data: LevelSwitchingData
): void {
this.clearTimer();
}

protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
const config = this.hls.config;
if (data.details.live) {
Expand Down
4 changes: 2 additions & 2 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,9 @@ class AudioStreamController

if (fragCurrent) {
fragCurrent.abortRequests();
this.fragmentTracker.removeFragment(fragCurrent);
}
this.fragCurrent = null;
this.clearWaitingFragment();
this.resetLoadingState();
// destroy useless transmuxer when switching audio to main
if (!altAudio) {
this.resetTransmuxer();
Expand Down
1 change: 0 additions & 1 deletion src/controller/audio-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ class AudioTrackController extends BasePlaylistController {
);

if (id === this.trackId) {
this.retryCount = 0;
this.playlistLoaded(id, data, curDetails);
}
}
Expand Down
40 changes: 16 additions & 24 deletions src/controller/base-playlist-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import type {
TrackLoadedData,
} from '../types/events';
import { ErrorData } from '../types/events';
import { ErrorDetails } from '../errors';
import { NetworkErrorAction } from '../errors';
import { getRetryDelay, isTimeoutError } from '../utils/error-helper';

export default class BasePlaylistController implements NetworkComponentAPI {
protected hls: Hls;
protected timer: number = -1;
protected requestScheduled: number = -1;
protected canLoad: boolean = false;
protected retryCount: number = 0;
protected log: (msg: any) => void;
protected warn: (msg: any) => void;

Expand All @@ -41,7 +41,6 @@ export default class BasePlaylistController implements NetworkComponentAPI {

public startLoad(): void {
this.canLoad = true;
this.retryCount = 0;
this.requestScheduled = -1;
this.loadPlaylist();
}
Expand Down Expand Up @@ -290,44 +289,37 @@ export default class BasePlaylistController implements NetworkComponentAPI {
}

protected checkRetry(errorEvent: ErrorData): boolean {
const { playlistLoadPolicy } = this.hls.config;
const errorDetails = errorEvent.details;
const isTimeout =
errorDetails === ErrorDetails.LEVEL_LOAD_TIMEOUT ||
errorDetails === ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT ||
errorDetails === ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT;
const httpStatus = errorEvent.response?.code;
const retryConfig =
playlistLoadPolicy.default[`${isTimeout ? 'timeout' : 'error'}Retry`];
const isTimeout = isTimeoutError(errorEvent);
const errorAction = errorEvent.errorAction;
const { action, retryCount = 0, retryConfig } = errorAction || {};
const retry =
!!retryConfig &&
this.retryCount < retryConfig.maxNumRetry &&
httpStatus !== 0;
action === NetworkErrorAction.RetryRequest &&
!!errorAction &&
!!retryConfig;
if (retry) {
this.requestScheduled = -1;
const retryCount = ++this.retryCount;
if (isTimeout && errorEvent.context?.deliveryDirectives) {
// The LL-HLS request already timed out so retry immediately
this.warn(
`Retrying playlist loading ${retryCount}/${retryConfig.maxNumRetry} after "${errorDetails}" without delivery-directives`
`Retrying playlist loading ${retryCount + 1}/${
retryConfig.maxNumRetry
} after "${errorDetails}" without delivery-directives`
);
this.loadPlaylist();
} else {
// exponential backoff capped to max retry delay
const backoffFactor =
retryConfig.backoff === 'linear' ? 1 : Math.pow(2, retryCount - 1);
const delay = Math.min(
backoffFactor * retryConfig.retryDelayMs,
retryConfig.maxRetryDelayMs
);
const delay = getRetryDelay(retryConfig, retryCount);
// Schedule level/track reload
this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
this.warn(
`Retrying playlist loading ${retryCount}/${retryConfig.maxNumRetry} after "${errorDetails}" in ${delay}ms`
`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;
}
Expand Down

0 comments on commit 024db11

Please sign in to comment.