Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base initial bandwidth estimate on first level bitrate #5649

Merged
merged 1 commit into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to move addGroupId because it created a circular dependency between level-controller -> config -> content-steering-controller.

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