Skip to content

Commit e85d938

Browse files
committed
Fix GAP tag handling with Redunant Streams
1 parent fecd4bf commit e85d938

File tree

7 files changed

+71
-28
lines changed

7 files changed

+71
-28
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -1949,6 +1949,8 @@ export class Level {
19491949
// (undocumented)
19501950
readonly audioCodec: string | undefined;
19511951
// (undocumented)
1952+
get audioGroupId(): string | undefined;
1953+
// (undocumented)
19521954
audioGroupIds?: (string | undefined)[];
19531955
// (undocumented)
19541956
readonly bitrate: number;
@@ -1978,6 +1980,8 @@ export class Level {
19781980
// (undocumented)
19791981
realBitrate: number;
19801982
// (undocumented)
1983+
get textGroupId(): string | undefined;
1984+
// (undocumented)
19811985
textGroupIds?: (string | undefined)[];
19821986
// (undocumented)
19831987
readonly unknownCodecs: string[] | undefined;

src/controller/base-stream-controller.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1425,7 +1425,8 @@ export default class BaseStreamController
14251425
);
14261426
return;
14271427
}
1428-
if (data.details === ErrorDetails.FRAG_GAP) {
1428+
const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP;
1429+
if (gapTagEncountered) {
14291430
this.fragmentTracker.fragBuffered(frag, true);
14301431
}
14311432
// keep retrying until the limit will be reached
@@ -1455,7 +1456,9 @@ export default class BaseStreamController
14551456
this.resetFragmentErrors(filterType);
14561457
if (retryCount < retryConfig.maxNumRetry) {
14571458
// Network retry is skipped when level switch is preferred
1458-
errorAction.resolved = true;
1459+
if (!gapTagEncountered) {
1460+
errorAction.resolved = true;
1461+
}
14591462
} else {
14601463
logger.warn(
14611464
`${data.details} reached or exceeded max retry (${retryCount})`

src/controller/error-controller.ts

+49-21
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type IErrorAction = {
4444
export default class ErrorController implements NetworkComponentAPI {
4545
private readonly hls: Hls;
4646
private playlistError: number = 0;
47+
private failoverError?: ErrorData;
4748
private log: (msg: any) => void;
4849
private warn: (msg: any) => void;
4950
private error: (msg: any) => void;
@@ -146,11 +147,9 @@ export default class ErrorController implements NetworkComponentAPI {
146147
if (
147148
level &&
148149
((context.type === PlaylistContextType.AUDIO_TRACK &&
149-
level.audioGroupIds &&
150-
context.groupId === level.audioGroupIds[level.urlId]) ||
150+
context.groupId === level.audioGroupId) ||
151151
(context.type === PlaylistContextType.SUBTITLE_TRACK &&
152-
level.textGroupIds &&
153-
context.groupId === level.textGroupIds[level.urlId]))
152+
context.groupId === level.textGroupId))
154153
) {
155154
// Perform Pathway switch or Redundant failover if possible for fastest recovery
156155
// otherwise allow playlist retry count to reach max error retries
@@ -310,23 +309,23 @@ export default class ErrorController implements NetworkComponentAPI {
310309
if (data.details !== ErrorDetails.FRAG_GAP) {
311310
level.loadError++;
312311
}
313-
const redundantLevels = level.url.length;
314-
// Try redundant fail-over until level.loadError reaches redundantLevels
315-
if (redundantLevels > 1 && level.loadError < redundantLevels) {
316-
data.levelRetry = true;
317-
} else if (hls.autoLevelEnabled) {
312+
if (hls.autoLevelEnabled) {
318313
// Search for next level to retry
319314
let nextLevel = -1;
320315
const levels = hls.levels;
316+
const fragErrorType = data.frag?.type;
317+
const { type: playlistErrorType, groupId: playlistErrorGroupId } =
318+
data.context ?? {};
321319
for (let i = levels.length; i--; ) {
322320
const candidate = (i + hls.loadLevel) % levels.length;
323321
if (
324322
candidate !== hls.loadLevel &&
325323
levels[candidate].loadError === 0
326324
) {
325+
const levelCandidate = levels[candidate];
327326
// Skip level switch if GAP tag is found in next level at same position
328327
if (data.details === ErrorDetails.FRAG_GAP && data.frag) {
329-
const levelDetails = hls.levels[candidate].details;
328+
const levelDetails = levels[candidate].details;
330329
if (levelDetails) {
331330
const fragCandidate = findFragmentByPTS(
332331
data.frag,
@@ -337,6 +336,22 @@ export default class ErrorController implements NetworkComponentAPI {
337336
continue;
338337
}
339338
}
339+
} else if (
340+
(playlistErrorType === PlaylistContextType.AUDIO_TRACK &&
341+
playlistErrorGroupId === levelCandidate.audioGroupId) ||
342+
(playlistErrorType === PlaylistContextType.SUBTITLE_TRACK &&
343+
playlistErrorGroupId === levelCandidate.textGroupId)
344+
) {
345+
// For audio/subs playlist errors find another group ID or fallthrough to redundant fail-over
346+
continue;
347+
} else if (
348+
(fragErrorType === PlaylistLevelType.AUDIO &&
349+
level.audioGroupId === levelCandidate.audioGroupId) ||
350+
(fragErrorType === PlaylistLevelType.SUBTITLE &&
351+
level.textGroupId === levelCandidate.textGroupId)
352+
) {
353+
// For audio/subs frag errors find another group ID or fallthrough to redundant fail-over
354+
continue;
340355
}
341356
nextLevel = candidate;
342357
break;
@@ -366,7 +381,10 @@ export default class ErrorController implements NetworkComponentAPI {
366381
break;
367382
case NetworkErrorAction.SendAlternateToPenaltyBox:
368383
this.sendAlternateToPenaltyBox(data);
369-
if (!data.errorAction.resolved) {
384+
if (
385+
!data.errorAction.resolved &&
386+
data.details !== ErrorDetails.FRAG_GAP
387+
) {
370388
data.fatal = true;
371389
}
372390
break;
@@ -395,13 +413,9 @@ export default class ErrorController implements NetworkComponentAPI {
395413
break;
396414
case ErrorActionFlags.MoveAllAlternatesMatchingHost:
397415
{
398-
const levelIndex =
399-
data.parent === PlaylistLevelType.MAIN
400-
? (data.level as number)
401-
: hls.loadLevel;
402-
// Handle Redundant Levels here. Patway switching is handled by content-steering-controller
416+
// Handle Redundant Levels here. Pathway switching is handled by content-steering-controller
403417
if (!errorAction.resolved) {
404-
errorAction.resolved = this.redundantFailover(levelIndex);
418+
errorAction.resolved = this.redundantFailover(data);
405419
}
406420
}
407421
break;
@@ -431,13 +445,27 @@ export default class ErrorController implements NetworkComponentAPI {
431445
}
432446
}
433447

434-
private redundantFailover(levelIndex: number): boolean {
435-
const hls = this.hls;
448+
private redundantFailover(data: ErrorData): boolean {
449+
const { hls, failoverError } = this;
450+
const levelIndex: number =
451+
data.parent === PlaylistLevelType.MAIN
452+
? (data.level as number)
453+
: hls.loadLevel;
436454
const level = hls.levels[levelIndex];
437455
const redundantLevels = level.url.length;
438-
if (redundantLevels > 1) {
456+
const newUrlId = (level.urlId + 1) % redundantLevels;
457+
if (
458+
redundantLevels > 1 &&
459+
// FIXME: Don't loop back to first redundant renditions unless the last gap is more than 60 seconds ago
460+
// TODO: We throw out information about the other redundant renditions but should keep track of gap ranges to avoid looping back to the same problem
461+
(newUrlId !== 0 ||
462+
!failoverError ||
463+
(failoverError.frag &&
464+
data.frag &&
465+
Math.abs(data.frag.start - failoverError.frag.start) > 60))
466+
) {
467+
this.failoverError = data;
439468
// Update the url id of all levels so that we stay on the same set of variants when level switching
440-
const newUrlId = (level.urlId + 1) % redundantLevels;
441469
this.log(
442470
`Switching to Redundant Stream ${newUrlId + 1}/${redundantLevels}: "${
443471
level.url[newUrlId]

src/controller/level-controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ export default class LevelController extends BasePlaylistController {
479479
const audioGroupId = this.hls.audioTracks[data.id].groupId;
480480
if (
481481
currentLevel.audioGroupIds &&
482-
currentLevel.audioGroupIds[currentLevel.urlId] !== audioGroupId
482+
currentLevel.audioGroupId !== audioGroupId
483483
) {
484484
let urlId = -1;
485485
for (let i = 0; i < currentLevel.audioGroupIds.length; i++) {

src/types/level.ts

+8
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ export class Level {
155155
}
156156
}
157157

158+
get audioGroupId(): string | undefined {
159+
return this.audioGroupIds?.[this.urlId];
160+
}
161+
162+
get textGroupId(): string | undefined {
163+
return this.textGroupIds?.[this.urlId];
164+
}
165+
158166
addFallback(data: LevelParsed) {
159167
this.url.push(data.url);
160168
this._attrs.push(data.attrs);

tests/unit/controller/audio-track-controller.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ describe('AudioTrackController', function () {
208208
});
209209

210210
const newLevelInfo = hls.levels[0];
211-
const newGroupId = newLevelInfo.audioGroupIds?.[newLevelInfo.urlId];
211+
const newGroupId = newLevelInfo.audioGroupId;
212212

213213
audioTrackController.tracks = tracks;
214214
// Update the level to set audioGroupId
@@ -325,7 +325,7 @@ describe('AudioTrackController', function () {
325325
};
326326

327327
const newLevelInfo = hls.levels[levelLoadedEvent.level];
328-
const newGroupId = newLevelInfo.audioGroupIds?.[newLevelInfo.urlId];
328+
const newGroupId = newLevelInfo.audioGroupId;
329329

330330
audioTrackController.tracks = tracks;
331331
audioTrackController.onLevelLoading(Events.LEVEL_LOADING, {

tests/unit/controller/error-controller.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ segment.mp4
737737
expect(
738738
errors.length,
739739
'fragment errors after yeilding to second error event'
740-
).to.equal(4);
740+
).to.equal(6);
741741
expect(hls.levels[0].uri).to.equal('http://www.baz.com/tier6.m3u8');
742742
return new Promise((resolve, reject) => {
743743
hls.on(Events.FRAG_LOADED, (event, data) => {
@@ -847,7 +847,7 @@ segment.mp4
847847
expect(
848848
errors.length,
849849
'fragment errors after yeilding to second error event'
850-
).to.equal(4);
850+
).to.equal(6);
851851
expect(hls.levels[0].uri).to.equal('http://www.baz.com/tier6.m3u8');
852852
return new Promise((resolve, reject) => {
853853
hls.on(Events.FRAG_LOADED, (event, data) => {

0 commit comments

Comments
 (0)