Skip to content

Commit

Permalink
Base initial bandwidth estimate on first level's bitrate (#5649)
Browse files Browse the repository at this point in the history
Resolves #2754
  • Loading branch information
robwalch authored Jul 13, 2023
1 parent 4afdbe0 commit bc60dbf
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 47 deletions.
8 changes: 7 additions & 1 deletion api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface AbrComponentAPI extends ComponentAPI {
export class AbrController implements AbrComponentAPI {
constructor(hls: Hls);
// (undocumented)
readonly bwEstimator: EwmaBandWidthEstimator;
bwEstimator: EwmaBandWidthEstimator;
// (undocumented)
clearTimer(): void;
// (undocumented)
Expand All @@ -45,6 +45,8 @@ export class AbrController implements AbrComponentAPI {
// (undocumented)
protected registerListeners(): void;
// (undocumented)
resetEstimator(abrEwmaDefaultEstimate?: number): void;
// (undocumented)
protected unregisterListeners(): void;
}

Expand All @@ -57,6 +59,7 @@ export type ABRControllerConfig = {
abrEwmaFastVoD: number;
abrEwmaSlowVoD: number;
abrEwmaDefaultEstimate: number;
abrEwmaDefaultEstimateMax: number;
abrBandWidthFactor: number;
abrBandWidthUpFactor: number;
abrMaxWithRealBitrate: boolean;
Expand Down Expand Up @@ -1523,6 +1526,7 @@ class Hls implements HlsEventEmitter {
set autoLevelCapping(newLevel: number);
get autoLevelEnabled(): boolean;
get bandwidthEstimate(): number;
set bandwidthEstimate(abrEwmaDefaultEstimate: number);
get capLevelToPlayerSize(): boolean;
// Warning: (ae-setter-with-docs) The doc comment for the property "capLevelToPlayerSize" must appear on the getter, not the setter.
set capLevelToPlayerSize(shouldStartCapping: boolean);
Expand Down Expand Up @@ -1982,6 +1986,8 @@ export class Level {
// (undocumented)
audioGroupIds?: (string | undefined)[];
// (undocumented)
get averageBitrate(): number;
// (undocumented)
readonly bitrate: number;
// (undocumented)
readonly codecSet: string;
Expand Down
10 changes: 10 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
- [`abrEwmaFastVoD`](#abrewmafastvod)
- [`abrEwmaSlowVoD`](#abrewmaslowvod)
- [`abrEwmaDefaultEstimate`](#abrewmadefaultestimate)
- [`abrEwmaDefaultEstimateMax`](#abrewmadefaultestimatemax)
- [`abrBandWidthFactor`](#abrbandwidthfactor)
- [`abrBandWidthUpFactor`](#abrbandwidthupfactor)
- [`abrMaxWithRealBitrate`](#abrmaxwithrealbitrate)
Expand Down Expand Up @@ -410,6 +411,7 @@ var config = {
abrEwmaFastVoD: 3.0,
abrEwmaSlowVoD: 9.0,
abrEwmaDefaultEstimate: 500000,
abrEwmaDefaultEstimateMax: 5000000,
abrBandWidthFactor: 0.95,
abrBandWidthUpFactor: 0.7,
abrMaxWithRealBitrate: false,
Expand Down Expand Up @@ -1342,6 +1344,12 @@ parameter should be a float greater than [abrEwmaFastVoD](#abrewmafastvod)

Default bandwidth estimate in bits/s prior to collecting fragment bandwidth samples.

### `abrEwmaDefaultEstimateMax`

(default: `5000000`)

Limits value of updated bandwidth estimate taken from first variant found in multivariant playlist on start.

### `abrBandWidthFactor`

(default: `0.95`)
Expand Down Expand Up @@ -1622,6 +1630,8 @@ Default value is set via [`capLevelToPlayerSize`](#capleveltoplayersize) in conf
get: Returns the current bandwidth estimate in bits/s, if available. Otherwise, `NaN` is returned.
set: Reset `EwmaBandWidthEstimator` using the value set as the new default estimate. This will update the value of `config.abrEwmaDefaultEstimate`.
### `hls.removeLevel(levelIndex, urlId)`
Remove a loaded level from the list of levels, or a url from a level's list of redundant urls.
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type ABRControllerConfig = {
* Default bandwidth estimate in bits/s prior to collecting fragment bandwidth samples
*/
abrEwmaDefaultEstimate: number;
abrEwmaDefaultEstimateMax: number;
abrBandWidthFactor: number;
abrBandWidthUpFactor: number;
abrMaxWithRealBitrate: boolean;
Expand Down Expand Up @@ -361,6 +362,7 @@ export const hlsDefaultConfig: HlsConfig = {
abrEwmaFastVoD: 3, // used by abr-controller
abrEwmaSlowVoD: 9, // used by abr-controller
abrEwmaDefaultEstimate: 5e5, // 500 kbps // used by abr-controller
abrEwmaDefaultEstimateMax: 5e6, // 5 mbps
abrBandWidthFactor: 0.95, // used by abr-controller
abrBandWidthUpFactor: 0.7, // used by abr-controller
abrMaxWithRealBitrate: false, // used by abr-controller
Expand Down
41 changes: 27 additions & 14 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,28 @@ class AbrController implements AbrComponentAPI {
private partCurrent: Part | null = null;
private bitrateTestDelay: number = 0;

public readonly bwEstimator: EwmaBandWidthEstimator;
public bwEstimator: EwmaBandWidthEstimator;

constructor(hls: Hls) {
this.hls = hls;
this.bwEstimator = this.initEstimator();
this.registerListeners();
}

const config = hls.config;
this.bwEstimator = new EwmaBandWidthEstimator(
public resetEstimator(abrEwmaDefaultEstimate?: number) {
if (abrEwmaDefaultEstimate) {
logger.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`);
this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate;
}
this.bwEstimator = this.initEstimator();
}
private initEstimator(): EwmaBandWidthEstimator {
const config = this.hls.config;
return new EwmaBandWidthEstimator(
config.abrEwmaSlowVoD,
config.abrEwmaFastVoD,
config.abrEwmaDefaultEstimate
);

this.registerListeners();
}

protected registerListeners() {
Expand Down Expand Up @@ -90,7 +99,7 @@ class AbrController implements AbrComponentAPI {
bandwidth: number,
fragSizeBits: number,
isSwitch: boolean
) {
): number {
const fragLoadSec = timeToFirstByteSec + fragSizeBits / bandwidth;
const playlistLoadSec = isSwitch ? this.lastLevelLoadSec : 0;
return fragLoadSec + playlistLoadSec;
Expand Down Expand Up @@ -173,7 +182,7 @@ class AbrController implements AbrComponentAPI {
? stats.loading.first - stats.loading.start
: -1;
const loadedFirstByte = stats.loaded && ttfb > -1;
const bwEstimate: number = this.bwEstimator.getEstimate();
const bwEstimate: number = this.getBwEstimate();
const { levels, minAutoLevel } = hls;
const level = levels[frag.level];
const expectedLen =
Expand Down Expand Up @@ -328,7 +337,7 @@ class AbrController implements AbrComponentAPI {
this.bwEstimator.getEstimateTTFB()
);
this.bwEstimator.sample(processingMs, stats.loaded);
stats.bwEstimate = this.bwEstimator.getEstimate();
stats.bwEstimate = this.getBwEstimate();
if (frag.bitrateTest) {
this.bitrateTestDelay = processingMs / 1000;
} else {
Expand All @@ -346,7 +355,7 @@ class AbrController implements AbrComponentAPI {
}

// return next auto level
get nextAutoLevel() {
get nextAutoLevel(): number {
const forcedAutoLevel = this._nextAutoLevel;
const bwEstimator = this.bwEstimator;
// in case next auto level has been forced, and bw not available or not reliable, return forced value
Expand Down Expand Up @@ -387,9 +396,7 @@ class AbrController implements AbrComponentAPI {
// if we're playing back at the normal rate.
const playbackRate =
media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0;
const avgbw = this.bwEstimator
? this.bwEstimator.getEstimate()
: config.abrEwmaDefaultEstimate;
const avgbw = this.getBwEstimate();
// bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
const bufferInfo = hls.mainForwardBufferInfo;
const bufferStarvationDelay =
Expand Down Expand Up @@ -455,6 +462,12 @@ class AbrController implements AbrComponentAPI {
return Math.max(bestLevel, 0);
}

private getBwEstimate(): number {
return this.bwEstimator
? this.bwEstimator.getEstimate()
: this.hls.config.abrEwmaDefaultEstimate;
}

private findBestLevel(
currentBw: number,
minAutoLevel: number,
Expand Down Expand Up @@ -543,7 +556,7 @@ class AbrController implements AbrComponentAPI {
// 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 === 0 ||
(fetchDuration <= ttfbEstimateSec ||
!Number.isFinite(fetchDuration) ||
(live && !this.bitrateTestDelay) ||
fetchDuration < maxFetchDuration)
Expand All @@ -556,7 +569,7 @@ class AbrController implements AbrComponentAPI {
return -1;
}

set nextAutoLevel(nextLevel) {
set nextAutoLevel(nextLevel: number) {
this._nextAutoLevel = nextLevel;
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/controller/content-steering-controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Events } from '../events';
import { Level } from '../types/level';
import { Level, addGroupId } from '../types/level';
import { AttrList } from '../utils/attr-list';
import { addGroupId } from './level-controller';
import { ErrorActionFlags, NetworkErrorAction } from './error-controller';
import { logger } from '../utils/logger';
import type Hls from '../hls';
Expand Down
45 changes: 21 additions & 24 deletions src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
LevelsUpdatedData,
ManifestLoadingData,
} from '../types/events';
import { Level } from '../types/level';
import { Level, addGroupId } from '../types/level';
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
import {
Expand All @@ -22,10 +22,11 @@ import {
} from '../utils/codecs';
import BasePlaylistController from './base-playlist-controller';
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
import ContentSteeringController from './content-steering-controller';
import { hlsDefaultConfig } from '../config';
import type Hls from '../hls';
import type { HlsUrlParameters, LevelParsed } from '../types/level';
import type { MediaPlaylist } from '../types/media-playlist';
import ContentSteeringController from './content-steering-controller';

let chromeOrFirefox: boolean;

Expand Down Expand Up @@ -279,9 +280,24 @@ export default class LevelController extends BasePlaylistController {
for (let i = 0; i < levels.length; i++) {
if (levels[i] === firstLevelInPlaylist) {
this._firstLevel = i;
const firstLevelBitrate = firstLevelInPlaylist.bitrate;
const bandwidthEstimate = this.hls.bandwidthEstimate;
this.log(
`manifest loaded, ${levels.length} level(s) found, first bitrate: ${firstLevelInPlaylist.bitrate}`
`manifest loaded, ${levels.length} level(s) found, first bitrate: ${firstLevelBitrate}`
);
// Update default bwe to first variant bitrate as long it has not been configured or set
if (this.hls.userConfig?.abrEwmaDefaultEstimate === undefined) {
const startingBwEstimate = Math.min(
firstLevelBitrate,
this.hls.config.abrEwmaDefaultEstimateMax
);
if (
startingBwEstimate > bandwidthEstimate &&
bandwidthEstimate === hlsDefaultConfig.abrEwmaDefaultEstimate
) {
this.hls.bandwidthEstimate = startingBwEstimate;
}
}
break;
}
}
Expand Down Expand Up @@ -380,6 +396,8 @@ export default class LevelController extends BasePlaylistController {
delete levelSwitchingData._attrs;
// @ts-ignore
delete levelSwitchingData._urlId;
// @ts-ignore
delete levelSwitchingData._avgBitrate;
this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData);
// check if we need to load playlist for this level
const levelDetails = level.details;
Expand Down Expand Up @@ -617,27 +635,6 @@ export default class LevelController extends BasePlaylistController {
}
}

export function addGroupId(
level: Level,
type: string,
id: string | undefined
): void {
if (!id) {
return;
}
if (type === 'audio') {
if (!level.audioGroupIds) {
level.audioGroupIds = [];
}
level.audioGroupIds[level.url.length - 1] = id;
} else if (type === 'text') {
if (!level.textGroupIds) {
level.textGroupIds = [];
}
level.textGroupIds[level.url.length - 1] = id;
}
}

function assignTrackIdsByGroup(tracks: MediaPlaylist[]): void {
const groups = {};
tracks.forEach((track) => {
Expand Down
4 changes: 4 additions & 0 deletions src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,10 @@ export default class Hls implements HlsEventEmitter {
return bwEstimator.getEstimate();
}

set bandwidthEstimate(abrEwmaDefaultEstimate: number) {
this.abrController.resetEstimator(abrEwmaDefaultEstimate);
}

/**
* get time to first byte estimate
* @type {number}
Expand Down
4 changes: 2 additions & 2 deletions src/loader/m3u8-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ export default class M3U8Parser {
const level: LevelParsed = {
attrs,
bitrate:
attrs.decimalInteger('AVERAGE-BANDWIDTH') ||
attrs.decimalInteger('BANDWIDTH'),
attrs.decimalInteger('BANDWIDTH') ||
attrs.decimalInteger('AVERAGE-BANDWIDTH'),
name: attrs.NAME,
url: M3U8Parser.resolve(uri, baseurl),
};
Expand Down
27 changes: 27 additions & 0 deletions src/types/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class Level {
public textGroupIds?: (string | undefined)[];
public url: string[];
private _urlId: number = 0;
private _avgBitrate: number = 0;

constructor(data: LevelParsed) {
this.url = [data.url];
Expand All @@ -117,6 +118,7 @@ export class Level {
this.name = data.name;
this.width = data.width || 0;
this.height = data.height || 0;
this._avgBitrate = this.attrs.decimalInteger('AVERAGE-BANDWIDTH');
this.audioCodec = data.audioCodec;
this.videoCodec = data.videoCodec;
this.unknownCodecs = data.unknownCodecs;
Expand All @@ -130,6 +132,10 @@ export class Level {
return Math.max(this.realBitrate, this.bitrate);
}

get averageBitrate(): number {
return this._avgBitrate || this.realBitrate || this.bitrate;
}

get attrs(): LevelAttributes {
return this._attrs[this._urlId];
}
Expand Down Expand Up @@ -169,3 +175,24 @@ export class Level {
this._attrs.push(data.attrs);
}
}

export function addGroupId(
level: Level,
type: string,
id: string | undefined
): void {
if (!id) {
return;
}
if (type === 'audio') {
if (!level.audioGroupIds) {
level.audioGroupIds = [];
}
level.audioGroupIds[level.url.length - 1] = id;
} else if (type === 'text') {
if (!level.textGroupIds) {
level.textGroupIds = [];
}
level.textGroupIds[level.url.length - 1] = id;
}
}
Loading

0 comments on commit bc60dbf

Please sign in to comment.