Skip to content

Commit 412413b

Browse files
committed
Emit and handle FRAG_PARSING_ERROR from transmuxers (#5018)
* Emit and handle FRAG_PARSING_ERROR from transmuxers Related to #5011 * Switch levels on Key and Fragment parsing errors or escalate to fatal error
1 parent 2afa6d3 commit 412413b

File tree

9 files changed

+118
-19
lines changed

9 files changed

+118
-19
lines changed

api-extractor/report/hls.js.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ export interface ErrorData {
374374
// (undocumented)
375375
bytes?: number;
376376
// (undocumented)
377+
chunkMeta?: ChunkMetadata;
378+
// (undocumented)
377379
context?: PlaylistLoaderContext;
378380
// (undocumented)
379381
details: ErrorDetails;

src/controller/audio-stream-controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ class AudioStreamController
627627
switch (data.details) {
628628
case ErrorDetails.FRAG_LOAD_ERROR:
629629
case ErrorDetails.FRAG_LOAD_TIMEOUT:
630+
case ErrorDetails.FRAG_PARSING_ERROR:
630631
case ErrorDetails.KEY_LOAD_ERROR:
631632
case ErrorDetails.KEY_LOAD_TIMEOUT:
632633
case ErrorDetails.KEY_SYSTEM_NO_SESSION:

src/controller/base-stream-controller.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,11 @@ export default class BaseStreamController
759759
protected _handleTransmuxerFlush(chunkMeta: ChunkMetadata) {
760760
const context = this.getCurrentContext(chunkMeta);
761761
if (!context || this.state !== State.PARSING) {
762-
if (!this.fragCurrent) {
762+
if (
763+
!this.fragCurrent &&
764+
this.state !== State.STOPPED &&
765+
this.state !== State.ERROR
766+
) {
763767
this.state = State.IDLE;
764768
}
765769
return;
@@ -1312,8 +1316,20 @@ export default class BaseStreamController
13121316
data: ErrorData
13131317
) {
13141318
if (data.fatal) {
1319+
this.stopLoad();
1320+
this.state = State.ERROR;
13151321
return;
13161322
}
1323+
const config = this.config;
1324+
if (data.chunkMeta) {
1325+
// Parsing Error: no retries
1326+
const context = this.getCurrentContext(data.chunkMeta);
1327+
if (context) {
1328+
data.frag = context.frag;
1329+
data.levelRetry = true;
1330+
this.fragLoadError = config.fragLoadingMaxRetry;
1331+
}
1332+
}
13171333
const frag = data.frag;
13181334
// Handle frag error related to caller's filterType
13191335
if (!frag || frag.type !== filterType) {
@@ -1327,7 +1343,6 @@ export default class BaseStreamController
13271343
frag.urlId === fragCurrent.urlId,
13281344
'Frag load error must match current frag to retry'
13291345
);
1330-
const config = this.config;
13311346
// keep retrying until the limit will be reached
13321347
if (this.fragLoadError + 1 <= config.fragLoadingMaxRetry) {
13331348
if (!this.loadedmetadata) {

src/controller/level-controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,13 +362,15 @@ export default class LevelController extends BasePlaylistController {
362362
}
363363
}
364364
break;
365+
case ErrorDetails.FRAG_PARSING_ERROR:
365366
case ErrorDetails.KEY_SYSTEM_NO_SESSION:
366367
case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:
367368
levelIndex =
368369
data.frag?.type === PlaylistLevelType.MAIN
369370
? data.frag.level
370371
: this.currentLevelIndex;
371-
levelError = true;
372+
// Do not retry level. Escalate to fatal if switching levels fails.
373+
data.levelRetry = false;
372374
break;
373375
case ErrorDetails.LEVEL_LOAD_ERROR:
374376
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
@@ -443,6 +445,9 @@ export default class LevelController extends BasePlaylistController {
443445
this.warn(`${errorDetails}: switch to ${nextLevel}`);
444446
errorEvent.levelRetry = true;
445447
this.hls.nextAutoLevel = nextLevel;
448+
} else if (errorEvent.levelRetry === false) {
449+
// No levels to switch to and no more retries
450+
errorEvent.fatal = true;
446451
}
447452
}
448453
}

src/controller/stream-controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,7 @@ export default class StreamController
852852
switch (data.details) {
853853
case ErrorDetails.FRAG_LOAD_ERROR:
854854
case ErrorDetails.FRAG_LOAD_TIMEOUT:
855+
case ErrorDetails.FRAG_PARSING_ERROR:
855856
case ErrorDetails.KEY_LOAD_ERROR:
856857
case ErrorDetails.KEY_LOAD_TIMEOUT:
857858
case ErrorDetails.KEY_SYSTEM_NO_SESSION:

src/demux/transmuxer-interface.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,20 @@ export default class TransmuxerInterface {
239239
state
240240
);
241241
if (isPromise(transmuxResult)) {
242-
transmuxResult.then((data) => {
243-
this.handleTransmuxComplete(data);
244-
});
242+
transmuxer.async = true;
243+
transmuxResult
244+
.then((data) => {
245+
this.handleTransmuxComplete(data);
246+
})
247+
.catch((error) => {
248+
this.transmuxerError(
249+
error,
250+
chunkMeta,
251+
'transmuxer-interface push error'
252+
);
253+
});
245254
} else {
255+
transmuxer.async = false;
246256
this.handleTransmuxComplete(transmuxResult as TransmuxerResult);
247257
}
248258
}
@@ -252,16 +262,29 @@ export default class TransmuxerInterface {
252262
chunkMeta.transmuxing.start = self.performance.now();
253263
const { transmuxer, worker } = this;
254264
if (worker) {
265+
1;
255266
worker.postMessage({
256267
cmd: 'flush',
257268
chunkMeta,
258269
});
259270
} else if (transmuxer) {
260-
const transmuxResult = transmuxer.flush(chunkMeta);
261-
if (isPromise(transmuxResult)) {
262-
transmuxResult.then((data) => {
263-
this.handleFlushResult(data, chunkMeta);
264-
});
271+
let transmuxResult = transmuxer.flush(chunkMeta);
272+
const asyncFlush = isPromise(transmuxResult);
273+
if (asyncFlush || transmuxer.async) {
274+
if (!isPromise(transmuxResult)) {
275+
transmuxResult = Promise.resolve(transmuxResult);
276+
}
277+
transmuxResult
278+
.then((data) => {
279+
this.handleFlushResult(data, chunkMeta);
280+
})
281+
.catch((error) => {
282+
this.transmuxerError(
283+
error,
284+
chunkMeta,
285+
'transmuxer-interface flush error'
286+
);
287+
});
265288
} else {
266289
this.handleFlushResult(
267290
transmuxResult as Array<TransmuxerResult>,
@@ -271,6 +294,25 @@ export default class TransmuxerInterface {
271294
}
272295
}
273296

297+
private transmuxerError(
298+
error: Error,
299+
chunkMeta: ChunkMetadata,
300+
reason: string
301+
) {
302+
if (!this.hls) {
303+
return;
304+
}
305+
this.hls.trigger(Events.ERROR, {
306+
type: ErrorTypes.MEDIA_ERROR,
307+
details: ErrorDetails.FRAG_PARSING_ERROR,
308+
chunkMeta,
309+
fatal: false,
310+
error,
311+
err: error,
312+
reason,
313+
});
314+
}
315+
274316
private handleFlushResult(
275317
results: Array<TransmuxerResult>,
276318
chunkMeta: ChunkMetadata

src/demux/transmuxer-worker.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ILogFunction, enableLogs, logger } from '../utils/logger';
44
import { EventEmitter } from 'eventemitter3';
55
import type { RemuxedTrack, RemuxerResult } from '../types/remuxer';
66
import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
7+
import { ErrorDetails, ErrorTypes } from '../errors';
78

89
export default function TransmuxerWorker(self) {
910
const observer = new EventEmitter();
@@ -59,21 +60,51 @@ export default function TransmuxerWorker(self) {
5960
data.state
6061
);
6162
if (isPromise(transmuxResult)) {
62-
transmuxResult.then((data) => {
63-
emitTransmuxComplete(self, data);
64-
});
63+
self.transmuxer.async = true;
64+
transmuxResult
65+
.then((data) => {
66+
emitTransmuxComplete(self, data);
67+
})
68+
.catch((error) => {
69+
forwardMessage(Events.ERROR, {
70+
type: ErrorTypes.MEDIA_ERROR,
71+
details: ErrorDetails.FRAG_PARSING_ERROR,
72+
chunkMeta: data.chunkMeta,
73+
fatal: false,
74+
error,
75+
err: error,
76+
reason: `transmuxer-worker push error`,
77+
});
78+
});
6579
} else {
80+
self.transmuxer.async = false;
6681
emitTransmuxComplete(self, transmuxResult);
6782
}
6883
break;
6984
}
7085
case 'flush': {
7186
const id = data.chunkMeta;
72-
const transmuxResult = self.transmuxer.flush(id);
73-
if (isPromise(transmuxResult)) {
74-
transmuxResult.then((results: Array<TransmuxerResult>) => {
75-
handleFlushResult(self, results as Array<TransmuxerResult>, id);
76-
});
87+
let transmuxResult = self.transmuxer.flush(id);
88+
const asyncFlush = isPromise(transmuxResult);
89+
if (asyncFlush || self.transmuxer.async) {
90+
if (!isPromise(transmuxResult)) {
91+
transmuxResult = Promise.resolve(transmuxResult);
92+
}
93+
transmuxResult
94+
.then((results: Array<TransmuxerResult>) => {
95+
handleFlushResult(self, results as Array<TransmuxerResult>, id);
96+
})
97+
.catch((error) => {
98+
forwardMessage(Events.ERROR, {
99+
type: ErrorTypes.MEDIA_ERROR,
100+
details: ErrorDetails.FRAG_PARSING_ERROR,
101+
chunkMeta: data.chunkMeta,
102+
fatal: false,
103+
error,
104+
err: error,
105+
reason: `transmuxer-worker flush error`,
106+
});
107+
});
77108
} else {
78109
handleFlushResult(
79110
self,

src/demux/transmuxer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const muxConfig: MuxConfig[] = [
3939
];
4040

4141
export default class Transmuxer {
42+
public async: boolean = false;
4243
private observer: HlsEventEmitter;
4344
private typeSupported: TypeSupported;
4445
private config: HlsConfig;

src/types/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export interface ErrorData {
216216
fatal: boolean;
217217
buffer?: number;
218218
bytes?: number;
219+
chunkMeta?: ChunkMetadata;
219220
context?: PlaylistLoaderContext;
220221
error?: Error;
221222
event?: keyof HlsListeners | 'demuxerWorker';

0 commit comments

Comments
 (0)