Skip to content

Commit cc9e635

Browse files
committed
Implement penalty box for rendundant levels
1 parent eafcd6c commit cc9e635

File tree

2 files changed

+113
-29
lines changed

2 files changed

+113
-29
lines changed

src/controller/error-controller.ts

+108-28
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import {
77
shouldRetry,
88
} from '../utils/error-helper';
99
import { findFragmentByPTS } from './fragment-finders';
10-
import { HdcpLevel, HdcpLevels } from '../types/level';
10+
import { HdcpLevel, HdcpLevels, type Level } from '../types/level';
1111
import { logger } from '../utils/logger';
1212
import type Hls from '../hls';
1313
import type { RetryConfig } from '../config';
1414
import type { NetworkComponentAPI } from '../types/component-api';
1515
import type { ErrorData } from '../types/events';
1616
import type { Fragment } from '../loader/fragment';
17+
import type { LevelDetails } from '../hls';
18+
19+
const RENDITION_PENALTY_DURATION_MS = 300000;
1720

1821
export const enum NetworkErrorAction {
1922
DoNothing = 0,
@@ -41,10 +44,18 @@ export type IErrorAction = {
4144
resolved?: boolean;
4245
};
4346

47+
type PenalizedRendition = {
48+
lastErrorPerfMs: number;
49+
errors: ErrorData[];
50+
details?: LevelDetails;
51+
};
52+
53+
type PenalizedRenditions = { [key: number]: PenalizedRendition };
54+
4455
export default class ErrorController implements NetworkComponentAPI {
4556
private readonly hls: Hls;
4657
private playlistError: number = 0;
47-
private failoverError?: ErrorData;
58+
private penalizedRenditions: PenalizedRenditions = {};
4859
private log: (msg: any) => void;
4960
private warn: (msg: any) => void;
5061
private error: (msg: any) => void;
@@ -58,12 +69,19 @@ export default class ErrorController implements NetworkComponentAPI {
5869
}
5970

6071
private registerListeners() {
61-
this.hls.on(Events.ERROR, this.onError, this);
72+
const hls = this.hls;
73+
hls.on(Events.ERROR, this.onError, this);
74+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
6275
}
6376

6477
private unregisterListeners() {
65-
this.hls.off(Events.ERROR, this.onError, this);
66-
this.hls.off(Events.ERROR, this.onErrorOut, this);
78+
const hls = this.hls;
79+
if (!hls) {
80+
return;
81+
}
82+
hls.off(Events.ERROR, this.onError, this);
83+
hls.off(Events.ERROR, this.onErrorOut, this);
84+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
6785
}
6886

6987
destroy() {
@@ -84,6 +102,11 @@ export default class ErrorController implements NetworkComponentAPI {
84102
: this.hls.loadLevel;
85103
}
86104

105+
private onManifestLoading() {
106+
this.playlistError = 0;
107+
this.penalizedRenditions = {};
108+
}
109+
87110
private onError(event: Events.ERROR, data: ErrorData) {
88111
if (data.fatal) {
89112
return;
@@ -446,38 +469,95 @@ export default class ErrorController implements NetworkComponentAPI {
446469
}
447470

448471
private redundantFailover(data: ErrorData): boolean {
449-
const { hls, failoverError } = this;
472+
const { hls, penalizedRenditions } = this;
450473
const levelIndex: number =
451474
data.parent === PlaylistLevelType.MAIN
452475
? (data.level as number)
453476
: hls.loadLevel;
454477
const level = hls.levels[levelIndex];
455478
const redundantLevels = level.url.length;
456-
const newUrlId = (level.urlId + 1) % redundantLevels;
479+
this.penalizeRendition(level, data);
480+
for (let i = 1; i < redundantLevels; i++) {
481+
const newUrlId = (level.urlId + i) % redundantLevels;
482+
const penalizedRendition = penalizedRenditions[newUrlId];
483+
// Check if rendition is penalized and skip if it is a bad fit for failover
484+
if (
485+
!penalizedRendition ||
486+
checkExpired(penalizedRendition, data, penalizedRenditions[level.urlId])
487+
) {
488+
// delete penalizedRenditions[newUrlId];
489+
// Update the url id of all levels so that we stay on the same set of variants when level switching
490+
this.warn(
491+
`Switching to Redundant Stream ${newUrlId + 1}/${redundantLevels}: "${
492+
level.url[newUrlId]
493+
}" after ${data.details}`
494+
);
495+
this.playlistError = 0;
496+
hls.levels.forEach((lv) => {
497+
lv.urlId = newUrlId;
498+
});
499+
hls.nextLoadLevel = levelIndex;
500+
return true;
501+
}
502+
}
503+
return false;
504+
}
505+
506+
private penalizeRendition(level: Level, data: ErrorData) {
507+
const { penalizedRenditions } = this;
508+
const penalizedRendition = penalizedRenditions[level.urlId] || {
509+
lastErrorPerfMs: 0,
510+
errors: [],
511+
details: undefined,
512+
};
513+
penalizedRendition.lastErrorPerfMs = performance.now();
514+
penalizedRendition.errors.push(data);
515+
penalizedRendition.details = level.details;
516+
penalizedRenditions[level.urlId] = penalizedRendition;
517+
}
518+
}
519+
520+
function checkExpired(
521+
penalizedRendition: PenalizedRendition,
522+
data: ErrorData,
523+
currentPenaltyState: PenalizedRendition | undefined
524+
): boolean {
525+
// Expire penalty for switching back to rendition after RENDITION_PENALTY_DURATION_MS
526+
if (
527+
performance.now() - penalizedRendition.lastErrorPerfMs >
528+
RENDITION_PENALTY_DURATION_MS
529+
) {
530+
return true;
531+
}
532+
// Expire penalty on GAP tag error if rendition has no GAP at position (does not cover media tracks)
533+
const lastErrorDetails = penalizedRendition.details;
534+
if (data.details === ErrorDetails.FRAG_GAP && lastErrorDetails && data.frag) {
535+
const position = data.frag.start;
536+
const candidateFrag = findFragmentByPTS(
537+
null,
538+
lastErrorDetails.fragments,
539+
position
540+
);
541+
if (candidateFrag && !candidateFrag.gap) {
542+
return true;
543+
}
544+
}
545+
// Expire penalty if there are more errors in currentLevel than in penalizedRendition
546+
if (
547+
currentPenaltyState &&
548+
penalizedRendition.errors.length < currentPenaltyState.errors.length
549+
) {
550+
const lastCandidateError =
551+
penalizedRendition.errors[penalizedRendition.errors.length - 1];
457552
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))
553+
lastErrorDetails &&
554+
lastCandidateError.frag &&
555+
data.frag &&
556+
Math.abs(lastCandidateError.frag.start - data.frag.start) >
557+
lastErrorDetails.targetduration * 3
466558
) {
467-
this.failoverError = data;
468-
// Update the url id of all levels so that we stay on the same set of variants when level switching
469-
this.log(
470-
`Switching to Redundant Stream ${newUrlId + 1}/${redundantLevels}: "${
471-
level.url[newUrlId]
472-
}"`
473-
);
474-
this.playlistError = 0;
475-
hls.levels.forEach((lv) => {
476-
lv.urlId = newUrlId;
477-
});
478-
hls.nextLoadLevel = levelIndex;
479559
return true;
480560
}
481-
return false;
482561
}
562+
return false;
483563
}

src/controller/subtitle-stream-controller.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,11 @@ export class SubtitleStreamController
220220
this.levels = subtitleTracks.map(
221221
(mediaPlaylist) => new Level(mediaPlaylist)
222222
);
223-
this.fragmentTracker.removeAllFragments();
223+
this.fragmentTracker.removeFragmentsInRange(
224+
0,
225+
Number.POSITIVE_INFINITY,
226+
PlaylistLevelType.SUBTITLE
227+
);
224228
this.fragPrevious = null;
225229
this.levels.forEach((level: Level) => {
226230
this.tracksBuffered[level.id] = [];

0 commit comments

Comments
 (0)