diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index cc58b99d8a0..ddaa9948601 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -374,6 +374,8 @@ export interface ErrorData { // (undocumented) bytes?: number; // (undocumented) + chunkMeta?: ChunkMetadata; + // (undocumented) context?: PlaylistLoaderContext; // (undocumented) details: ErrorDetails; diff --git a/src/controller/audio-stream-controller.ts b/src/controller/audio-stream-controller.ts index e3d663b49a4..b94d37229bc 100644 --- a/src/controller/audio-stream-controller.ts +++ b/src/controller/audio-stream-controller.ts @@ -628,6 +628,7 @@ class AudioStreamController switch (data.details) { 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: case ErrorDetails.KEY_SYSTEM_NO_SESSION: diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index 69a3c7376fb..677bc751a41 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -751,7 +751,11 @@ export default class BaseStreamController protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata) { const context = this.getCurrentContext(chunkMeta); if (!context || this.state !== State.PARSING) { - if (!this.fragCurrent) { + if ( + !this.fragCurrent && + this.state !== State.STOPPED && + this.state !== State.ERROR + ) { this.state = State.IDLE; } return; @@ -1304,8 +1308,20 @@ export default class BaseStreamController data: ErrorData ) { if (data.fatal) { + this.stopLoad(); + this.state = State.ERROR; return; } + const config = this.config; + if (data.chunkMeta) { + // Parsing Error: no retries + const context = this.getCurrentContext(data.chunkMeta); + if (context) { + data.frag = context.frag; + data.levelRetry = true; + this.fragLoadError = config.fragLoadingMaxRetry; + } + } const frag = data.frag; // Handle frag error related to caller's filterType if (!frag || frag.type !== filterType) { @@ -1319,7 +1335,6 @@ export default class BaseStreamController frag.urlId === fragCurrent.urlId, 'Frag load error must match current frag to retry' ); - const config = this.config; // keep retrying until the limit will be reached if (this.fragLoadError + 1 <= config.fragLoadingMaxRetry) { if (!this.loadedmetadata) { diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index c9e884ad978..181c3fc17ea 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -362,13 +362,15 @@ export default class LevelController extends BasePlaylistController { } } break; + case ErrorDetails.FRAG_PARSING_ERROR: case ErrorDetails.KEY_SYSTEM_NO_SESSION: case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED: levelIndex = data.frag?.type === PlaylistLevelType.MAIN ? data.frag.level : this.currentLevelIndex; - levelError = true; + // Do not retry level. Escalate to fatal if switching levels fails. + data.levelRetry = false; break; case ErrorDetails.LEVEL_LOAD_ERROR: case ErrorDetails.LEVEL_LOAD_TIMEOUT: @@ -443,6 +445,9 @@ export default class LevelController extends BasePlaylistController { this.warn(`${errorDetails}: switch to ${nextLevel}`); errorEvent.levelRetry = true; this.hls.nextAutoLevel = nextLevel; + } else if (errorEvent.levelRetry === false) { + // No levels to switch to and no more retries + errorEvent.fatal = true; } } } diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index 6c54534f3c6..0de8011a806 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -852,6 +852,7 @@ export default class StreamController switch (data.details) { 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: case ErrorDetails.KEY_SYSTEM_NO_SESSION: diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index db5a8634525..a6af4619893 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -239,10 +239,20 @@ export default class TransmuxerInterface { state ); if (isPromise(transmuxResult)) { - transmuxResult.then((data) => { - this.handleTransmuxComplete(data); - }); + transmuxer.async = true; + transmuxResult + .then((data) => { + this.handleTransmuxComplete(data); + }) + .catch((error) => { + this.transmuxerError( + error, + chunkMeta, + 'transmuxer-interface push error' + ); + }); } else { + transmuxer.async = false; this.handleTransmuxComplete(transmuxResult as TransmuxerResult); } } @@ -252,16 +262,29 @@ export default class TransmuxerInterface { chunkMeta.transmuxing.start = self.performance.now(); const { transmuxer, worker } = this; if (worker) { + 1; worker.postMessage({ cmd: 'flush', chunkMeta, }); } else if (transmuxer) { - const transmuxResult = transmuxer.flush(chunkMeta); - if (isPromise(transmuxResult)) { - transmuxResult.then((data) => { - this.handleFlushResult(data, chunkMeta); - }); + let transmuxResult = transmuxer.flush(chunkMeta); + const asyncFlush = isPromise(transmuxResult); + if (asyncFlush || transmuxer.async) { + if (!isPromise(transmuxResult)) { + transmuxResult = Promise.resolve(transmuxResult); + } + transmuxResult + .then((data) => { + this.handleFlushResult(data, chunkMeta); + }) + .catch((error) => { + this.transmuxerError( + error, + chunkMeta, + 'transmuxer-interface flush error' + ); + }); } else { this.handleFlushResult( transmuxResult as Array, @@ -271,6 +294,25 @@ export default class TransmuxerInterface { } } + private transmuxerError( + error: Error, + chunkMeta: ChunkMetadata, + reason: string + ) { + if (!this.hls) { + return; + } + this.hls.trigger(Events.ERROR, { + type: ErrorTypes.MEDIA_ERROR, + details: ErrorDetails.FRAG_PARSING_ERROR, + chunkMeta, + fatal: false, + error, + err: error, + reason, + }); + } + private handleFlushResult( results: Array, chunkMeta: ChunkMetadata diff --git a/src/demux/transmuxer-worker.ts b/src/demux/transmuxer-worker.ts index 4c4d0e7d946..7f898e87efc 100644 --- a/src/demux/transmuxer-worker.ts +++ b/src/demux/transmuxer-worker.ts @@ -4,6 +4,7 @@ import { ILogFunction, enableLogs, logger } from '../utils/logger'; import { EventEmitter } from 'eventemitter3'; import type { RemuxedTrack, RemuxerResult } from '../types/remuxer'; import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer'; +import { ErrorDetails, ErrorTypes } from '../errors'; export default function TransmuxerWorker(self) { const observer = new EventEmitter(); @@ -59,21 +60,51 @@ export default function TransmuxerWorker(self) { data.state ); if (isPromise(transmuxResult)) { - transmuxResult.then((data) => { - emitTransmuxComplete(self, data); - }); + self.transmuxer.async = true; + transmuxResult + .then((data) => { + emitTransmuxComplete(self, data); + }) + .catch((error) => { + forwardMessage(Events.ERROR, { + type: ErrorTypes.MEDIA_ERROR, + details: ErrorDetails.FRAG_PARSING_ERROR, + chunkMeta: data.chunkMeta, + fatal: false, + error, + err: error, + reason: `transmuxer-worker push error`, + }); + }); } else { + self.transmuxer.async = false; emitTransmuxComplete(self, transmuxResult); } break; } case 'flush': { const id = data.chunkMeta; - const transmuxResult = self.transmuxer.flush(id); - if (isPromise(transmuxResult)) { - transmuxResult.then((results: Array) => { - handleFlushResult(self, results as Array, id); - }); + let transmuxResult = self.transmuxer.flush(id); + const asyncFlush = isPromise(transmuxResult); + if (asyncFlush || self.transmuxer.async) { + if (!isPromise(transmuxResult)) { + transmuxResult = Promise.resolve(transmuxResult); + } + transmuxResult + .then((results: Array) => { + handleFlushResult(self, results as Array, id); + }) + .catch((error) => { + forwardMessage(Events.ERROR, { + type: ErrorTypes.MEDIA_ERROR, + details: ErrorDetails.FRAG_PARSING_ERROR, + chunkMeta: data.chunkMeta, + fatal: false, + error, + err: error, + reason: `transmuxer-worker flush error`, + }); + }); } else { handleFlushResult( self, diff --git a/src/demux/transmuxer.ts b/src/demux/transmuxer.ts index d1443baf485..b98e98c7824 100644 --- a/src/demux/transmuxer.ts +++ b/src/demux/transmuxer.ts @@ -39,6 +39,7 @@ const muxConfig: MuxConfig[] = [ ]; export default class Transmuxer { + public async: boolean = false; private observer: HlsEventEmitter; private typeSupported: TypeSupported; private config: HlsConfig; diff --git a/src/types/events.ts b/src/types/events.ts index 6c1d0d70845..33860b55f74 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -216,6 +216,7 @@ export interface ErrorData { fatal: boolean; buffer?: number; bytes?: number; + chunkMeta?: ChunkMetadata; context?: PlaylistLoaderContext; error?: Error; event?: keyof HlsListeners | 'demuxerWorker';