Skip to content

Commit

Permalink
Emit and handle FRAG_PARSING_ERROR from transmuxers (#5018)
Browse files Browse the repository at this point in the history
* Emit and handle FRAG_PARSING_ERROR from transmuxers
Related to #5011
* Switch levels on Key and Fragment parsing errors or escalate to fatal error
  • Loading branch information
robwalch authored Nov 17, 2022
1 parent 6788118 commit 620eac8
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 19 deletions.
2 changes: 2 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ export interface ErrorData {
// (undocumented)
bytes?: number;
// (undocumented)
chunkMeta?: ChunkMetadata;
// (undocumented)
context?: PlaylistLoaderContext;
// (undocumented)
details: ErrorDetails;
Expand Down
1 change: 1 addition & 0 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 17 additions & 2 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
58 changes: 50 additions & 8 deletions src/demux/transmuxer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand All @@ -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<TransmuxerResult>,
Expand All @@ -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<TransmuxerResult>,
chunkMeta: ChunkMetadata
Expand Down
47 changes: 39 additions & 8 deletions src/demux/transmuxer-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<TransmuxerResult>) => {
handleFlushResult(self, results as Array<TransmuxerResult>, 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<TransmuxerResult>) => {
handleFlushResult(self, results as Array<TransmuxerResult>, 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,
Expand Down
1 change: 1 addition & 0 deletions src/demux/transmuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const muxConfig: MuxConfig[] = [
];

export default class Transmuxer {
public async: boolean = false;
private observer: HlsEventEmitter;
private typeSupported: TypeSupported;
private config: HlsConfig;
Expand Down
1 change: 1 addition & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export interface ErrorData {
fatal: boolean;
buffer?: number;
bytes?: number;
chunkMeta?: ChunkMetadata;
context?: PlaylistLoaderContext;
error?: Error;
event?: keyof HlsListeners | 'demuxerWorker';
Expand Down

0 comments on commit 620eac8

Please sign in to comment.