Skip to content

Commit 1adf475

Browse files
committed
abr-controller: fine tune EWMA algorithm
inspired from Shaka-Player: average bandwidth is now the minimum of two exponentially-weighted moving averages with different half-lives. default halflifes are set to zero (no EWMA) to keep behaviour unchanged related to https://github.com/dailymotion/hls.js/pull/467
1 parent 7bb1f2a commit 1adf475

File tree

5 files changed

+114
-27
lines changed

5 files changed

+114
-27
lines changed

API.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,14 +483,20 @@ this helps playback continue in certain cases that might otherwise get stuck.
483483
484484
parameter should be a boolean
485485
486-
#### ```abrBandwidthWeight```
487-
(default : 1.0)
486+
#### ```abrEwmaFast```
487+
(default : 0.0)
488488
489-
The weight to apply to the current bandwidth measurement when calculating the Exponentially-Weighted Moving Average (EWMA) of the bandwidth in the ABR controller.
489+
Fast bitrate Exponential moving average half-life , used to compute average bitrate
490+
Half of the estimate is based on the last abrEwmaFast seconds of sample history.
491+
parameter should be a float greater than 0
490492
491-
If ```α := abrBandwidthWeight```, then ```bandwidth average :=* latest bandwidth measurement) + ((1 - α) * previous bandwidth average)```.
493+
#### ```abrEwmaSlow```
494+
(default : 0.0)
495+
496+
Slow bitrate Exponential moving average half-life , used to compute average bitrate
497+
Half of the estimate is based on the last abrEwmaFast seconds of sample history.
498+
parameter should be a float greater than abrEwmaFast
492499
493-
parameter should be a float in the range (0.0, 1.0]
494500
495501
#### ```abrBandWidthFactor```
496502
(default : 0.8)

src/controller/abr-controller.js

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ import EventHandler from '../event-handler';
99
import BufferHelper from '../helper/buffer-helper';
1010
import {ErrorDetails} from '../errors';
1111
import {logger} from '../utils/logger';
12+
import EwmaBandWidthEstimator from './ewma-bandwidth-estimator';
1213

1314
class AbrController extends EventHandler {
1415

1516
constructor(hls) {
1617
super(hls, Event.FRAG_LOADING,
17-
Event.FRAG_LOAD_PROGRESS,
1818
Event.FRAG_LOADED,
1919
Event.ERROR);
2020
this.lastLoadedFragLevel = 0;
2121
this._autoLevelCapping = -1;
2222
this._nextAutoLevel = -1;
2323
this.hls = hls;
24+
this.bwEstimator = new EwmaBandWidthEstimator(hls);
2425
this.onCheck = this.abandonRulesCheck.bind(this);
2526
}
2627

@@ -36,24 +37,6 @@ class AbrController extends EventHandler {
3637
this.fragCurrent = data.frag;
3738
}
3839

39-
onFragLoadProgress(data) {
40-
var stats = data.stats;
41-
// only update stats if first frag loading
42-
// if same frag is loaded multiple times, it might be in browser cache, and loaded quickly
43-
// and leading to wrong bw estimation
44-
if (stats.aborted === undefined && data.frag.loadCounter === 1) {
45-
this.lastfetchduration = (performance.now() - stats.trequest) / 1000;
46-
let thisbw = (stats.loaded * 8) / this.lastfetchduration;
47-
if (this.lastbw) {
48-
let w = this.hls.config.abrBandwidthWeight;
49-
this.lastbw = (w * thisbw) + (1.0 - w) * this.lastbw;
50-
} else {
51-
this.lastbw = thisbw;
52-
}
53-
//console.log(`fetchDuration:${this.lastfetchduration},bw:${(this.lastbw/1000).toFixed(0)}/${stats.aborted}`);
54-
}
55-
}
56-
5740
abandonRulesCheck() {
5841
/*
5942
monitor fragment retrieval time...
@@ -107,6 +90,8 @@ class AbrController extends EventHandler {
10790
nextLoadLevel = Math.max(0,nextLoadLevel);
10891
// force next load level in auto mode
10992
hls.nextLoadLevel = nextLoadLevel;
93+
// update bw estimate for this fragment before cancelling load (this will help reducing the bw)
94+
this.bwEstimator.sample(requestDelay,frag.loaded);
11095
// abort fragment loading ...
11196
logger.warn(`loading too slow, abort fragment loading and switch to level ${nextLoadLevel}`);
11297
//abort fragment loading
@@ -120,6 +105,14 @@ class AbrController extends EventHandler {
120105
}
121106

122107
onFragLoaded(data) {
108+
var stats = data.stats;
109+
// only update stats on first frag loading
110+
// if same frag is loaded multiple times, it might be in browser cache, and loaded quickly
111+
// and leading to wrong bw estimation
112+
if (stats.aborted === undefined && data.frag.loadCounter === 1) {
113+
this.bwEstimator.sample(performance.now() - stats.trequest,stats.loaded);
114+
}
115+
123116
// stop monitoring bw once frag loaded
124117
this.clearTimer();
125118
// store level id after successful fragment load
@@ -174,6 +167,7 @@ class AbrController extends EventHandler {
174167
return Math.min(this._nextAutoLevel,maxAutoLevel);
175168
}
176169

170+
let avgbw = this.bwEstimator.getEstimate(),adjustedbw;
177171
// follow algorithm captured from stagefright :
178172
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
179173
// Pick the highest bandwidth stream below or equal to estimated bandwidth.
@@ -182,9 +176,9 @@ class AbrController extends EventHandler {
182176
// be even more conservative (70%) to avoid overestimating and immediately
183177
// switching back.
184178
if (i <= this.lastLoadedFragLevel) {
185-
adjustedbw = config.abrBandWidthFactor * lastbw;
179+
adjustedbw = config.abrBandWidthFactor * avgbw;
186180
} else {
187-
adjustedbw = config.abrBandWidthUpFactor * lastbw;
181+
adjustedbw = config.abrBandWidthUpFactor * avgbw;
188182
}
189183
if (adjustedbw < levels[i].bitrate) {
190184
return Math.max(0, i - 1);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* EWMA Bandwidth Estimator
3+
* - heavily inspired from shaka-player
4+
* Tracks bandwidth samples and estimates available bandwidth.
5+
* Based on the minimum of two exponentially-weighted moving averages with
6+
* different half-lives.
7+
*/
8+
9+
import EWMA from '../utils/ewma';
10+
11+
12+
class EwmaBandWidthEstimator {
13+
14+
constructor(hls) {
15+
this.hls = hls;
16+
this.defaultEstimate_ = 5e5; // 500kbps
17+
this.minWeight_ = 0.001;
18+
this.minDelayMs_ = 50;
19+
this.fast_ = new EWMA(hls.config.abrEwmaFast);
20+
this.slow_ = new EWMA(hls.config.abrEwmaSlow);
21+
}
22+
23+
sample(durationMs,numBytes) {
24+
durationMs = Math.max(durationMs, this.minDelayMs_);
25+
var bandwidth = 8000* numBytes / durationMs;
26+
//console.log('instant bw:'+ Math.round(bandwidth));
27+
// we weight sample using loading duration....
28+
var weigth = durationMs / 1000;
29+
this.fast_.sample(weigth,bandwidth);
30+
this.slow_.sample(weigth,bandwidth);
31+
}
32+
33+
34+
getEstimate() {
35+
if (this.fast_.getTotalWeight() < this.minWeight_) {
36+
return this.defaultEstimate_;
37+
}
38+
//console.log('slow estimate:'+ Math.round(this.slow_.getEstimate()));
39+
//console.log('fast estimate:'+ Math.round(this.fast_.getEstimate()));
40+
// Take the minimum of these two estimates. This should have the effect of
41+
// adapting down quickly, but up more slowly.
42+
return Math.min(this.fast_.getEstimate(),this.slow_.getEstimate());
43+
}
44+
45+
destroy() {
46+
}
47+
}
48+
export default EwmaBandWidthEstimator;
49+

src/hls.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ class Hls {
9191
enableCEA708Captions: true,
9292
enableMP2TPassThrough : false,
9393
stretchShortVideoTrack: false,
94-
abrBandwidthWeight: 1.0,
94+
abrEwmaFast: 0,
95+
abrEwmaSlow: 0,
9596
abrBandWidthFactor : 0.8,
9697
abrBandWidthUpFactor : 0.7
9798
};

src/utils/ewma.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* compute an Exponential Weighted moving average
3+
* - https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
4+
* - heavily inspired from shaka-player
5+
*/
6+
7+
class EWMA {
8+
9+
// About half of the estimated value will be from the last |halfLife| samples by weight.
10+
constructor(halfLife) {
11+
// Larger values of alpha expire historical data more slowly.
12+
this.alpha_ = halfLife ? Math.exp(Math.log(0.5) / halfLife) : 0;
13+
this.estimate_ = 0;
14+
this.totalWeight_ = 0;
15+
}
16+
17+
sample(weight,value) {
18+
var adjAlpha = Math.pow(this.alpha_, weight);
19+
this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_;
20+
this.totalWeight_ += weight;
21+
}
22+
23+
getTotalWeight() {
24+
return this.totalWeight_;
25+
}
26+
27+
getEstimate() {
28+
if (this.alpha_) {
29+
var zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_);
30+
return this.estimate_ / zeroFactor;
31+
} else {
32+
return this.estimate_;
33+
}
34+
}
35+
}
36+
37+
export default EWMA;

0 commit comments

Comments
 (0)