Skip to content

Commit

Permalink
Force auto level on emergency switch down (#6082)
Browse files Browse the repository at this point in the history
Update estimates on frag load timeout
Do not abort request in _abandonRulesCheck
Remove two segment forward buffer length limit in _abandonRulesCheck
Reset estimate when candidate bitrate is lower than adjusted estimate
Resolves #6079
  • Loading branch information
robwalch committed Jan 9, 2024
1 parent 0b0c98a commit 4265602
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 23 deletions.
72 changes: 51 additions & 21 deletions src/controller/abr-controller.ts
Expand Up @@ -41,7 +41,6 @@ class AbrController implements AbrComponentAPI {
private audioTracksByGroup: AudioTracksByGroup | null = null;
private codecTiers: Record<string, CodecSetTier> | null = null;
private timer: number = -1;
private onCheck: Function = this._abandonRulesCheck.bind(this);
private fragCurrent: Fragment | null = null;
private partCurrent: Part | null = null;
private bitrateTestDelay: number = 0;
Expand Down Expand Up @@ -105,7 +104,7 @@ class AbrController implements AbrComponentAPI {
this.unregisterListeners();
this.clearTimer();
// @ts-ignore
this.hls = this.onCheck = null;
this.hls = this._abandonRulesCheck = null;
this.fragCurrent = this.partCurrent = null;
}

Expand Down Expand Up @@ -146,7 +145,7 @@ class AbrController implements AbrComponentAPI {
this.partCurrent = data.part ?? null;
}
this.clearTimer();
this.timer = self.setInterval(this.onCheck, 100);
this.timer = self.setInterval(this._abandonRulesCheck, 100);
}

protected onLevelSwitching(
Expand All @@ -166,6 +165,35 @@ class AbrController implements AbrComponentAPI {
// Reset last loaded level so that a new selection can be made after calling recoverMediaError
this.lastLoadedFragLevel = -1;
this.firstSelection = -1;
break;
case ErrorDetails.FRAG_LOAD_TIMEOUT: {
const frag = data.frag;
const { fragCurrent, partCurrent: part } = this;
if (
frag &&
fragCurrent &&
frag.sn === fragCurrent.sn &&
frag.level === fragCurrent.level
) {
const now = performance.now();
const stats: LoaderStats = part ? part.stats : frag.stats;
const timeLoading = now - stats.loading.start;
const ttfb = stats.loading.first
? stats.loading.first - stats.loading.start
: -1;
const loadedFirstByte = stats.loaded && ttfb > -1;
if (loadedFirstByte) {
const ttfbEstimate = this.bwEstimator.getEstimateTTFB();
this.bwEstimator.sample(
timeLoading - Math.min(ttfbEstimate, ttfb),
stats.loaded,
);
} else {
this.bwEstimator.sampleTTFB(timeLoading);
}
}
break;
}
}
}

Expand Down Expand Up @@ -198,7 +226,7 @@ class AbrController implements AbrComponentAPI {
This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load
quickly enough to prevent underbuffering
*/
private _abandonRulesCheck() {
private _abandonRulesCheck = () => {
const { fragCurrent: frag, partCurrent: part, hls } = this;
const { autoLevelEnabled, media } = hls;
if (!frag || !media) {
Expand Down Expand Up @@ -249,11 +277,6 @@ class AbrController implements AbrComponentAPI {

// bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer
const bufferStarvationDelay = bufferInfo.len / playbackRate;
// Only downswitch if less than 2 fragment lengths are buffered
if (bufferStarvationDelay >= (2 * duration) / playbackRate) {
return;
}

const ttfb = stats.loading.first
? stats.loading.first - stats.loading.start
: -1;
Expand Down Expand Up @@ -312,7 +335,7 @@ class AbrController implements AbrComponentAPI {
if (fragLevelNextLoadedDelay > duration * 10) {
return;
}
hls.nextLoadLevel = nextLoadLevel;
hls.nextLoadLevel = hls.nextAutoLevel = nextLoadLevel;
if (loadedFirstByte) {
// If there has been loading progress, sample bandwidth using loading time offset by minimum TTFB time
this.bwEstimator.sample(
Expand All @@ -323,6 +346,13 @@ class AbrController implements AbrComponentAPI {
// If there has been no loading progress, sample TTFB
this.bwEstimator.sampleTTFB(timeLoading);
}
const nextLoadLevelBitrate = levels[nextLoadLevel].bitrate;
if (
this.getBwEstimate() * this.hls.config.abrBandWidthUpFactor >
nextLoadLevelBitrate
) {
this.resetEstimator(nextLoadLevelBitrate);
}

this.clearTimer();
logger.warn(`[abr] Fragment ${frag.sn}${
Expand All @@ -333,18 +363,14 @@ class AbrController implements AbrComponentAPI {
Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(
3,
)} s
TTFB estimate: ${ttfb}
TTFB estimate: ${ttfb | 0} ms
Current BW estimate: ${
Number.isFinite(bwEstimate) ? (bwEstimate / 1024).toFixed(3) : 'Unknown'
} Kb/s
New BW estimate: ${(this.getBwEstimate() / 1024).toFixed(3)} Kb/s
Aborting and switching to level ${nextLoadLevel}`);
if (frag.loader) {
this.fragCurrent = this.partCurrent = null;
frag.abortRequests();
}
Number.isFinite(bwEstimate) ? bwEstimate | 0 : 'Unknown'
} bps
New BW estimate: ${this.getBwEstimate() | 0} bps
Switching to level ${nextLoadLevel} @ ${nextLoadLevelBitrate | 0} bps`);
hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats });
}
};

protected onFragLoaded(
event: Events.FRAG_LOADED,
Expand Down Expand Up @@ -799,7 +825,11 @@ class AbrController implements AbrComponentAPI {
(live && !this.bitrateTestDelay) ||
fetchDuration < maxFetchDuration);
if (canSwitchWithinTolerance) {
if (i !== loadLevel) {
const forcedAutoLevel = this.forcedAutoLevel;
if (
i !== loadLevel &&
(forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)
) {
if (levelsSkipped.length) {
logger.trace(
`[abr] Skipped level(s) ${levelsSkipped.join(
Expand Down
5 changes: 3 additions & 2 deletions src/utils/level-helper.ts
Expand Up @@ -202,8 +202,9 @@ export function mergeDetails(
: newDetails.fragments;
fragmentsToCheck.forEach((frag) => {
if (
!frag.initSegment ||
frag.initSegment.relurl === currentInitSegment?.relurl
frag &&
(!frag.initSegment ||
frag.initSegment.relurl === currentInitSegment?.relurl)
) {
frag.initSegment = currentInitSegment;
}
Expand Down

0 comments on commit 4265602

Please sign in to comment.