diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index 65cdb1e3498..254e7312b53 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -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) @@ -45,6 +45,8 @@ export class AbrController implements AbrComponentAPI { // (undocumented) protected registerListeners(): void; // (undocumented) + resetEstimator(abrEwmaDefaultEstimate?: number): void; + // (undocumented) protected unregisterListeners(): void; } @@ -57,6 +59,7 @@ export type ABRControllerConfig = { abrEwmaFastVoD: number; abrEwmaSlowVoD: number; abrEwmaDefaultEstimate: number; + abrEwmaDefaultEstimateMax: number; abrBandWidthFactor: number; abrBandWidthUpFactor: number; abrMaxWithRealBitrate: boolean; @@ -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); @@ -1982,6 +1986,8 @@ export class Level { // (undocumented) audioGroupIds?: (string | undefined)[]; // (undocumented) + get averageBitrate(): number; + // (undocumented) readonly bitrate: number; // (undocumented) readonly codecSet: string; diff --git a/docs/API.md b/docs/API.md index ff869ce4ef7..5982944d1d8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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) @@ -410,6 +411,7 @@ var config = { abrEwmaFastVoD: 3.0, abrEwmaSlowVoD: 9.0, abrEwmaDefaultEstimate: 500000, + abrEwmaDefaultEstimateMax: 5000000, abrBandWidthFactor: 0.95, abrBandWidthUpFactor: 0.7, abrMaxWithRealBitrate: false, @@ -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`) @@ -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. diff --git a/src/config.ts b/src/config.ts index e7a58a1d470..2404b47fa69 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; @@ -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 diff --git a/src/controller/abr-controller.ts b/src/controller/abr-controller.ts index 8b95ea018af..2f663839f13 100644 --- a/src/controller/abr-controller.ts +++ b/src/controller/abr-controller.ts @@ -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() { @@ -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; @@ -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 = @@ -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 { @@ -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 @@ -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 = @@ -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, @@ -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) @@ -556,7 +569,7 @@ class AbrController implements AbrComponentAPI { return -1; } - set nextAutoLevel(nextLevel) { + set nextAutoLevel(nextLevel: number) { this._nextAutoLevel = nextLevel; } } diff --git a/src/controller/content-steering-controller.ts b/src/controller/content-steering-controller.ts index 6a547e73777..6dc8d6f14dc 100644 --- a/src/controller/content-steering-controller.ts +++ b/src/controller/content-steering-controller.ts @@ -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'; diff --git a/src/controller/level-controller.ts b/src/controller/level-controller.ts index 0bb244575f3..a100951b5e9 100644 --- a/src/controller/level-controller.ts +++ b/src/controller/level-controller.ts @@ -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 { @@ -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; @@ -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; } } @@ -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; @@ -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) => { diff --git a/src/hls.ts b/src/hls.ts index 859d65cf5b9..81584672a4d 100644 --- a/src/hls.ts +++ b/src/hls.ts @@ -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} diff --git a/src/loader/m3u8-parser.ts b/src/loader/m3u8-parser.ts index 2ecf9368d66..edd072b5afe 100644 --- a/src/loader/m3u8-parser.ts +++ b/src/loader/m3u8-parser.ts @@ -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), }; diff --git a/src/types/level.ts b/src/types/level.ts index 7abe3c2f4cd..3fccd8265bd 100644 --- a/src/types/level.ts +++ b/src/types/level.ts @@ -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]; @@ -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; @@ -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]; } @@ -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; + } +} diff --git a/tests/unit/controller/abr-controller.js b/tests/unit/controller/abr-controller.ts similarity index 64% rename from tests/unit/controller/abr-controller.js rename to tests/unit/controller/abr-controller.ts index ab555430485..2c0f4d83256 100644 --- a/tests/unit/controller/abr-controller.js +++ b/tests/unit/controller/abr-controller.ts @@ -2,10 +2,25 @@ import AbrController from '../../../src/controller/abr-controller'; import EwmaBandWidthEstimator from '../../../src/utils/ewma-bandwidth-estimator'; import Hls from '../../../src/hls'; +import chai from 'chai'; +import sinonChai from 'sinon-chai'; + +chai.use(sinonChai); +const expect = chai.expect; + describe('AbrController', function () { + it('can be reset with new BWE', function () { + const hls = new Hls({ maxStarvationDelay: 4 }); + const abrController = new AbrController(hls); + abrController.bwEstimator = new EwmaBandWidthEstimator(15, 4, 5e5, 100); + expect(abrController.bwEstimator.getEstimate()).to.equal(5e5); + abrController.resetEstimator(5e6); + expect(abrController.bwEstimator.getEstimate()).to.equal(5e6); + }); + it('should return correct next auto level', function () { const hls = new Hls({ maxStarvationDelay: 4 }); - hls.levelController._levels = [ + (hls as any).levelController._levels = [ { bitrate: 105000, name: '144', @@ -38,7 +53,7 @@ describe('AbrController', function () { }, ]; const abrController = new AbrController(hls); - abrController.bwEstimator = new EwmaBandWidthEstimator(hls, 15, 4, 5e5); + abrController.bwEstimator = new EwmaBandWidthEstimator(15, 4, 5e5, 100); expect(abrController.nextAutoLevel).to.equal(0); }); }); diff --git a/tests/unit/controller/subtitle-stream-controller.js b/tests/unit/controller/subtitle-stream-controller.js index f12eb8b267d..11cd0014a74 100644 --- a/tests/unit/controller/subtitle-stream-controller.js +++ b/tests/unit/controller/subtitle-stream-controller.js @@ -7,6 +7,7 @@ import { Fragment } from '../../../src/loader/fragment'; import { PlaylistLevelType } from '../../../src/types/loader'; import KeyLoader from '../../../src/loader/key-loader'; import { SubtitleStreamController } from '../../../src/controller/subtitle-stream-controller'; +import { AttrList } from '../../../src/utils/attr-list'; const mediaMock = { currentTime: 0, @@ -18,11 +19,11 @@ const tracksMock = [ { id: 0, details: { url: '', fragments: [] }, - attrs: {}, + attrs: new AttrList(), }, { id: 1, - attrs: {}, + attrs: new AttrList(), }, ];