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

Fix bandwidth sampling for small transfers and fast connections #3595

Closed
wants to merge 3 commits into from
Closed
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
26 changes: 12 additions & 14 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
//
// @public (undocumented)
export type ABRControllerConfig = {
abrEwmaFastLive: number;
abrEwmaSlowLive: number;
abrEwmaFastVoD: number;
abrEwmaSlowVoD: number;
abrEwmaFast: number;
abrEwmaSlow: number;
abrEwmaDefaultEstimate: number;
abrBandWidthFactor: number;
abrBandWidthUpFactor: number;
Expand Down Expand Up @@ -2110,16 +2108,16 @@ export interface UserdataSample {

// Warnings were encountered during analysis:
//
// src/config.ts:156:3 - (ae-forgotten-export) The symbol "AudioStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:157:3 - (ae-forgotten-export) The symbol "AudioTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:159:3 - (ae-forgotten-export) The symbol "SubtitleStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:160:3 - (ae-forgotten-export) The symbol "SubtitleTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:161:3 - (ae-forgotten-export) The symbol "TimelineController" needs to be exported by the entry point hls.d.ts
// src/config.ts:163:3 - (ae-forgotten-export) The symbol "EMEController" needs to be exported by the entry point hls.d.ts
// src/config.ts:165:3 - (ae-forgotten-export) The symbol "AbrController" needs to be exported by the entry point hls.d.ts
// src/config.ts:166:3 - (ae-forgotten-export) The symbol "BufferController" needs to be exported by the entry point hls.d.ts
// src/config.ts:167:3 - (ae-forgotten-export) The symbol "CapLevelController" needs to be exported by the entry point hls.d.ts
// src/config.ts:168:3 - (ae-forgotten-export) The symbol "FPSController" needs to be exported by the entry point hls.d.ts
// src/config.ts:154:3 - (ae-forgotten-export) The symbol "AudioStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:155:3 - (ae-forgotten-export) The symbol "AudioTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:157:3 - (ae-forgotten-export) The symbol "SubtitleStreamController" needs to be exported by the entry point hls.d.ts
// src/config.ts:158:3 - (ae-forgotten-export) The symbol "SubtitleTrackController" needs to be exported by the entry point hls.d.ts
// src/config.ts:159:3 - (ae-forgotten-export) The symbol "TimelineController" needs to be exported by the entry point hls.d.ts
// src/config.ts:161:3 - (ae-forgotten-export) The symbol "EMEController" needs to be exported by the entry point hls.d.ts
// src/config.ts:163:3 - (ae-forgotten-export) The symbol "AbrController" needs to be exported by the entry point hls.d.ts
// src/config.ts:164:3 - (ae-forgotten-export) The symbol "BufferController" needs to be exported by the entry point hls.d.ts
// src/config.ts:165:3 - (ae-forgotten-export) The symbol "CapLevelController" needs to be exported by the entry point hls.d.ts
// src/config.ts:166:3 - (ae-forgotten-export) The symbol "FPSController" needs to be exported by the entry point hls.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
48 changes: 11 additions & 37 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,8 @@
- [`stretchShortVideoTrack`](#stretchshortvideotrack)
- [`maxAudioFramesDrift`](#maxaudioframesdrift)
- [`forceKeyFrameOnDiscontinuity`](#forcekeyframeondiscontinuity)
- [`abrEwmaFastLive`](#abrewmafastlive)
- [`abrEwmaSlowLive`](#abrewmaslowlive)
- [`abrEwmaFastVoD`](#abrewmafastvod)
- [`abrEwmaSlowVoD`](#abrewmaslowvod)
- [`abrEwmaFast`](#abrewmafast)
- [`abrEwmaSlow`](#abrewmaslow)
- [`abrEwmaDefaultEstimate`](#abrewmadefaultestimate)
- [`abrBandWidthFactor`](#abrbandwidthfactor)
- [`abrBandWidthUpFactor`](#abrbandwidthupfactor)
Expand Down Expand Up @@ -375,10 +373,8 @@ var config = {
stretchShortVideoTrack: false,
maxAudioFramesDrift: 1,
forceKeyFrameOnDiscontinuity: true,
abrEwmaFastLive: 3.0,
abrEwmaSlowLive: 9.0,
abrEwmaFastVoD: 3.0,
abrEwmaSlowVoD: 9.0,
abrEwmaFastLive: 0.5,
abrEwmaSlowLive: 1.5,
abrEwmaDefaultEstimate: 500000,
abrBandWidthFactor: 0.95,
abrBandWidthUpFactor: 0.7,
Expand Down Expand Up @@ -1056,45 +1052,23 @@ Setting this parameter to false can also generate decoding weirdness when switch

parameter should be a boolean

### `abrEwmaFastLive`
### `abrEwmaFast`

(default: `3.0`)
(default: `0.5`)

Fast bitrate Exponential moving average half-life, used to compute average bitrate for Live streams.
Half of the estimate is based on the last abrEwmaFastLive seconds of sample history.
Each of the sample is weighted by the fragment loading duration.
Half of the estimate is based on the last abrEwmaFast fragments or parts of sample history.

parameter should be a float greater than 0

### `abrEwmaSlowLive`
### `abrEwmaSlow`

(default: `9.0`)
(default: `1.5`)

Slow bitrate Exponential moving average half-life, used to compute average bitrate for Live streams.
Half of the estimate is based on the last abrEwmaSlowLive seconds of sample history.
Each of the sample is weighted by the fragment loading duration.
Half of the estimate is based on the last abrEwmaSlow fragments or parts of sample history.

parameter should be a float greater than [abrEwmaFastLive](#abrewmafastlive)

### `abrEwmaFastVoD`

(default: `3.0`)

Fast bitrate Exponential moving average half-life, used to compute average bitrate for VoD streams.
Half of the estimate is based on the last abrEwmaFastVoD seconds of sample history.
Each of the sample is weighted by the fragment loading duration.

parameter should be a float greater than 0

### `abrEwmaSlowVoD`

(default: `9.0`)

Slow bitrate Exponential moving average half-life, used to compute average bitrate for VoD streams.
Half of the estimate is based on the last abrEwmaSlowVoD seconds of sample history.
Each of the sample is weighted by the fragment loading duration.

parameter should be a float greater than [abrEwmaFastVoD](#abrewmafastvod)
parameter should be a float greater than [abrEwmaFast](#abrewmafast)

### `abrEwmaDefaultEstimate`

Expand Down
12 changes: 4 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ import type {
} from './types/loader';

export type ABRControllerConfig = {
abrEwmaFastLive: number;
abrEwmaSlowLive: number;
abrEwmaFastVoD: number;
abrEwmaSlowVoD: number;
abrEwmaFast: number;
abrEwmaSlow: number;
abrEwmaDefaultEstimate: number;
abrBandWidthFactor: number;
abrBandWidthUpFactor: number;
Expand Down Expand Up @@ -242,10 +240,8 @@ export const hlsDefaultConfig: HlsConfig = {
stretchShortVideoTrack: false, // used by mp4-remuxer
maxAudioFramesDrift: 1, // used by mp4-remuxer
forceKeyFrameOnDiscontinuity: true, // used by ts-demuxer
abrEwmaFastLive: 3, // used by abr-controller
abrEwmaSlowLive: 9, // used by abr-controller
abrEwmaFastVoD: 3, // used by abr-controller
abrEwmaSlowVoD: 9, // used by abr-controller
abrEwmaFast: 0.5, // used by abr-controller
abrEwmaSlow: 1.5, // used by abr-controller
abrEwmaDefaultEstimate: 5e5, // 500 kbps // used by abr-controller
abrBandWidthFactor: 0.95, // used by abr-controller
abrBandWidthUpFactor: 0.7, // used by abr-controller
Expand Down
16 changes: 2 additions & 14 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {
FragLoadedData,
FragBufferedData,
ErrorData,
LevelLoadedData,
} from '../types/events';
import type { ComponentAPI } from '../types/component-api';

Expand All @@ -35,8 +34,8 @@ class AbrController implements ComponentAPI {

const config = hls.config;
this.bwEstimator = new EwmaBandWidthEstimator(
config.abrEwmaSlowVoD,
config.abrEwmaFastVoD,
config.abrEwmaSlow,
config.abrEwmaFast,
config.abrEwmaDefaultEstimate
);

Expand All @@ -48,7 +47,6 @@ class AbrController implements ComponentAPI {
hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
hls.on(Events.ERROR, this.onError, this);
}

Expand All @@ -57,7 +55,6 @@ class AbrController implements ComponentAPI {
hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
hls.off(Events.ERROR, this.onError, this);
}

Expand All @@ -80,15 +77,6 @@ class AbrController implements ComponentAPI {
}
}

protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
const config = this.hls.config;
if (data.details.live) {
this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive);
} else {
this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD);
}
}

/*
This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load
quickly enough to prevent underbuffering
Expand Down
33 changes: 10 additions & 23 deletions src/utils/ewma-bandwidth-estimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,29 @@ import EWMA from '../utils/ewma';
class EwmaBandWidthEstimator {
private defaultEstimate_: number;
private minWeight_: number;
private minDelayMs_: number;
private slow_: EWMA;
private fast_: EWMA;

constructor(slow: number, fast: number, defaultEstimate: number) {
this.defaultEstimate_ = defaultEstimate;
this.minWeight_ = 0.001;
this.minDelayMs_ = 50;
this.minWeight_ = 1;
this.slow_ = new EWMA(slow);
this.fast_ = new EWMA(fast);
}

update(slow: number, fast: number) {
const { slow_, fast_ } = this;
if (this.slow_.halfLife !== slow) {
this.slow_ = new EWMA(slow, slow_.getEstimate(), slow_.getTotalWeight());
sample(transferMs: number, numBytes: number) {
// limit speed to mitigate uncertainty from very fast transfers
if (numBytes) {
transferMs = Math.max(transferMs, 2);
// value is bandwidth in bits/s
const bandwidthInBps = (8000 * numBytes) / transferMs;
this.fast_.sample(bandwidthInBps);
this.slow_.sample(bandwidthInBps);
}
if (this.fast_.halfLife !== fast) {
this.fast_ = new EWMA(fast, fast_.getEstimate(), fast_.getTotalWeight());
}
}

sample(durationMs: number, numBytes: number) {
durationMs = Math.max(durationMs, this.minDelayMs_);
const numBits = 8 * numBytes;
// weight is duration in seconds
const durationS = durationMs / 1000;
// value is bandwidth in bits/s
const bandwidthInBps = numBits / durationS;
this.fast_.sample(durationS, bandwidthInBps);
this.slow_.sample(durationS, bandwidthInBps);
}

canEstimate(): boolean {
const fast = this.fast_;
return fast && fast.getTotalWeight() >= this.minWeight_;
return this.fast_.getTotalWeight() >= this.minWeight_;
}

getEstimate(): number {
Expand Down
8 changes: 3 additions & 5 deletions src/utils/ewma.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/*
* compute an Exponential Weighted moving average
* - https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
* - heavily inspired from shaka-player
*/

class EWMA {
Expand All @@ -19,10 +18,9 @@ class EWMA {
this.totalWeight_ = weight;
}

sample(weight: number, value: number) {
const adjAlpha = Math.pow(this.alpha_, weight);
this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_;
this.totalWeight_ += weight;
sample(value: number) {
this.estimate_ = value * (1 - this.alpha_) + this.alpha_ * this.estimate_;
this.totalWeight_++;
}

getTotalWeight(): number {
Expand Down
40 changes: 4 additions & 36 deletions tests/unit/controller/ewma-bandwidth-estimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ describe('EwmaBandWidthEstimator', function () {
expect(bwEstimator.getEstimate()).to.equal(1000000);
bwEstimator.sample(4000, 1000000);
expect(bwEstimator.getEstimate()).to.closeTo(
1396480.1544736226,
1511550.3977404737,
0.000000001
);
bwEstimator.sample(1000, 1000000);
expect(bwEstimator.getEstimate()).to.closeTo(
2056826.9489827948,
3775044.041335162,
0.000000001
);
});
Expand All @@ -47,45 +47,13 @@ describe('EwmaBandWidthEstimator', function () {
expect(bwEstimator.getEstimate()).to.equal(1000000);
bwEstimator.sample(4000, 1000000);
expect(bwEstimator.getEstimate()).to.closeTo(
1439580.319105247,
1519244.5768252169,
0.000000001
);
bwEstimator.sample(1000, 1000000);
expect(bwEstimator.getEstimate()).to.closeTo(
2208342.324322311,
3847839.2697109017,
0.000000001
);
});

it('returns correct value after updating slow and fast', function () {
const defaultEstimate = 5e5;
const bwEstimator = new EwmaBandWidthEstimator(9, 3, defaultEstimate);
expect(bwEstimator.getEstimate()).to.equal(defaultEstimate);
bwEstimator.sample(8000, 1000000);
expect(bwEstimator.getEstimate()).to.equal(1000000);
bwEstimator.sample(4000, 1000000);
expect(bwEstimator.getEstimate()).to.closeTo(
1439580.319105247,
0.000000001
);
bwEstimator.update(15, 4);
expect(bwEstimator.getEstimate()).to.closeTo(
1878125.393685882,
0.000000001
);
bwEstimator.sample(1000, 1000000);
expect(bwEstimator.getEstimate()).to.closeTo(
2966543.443461984,
0.000000001
);
});

it('returns correct value when updating before a sample', function () {
const defaultEstimate = 5e5;
const bwEstimator = new EwmaBandWidthEstimator(9, 3, defaultEstimate);
bwEstimator.update(15, 4);
expect(bwEstimator.getEstimate()).to.equal(defaultEstimate);
bwEstimator.sample(8000, 1000000);
expect(bwEstimator.getEstimate()).to.equal(1000000);
});
});