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 21, 2023
1 parent 8722c3b commit fcb1354
Show file tree
Hide file tree
Showing 22 changed files with 534 additions and 288 deletions.
65 changes: 58 additions & 7 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,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 +877,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 +922,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 +1466,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 +1813,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 +2294,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 +2414,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 +2706,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
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
41 changes: 16 additions & 25 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,40 +289,32 @@ 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 retry =
!!retryConfig &&
this.retryCount < retryConfig.maxNumRetry &&
httpStatus !== 0;
const isTimeout = isTimeoutError(errorEvent);
const {
action,
retryCount = 0,
retryConfig,
} = errorEvent.errorAction || {};
const retry = action === NetworkErrorAction.RetryRequest && !!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
Expand Down
35 changes: 13 additions & 22 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FragmentState } from './fragment-tracker';
import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
import { logger } from '../utils/logger';
import { Events } from '../events';
import { ErrorDetails, ErrorTypes } from '../errors';
import { ErrorDetails, ErrorTypes, NetworkErrorAction } from '../errors';
import { ChunkMetadata } from '../types/transmuxer';
import { appendUint8Array } from '../utils/mp4-tools';
import { alignStream } from '../utils/discontinuities';
Expand Down Expand Up @@ -47,6 +47,7 @@ import type { HlsConfig } from '../config';
import type { NetworkComponentAPI } from '../types/component-api';
import type { SourceBufferName } from '../types/buffer';
import type { RationalTimestamp } from '../utils/timescale-conversion';
import { getRetryDelay } from '../utils/error-helper';

type ResolveFragLoaded = (FragLoadedEndData) => void;
type RejectFragLoaded = (LoadError) => void;
Expand Down Expand Up @@ -1370,7 +1371,6 @@ export default class BaseStreamController
if (!frag || frag.type !== filterType || !this.levels) {
return;
}
const level = this.levels[frag.level];
if (this.fragContextChanged(frag)) {
this.warn(
`Frag load error must match current frag to retry ${frag.url} > ${this.fragCurrent?.url}`
Expand All @@ -1382,37 +1382,28 @@ export default class BaseStreamController
(acc, level) => acc + level.fragmentError,
0
);
const retryCount = level ? fragmentErrors + 1 : Infinity;
const { fragLoadPolicy, keyLoadPolicy } = this.config;
const isTimeout =
data.details === ErrorDetails.FRAG_LOAD_TIMEOUT ||
data.details === ErrorDetails.KEY_LOAD_TIMEOUT;
const retryConfig = (
data.details.startsWith('key') ? keyLoadPolicy : fragLoadPolicy
).default[`${isTimeout ? 'timeout' : 'error'}Retry`];
const retry = !!retryConfig && retryCount <= retryConfig.maxNumRetry;
if (retry) {
level.fragmentError++;
const { action, retryCount = 0, retryConfig } = data.errorAction || {};
if (action === NetworkErrorAction.RetryRequest && retryConfig) {
if (!this.loadedmetadata) {
this.startFragRequested = false;
this.nextLoadPosition = this.startPosition;
}
// exponential backoff capped to max retry delay
const backoffFactor =
retryConfig.backoff === 'linear' ? 1 : Math.pow(2, fragmentErrors);
const delay = Math.min(
backoffFactor * retryConfig.retryDelayMs,
retryConfig.maxRetryDelayMs
);
const delay = getRetryDelay(retryConfig, retryCount);
this.warn(
`Fragment ${frag.sn} of ${filterType} ${frag.level} errored with ${data.details}, retrying loading ${retryCount}/${retryConfig.maxNumRetry} in ${delay}ms`
`Fragment ${frag.sn} of ${filterType} ${frag.level} errored with ${
data.details
}, retrying loading ${retryCount + 1}/${
retryConfig.maxNumRetry
} in ${delay}ms`
);
this.retryDate = self.performance.now() + delay;
this.state = State.FRAG_LOADING_WAITING_RETRY;
} else if (data.levelRetry) {
this.resetFragmentErrors(filterType);
} else {
logger.warn(`${data.details} reached max retry (${fragmentErrors})`);
logger.warn(
`${data.details} reached or exceeded max retry (${fragmentErrors})`
);
// `levelRetry = false` used to inform other controllers that if a level switch is not possible, error should escalated to fatal
data.levelRetry = false;
this.state = State.ERROR;
Expand Down
Loading

0 comments on commit fcb1354

Please sign in to comment.