Skip to content

Commit

Permalink
Fix estimates and assumptions in abandonRulesCheck that occur before …
Browse files Browse the repository at this point in the history
…first byte loaded (#4917)

Subset of #4825
  • Loading branch information
robwalch committed Sep 21, 2022
1 parent c7b01b7 commit 98d4ab3
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 39 deletions.
4 changes: 4 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,10 @@ class Hls implements HlsEventEmitter {
get lowLatencyMode(): boolean;
// Warning: (ae-setter-with-docs) The doc comment for the property "lowLatencyMode" must appear on the getter, not the setter.
set lowLatencyMode(mode: boolean);
// Warning: (ae-forgotten-export) The symbol "BufferInfo" needs to be exported by the entry point hls.d.ts
//
// (undocumented)
get mainForwardBufferInfo(): BufferInfo | null;
get manualLevel(): number;
get maxAutoLevel(): number;
get maxLatency(): number;
Expand Down
63 changes: 33 additions & 30 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,12 @@ class AbrController implements ComponentAPI {

const stats: LoaderStats = part ? part.stats : frag.stats;
const duration = part ? part.duration : frag.duration;
// If loading has been aborted and not in lowLatencyMode, stop timer and return
if (stats.aborted) {
logger.warn('frag loader destroy or aborted, disarm abandonRules');
// If frag loading is aborted, complete, or from lowest level, stop timer and return
if (
stats.aborted ||
(stats.loaded && stats.loaded === stats.total) ||
frag.level === 0
) {
this.clearTimer();
// reset forced auto level value so that next level will be selected
this._nextAutoLevel = -1;
Expand All @@ -121,39 +124,37 @@ class AbrController implements ComponentAPI {
return;
}

const bufferInfo = hls.mainForwardBufferInfo;
if (bufferInfo === null) {
return;
}

const requestDelay = performance.now() - stats.loading.start;
const playbackRate = Math.abs(media.playbackRate);
// In order to work with a stable bandwidth, only begin monitoring bandwidth after half of the fragment has been loaded
if (requestDelay <= (500 * duration) / playbackRate) {
return;
}

const loadedFirstByte = stats.loaded && stats.loading.first;
const bwEstimate: number = this.bwEstimator.getEstimate();
const { levels, minAutoLevel } = hls;
const level = levels[frag.level];
const expectedLen =
stats.total ||
Math.max(stats.loaded, Math.round((duration * level.maxBitrate) / 8));
const loadRate = Math.max(
1,
stats.bwEstimate
? stats.bwEstimate / 8
: (stats.loaded * 1000) / requestDelay
);
// fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the entire fragment
const fragLoadedDelay = (expectedLen - stats.loaded) / loadRate;
const loadRate = loadedFirstByte ? (stats.loaded * 1000) / requestDelay : 0;

// fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the remainder of the fragment
const fragLoadedDelay = loadRate
? (expectedLen - stats.loaded) / loadRate
: (expectedLen * 8) / bwEstimate;

const pos = media.currentTime;
// bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer
const bufferStarvationDelay =
(BufferHelper.bufferInfo(media, pos, config.maxBufferHole).end - pos) /
playbackRate;
const bufferStarvationDelay = bufferInfo.len / playbackRate;

// Attempt an emergency downswitch only if less than 2 fragment lengths are buffered, and the time to finish loading
// the current fragment is greater than the amount of buffer we have left
if (
bufferStarvationDelay >= (2 * duration) / playbackRate ||
fragLoadedDelay <= bufferStarvationDelay
) {
// Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left
if (fragLoadedDelay <= bufferStarvationDelay) {
return;
}

Expand All @@ -169,8 +170,9 @@ class AbrController implements ComponentAPI {
// 0.8 : consider only 80% of current bw to be conservative
// 8 = bits per byte (bps/Bps)
const levelNextBitrate = levels[nextLoadLevel].maxBitrate;
fragLevelNextLoadedDelay =
(duration * levelNextBitrate) / (8 * 0.8 * loadRate);
fragLevelNextLoadedDelay = loadRate
? (duration * levelNextBitrate) / (8 * 0.8 * loadRate)
: (duration * levelNextBitrate) / bwEstimate;

if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
break;
Expand All @@ -181,7 +183,6 @@ class AbrController implements ComponentAPI {
if (fragLevelNextLoadedDelay >= fragLoadedDelay) {
return;
}
const bwEstimate: number = this.bwEstimator.getEstimate();
logger.warn(`Fragment ${frag.sn}${
part ? ' part ' + part.index : ''
} of level ${
Expand All @@ -196,7 +197,10 @@ class AbrController implements ComponentAPI {
)} s
Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s`);
hls.nextLoadLevel = nextLoadLevel;
this.bwEstimator.sample(requestDelay, stats.loaded);
if (loadedFirstByte) {
// If there has been loading progress, sample bandwidth
this.bwEstimator.sample(requestDelay, stats.loaded);
}
this.clearTimer();
if (frag.loader) {
this.fragCurrent = this.partCurrent = null;
Expand Down Expand Up @@ -329,11 +333,9 @@ class AbrController implements ComponentAPI {
? this.bwEstimator.getEstimate()
: config.abrEwmaDefaultEstimate;
// bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
const bufferInfo = hls.mainForwardBufferInfo;
const bufferStarvationDelay =
(BufferHelper.bufferInfo(media as Bufferable, pos, config.maxBufferHole)
.end -
pos) /
playbackRate;
(bufferInfo ? bufferInfo.len : 0) / playbackRate;

// First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
let bestLevel = this.findBestLevel(
Expand Down Expand Up @@ -461,7 +463,8 @@ class AbrController implements ComponentAPI {
// fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
// we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1
(!fetchDuration ||
(fetchDuration === 0 ||
!Number.isFinite(fetchDuration) ||
(live && !this.bitrateTestDelay) ||
fetchDuration < maxFetchDuration)
) {
Expand Down
9 changes: 2 additions & 7 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import TaskLoop from '../task-loop';
import { FragmentState } from './fragment-tracker';
import { Bufferable, BufferHelper } from '../utils/buffer-helper';
import { Bufferable, BufferHelper, BufferInfo } from '../utils/buffer-helper';
import { logger } from '../utils/logger';
import { Events } from '../events';
import { ErrorDetails } from '../errors';
Expand Down Expand Up @@ -778,12 +778,7 @@ export default class BaseStreamController
protected getFwdBufferInfo(
bufferable: Bufferable | null,
type: PlaylistLevelType
): {
len: number;
start: number;
end: number;
nextStart?: number;
} | null {
): BufferInfo | null {
const { config } = this;
const pos = this.getLoadPosition();
if (!Number.isFinite(pos)) {
Expand Down
4 changes: 2 additions & 2 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BaseStreamController, { State } from './base-stream-controller';
import { changeTypeSupported } from '../is-supported';
import type { NetworkComponentAPI } from '../types/component-api';
import { Events } from '../events';
import { BufferHelper } from '../utils/buffer-helper';
import { BufferHelper, BufferInfo } from '../utils/buffer-helper';
import type { FragmentTracker } from './fragment-tracker';
import { FragmentState } from './fragment-tracker';
import type { Level } from '../types/level';
Expand Down Expand Up @@ -1276,7 +1276,7 @@ export default class StreamController
this.tick();
}

private getMainFwdBufferInfo() {
public getMainFwdBufferInfo(): BufferInfo | null {
return this.getFwdBufferInfo(
this.mediaBuffer ? this.mediaBuffer : this.media,
PlaylistLevelType.MAIN
Expand Down
5 changes: 5 additions & 0 deletions src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { MediaPlaylist } from './types/media-playlist';
import type { HlsConfig } from './config';
import type { Level } from './types/level';
import type { Fragment } from './loader/fragment';
import { BufferInfo } from './utils/buffer-helper';

/**
* @module Hls
Expand Down Expand Up @@ -682,6 +683,10 @@ export default class Hls implements HlsEventEmitter {
return this.streamController.currentProgramDateTime;
}

public get mainForwardBufferInfo(): BufferInfo | null {
return this.streamController.getMainFwdBufferInfo();
}

/**
* @type {AudioTrack[]}
*/
Expand Down

0 comments on commit 98d4ab3

Please sign in to comment.