Skip to content

Commit

Permalink
Implement ErrorController
Browse files Browse the repository at this point in the history
Add Error Handling Integration Tests
Handle missed probe transmux probe with with FRAG_PARSING_ERROR
Handle encrypted init segment and subtitle decryption failure with FRAG_DECRYPT_ERROR
Remove delay when retrying after timeout
Trigger FRAG_LOADING after request is made
ERROR event data type must include an error property
Remove use of `console.warn` from mp4-tools
Add missing TypeScript type exports
  • Loading branch information
robwalch committed Feb 15, 2023
1 parent 3e578db commit 49a3da3
Show file tree
Hide file tree
Showing 38 changed files with 2,615 additions and 768 deletions.
710 changes: 690 additions & 20 deletions api-extractor/report/hls.js.api.md

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import EMEController, {
} from './controller/eme-controller';
import CMCDController from './controller/cmcd-controller';
import ContentSteeringController from './controller/content-steering-controller';
import ErrorController from './controller/error-controller';
import XhrLoader from './utils/xhr-loader';
import FetchLoader, { fetchSupported } from './utils/fetch-loader';
import Cues from './utils/cues';
Expand Down Expand Up @@ -235,7 +236,7 @@ export type HlsConfig = {
cmcdController?: typeof CMCDController;
// Content Steering
contentSteeringController?: typeof ContentSteeringController;

errorController: typeof ErrorController;
abrController: typeof AbrController;
bufferController: typeof BufferController;
capLevelController: typeof CapLevelController;
Expand Down Expand Up @@ -299,13 +300,13 @@ export const hlsDefaultConfig: HlsConfig = {
manifestLoadingMaxRetryTimeout: 64000, // used by playlist-loader
startLevel: undefined, // used by level-controller
levelLoadingTimeOut: 10000, // used by playlist-loader
levelLoadingMaxRetry: 4, // used by playlist-loader
levelLoadingRetryDelay: 1000, // used by playlist-loader
levelLoadingMaxRetryTimeout: 64000, // used by playlist-loader
levelLoadingMaxRetry: 4, // used by playlist/track controllers
levelLoadingRetryDelay: 1000, // used by playlist/track controllers
levelLoadingMaxRetryTimeout: 64000, // used by playlist/track controllers
fragLoadingTimeOut: 20000, // used by fragment-loader
fragLoadingMaxRetry: 6, // used by fragment-loader
fragLoadingRetryDelay: 1000, // used by fragment-loader
fragLoadingMaxRetryTimeout: 64000, // used by fragment-loader
fragLoadingMaxRetry: 6, // used by stream controllers
fragLoadingRetryDelay: 1000, // used by stream controllers
fragLoadingMaxRetryTimeout: 64000, // used by stream controllers
startFragPrefetch: false, // used by stream-controller
fpsDroppedMonitoringPeriod: 5000, // used by fps-controller
fpsDroppedMonitoringThreshold: 0.2, // used by fps-controller
Expand Down Expand Up @@ -366,6 +367,7 @@ export const hlsDefaultConfig: HlsConfig = {
contentSteeringController: __USE_CONTENT_STEERING__
? ContentSteeringController
: undefined,
errorController: ErrorController,
};

function timelineConfig(): TimelineControllerConfig {
Expand Down
24 changes: 12 additions & 12 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Events } from '../events';
import { Bufferable, BufferHelper } from '../utils/buffer-helper';
import { FragmentState } from './fragment-tracker';
import { Level } from '../types/level';
import { PlaylistLevelType } from '../types/loader';
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
import { Fragment, ElementaryStreamTypes, Part } from '../loader/fragment';
import ChunkCache from '../demux/chunk-cache';
import TransmuxerInterface from '../demux/transmuxer-interface';
Expand Down Expand Up @@ -636,28 +636,28 @@ class AudioStreamController
}

private onError(event: Events.ERROR, data: ErrorData) {
if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) {
this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data);
if (data.fatal) {
this.state = State.ERROR;
return;
}
switch (data.details) {
case ErrorDetails.FRAG_PARSING_ERROR:
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.FRAG_PARSING_ERROR:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
// TODO: Skip fragments that do not belong to this.fragCurrent audio-group id
this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data);
break;
case ErrorDetails.AUDIO_TRACK_LOAD_ERROR:
case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT:
// when in ERROR state, don't switch back to IDLE state in case a non-fatal error is received
if (this.state !== State.ERROR && this.state !== State.STOPPED) {
// if fatal error, stop processing, otherwise move to IDLE to retry loading
this.state = data.fatal ? State.ERROR : State.IDLE;
this.warn(
`${data.details} while loading frag, switching to ${this.state} state`
);
case ErrorDetails.LEVEL_PARSING_ERROR:
// in case of non fatal error while loading level, if level controller is not retrying to load level, switch back to IDLE
if (
!data.levelRetry &&
this.state === State.WAITING_TRACK &&
data.context?.type === PlaylistContextType.AUDIO_TRACK
) {
this.state = State.IDLE;
}
break;
case ErrorDetails.BUFFER_FULL_ERROR:
Expand Down
9 changes: 6 additions & 3 deletions src/controller/audio-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ class AudioTrackController extends BasePlaylistController {
}

protected onError(event: Events.ERROR, data: ErrorData): void {
super.onError(event, data);
if (data.fatal || !data.context) {
return;
}
Expand All @@ -156,7 +155,7 @@ class AudioTrackController extends BasePlaylistController {
data.context.id === this.trackId &&
data.context.groupId === this.groupId
) {
this.retryLoadingOrFail(data);
this.checkRetry(data);
}
}

Expand Down Expand Up @@ -217,12 +216,16 @@ class AudioTrackController extends BasePlaylistController {
if (trackId !== -1) {
this.setAudioTrack(trackId);
} else {
this.warn(`No track found for running audio group-ID: ${this.groupId}`);
const error = new Error(
`No track found for running audio group-ID: ${this.groupId}`
);
this.warn(error.message);

this.hls.trigger(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR,
fatal: true,
error,
});
}
}
Expand Down
29 changes: 9 additions & 20 deletions src/controller/base-playlist-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import type {
TrackLoadedData,
} from '../types/events';
import { ErrorData } from '../types/events';
import { Events } from '../events';
import { ErrorTypes } from '../errors';
import { ErrorDetails } from '../errors';

export default class BasePlaylistController implements NetworkComponentAPI {
protected hls: Hls;
Expand All @@ -35,16 +34,6 @@ export default class BasePlaylistController implements NetworkComponentAPI {
this.hls = this.log = this.warn = null;
}

protected onError(event: Events.ERROR, data: ErrorData): void {
if (
data.fatal &&
(data.type === ErrorTypes.NETWORK_ERROR ||
data.type === ErrorTypes.KEY_SYSTEM_ERROR)
) {
this.stopLoad();
}
}

protected clearTimer(): void {
clearTimeout(this.timer);
this.timer = -1;
Expand Down Expand Up @@ -299,7 +288,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
return new HlsUrlParameters(msn, part, skip);
}

protected retryLoadingOrFail(errorEvent: ErrorData): boolean {
protected checkRetry(errorEvent: ErrorData): boolean {
const { config } = this.hls;
const retry = this.retryCount < config.levelLoadingMaxRetry;
if (retry) {
Expand All @@ -318,20 +307,20 @@ export default class BasePlaylistController implements NetworkComponentAPI {
// exponential backoff capped to max retry timeout
const delay = Math.min(
Math.pow(2, this.retryCount) * config.levelLoadingRetryDelay,
config.levelLoadingMaxRetryTimeout
errorEvent.details === ErrorDetails.LEVEL_LOAD_TIMEOUT ||
errorEvent.details === ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT ||
errorEvent.details === ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT
? 0
: config.levelLoadingMaxRetryTimeout
);
// Schedule level/track reload
this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
this.warn(
`retry playlist loading #${this.retryCount} in ${delay} ms after "${errorEvent.details}"`
);
}
} else {
this.warn(`cannot recover from error "${errorEvent.details}"`);
// stopping live reloading timer if any
this.clearTimer();
// switch error to fatal
errorEvent.fatal = true;
// boolean used to inform other controllers that a retry is happening
errorEvent.levelRetry = true;
}
return retry;
}
Expand Down
Loading

0 comments on commit 49a3da3

Please sign in to comment.