182 lines
7.2 KiB
JavaScript
182 lines
7.2 KiB
JavaScript
/*
|
|
* simple ABR Controller
|
|
* - compute next level based on last fragment bw heuristics
|
|
* - implement an abandon rules triggered if we have less than 2 frag buffered and if computed bw shows that we risk buffer stalling
|
|
*/
|
|
|
|
import Event from '../events';
|
|
import EventHandler from '../event-handler';
|
|
import BufferHelper from '../helper/buffer-helper';
|
|
import {ErrorDetails} from '../errors';
|
|
import {logger} from '../utils/logger';
|
|
|
|
class AbrController extends EventHandler {
|
|
|
|
constructor(hls) {
|
|
super(hls, Event.FRAG_LOADING,
|
|
Event.FRAG_LOAD_PROGRESS,
|
|
Event.FRAG_LOADED,
|
|
Event.ERROR);
|
|
this.lastLoadedFragLevel = 0;
|
|
this._autoLevelCapping = -1;
|
|
this._nextAutoLevel = -1;
|
|
this.hls = hls;
|
|
this.onCheck = this.abandonRulesCheck.bind(this);
|
|
}
|
|
|
|
destroy() {
|
|
this.clearTimer();
|
|
EventHandler.prototype.destroy.call(this);
|
|
}
|
|
|
|
onFragLoading(data) {
|
|
this.timer = setInterval(this.onCheck, 100);
|
|
this.fragCurrent = data.frag;
|
|
}
|
|
|
|
onFragLoadProgress(data) {
|
|
var stats = data.stats;
|
|
// only update stats if first frag loading
|
|
// if same frag is loaded multiple times, it might be in browser cache, and loaded quickly
|
|
// and leading to wrong bw estimation
|
|
if (stats.aborted === undefined && data.frag.loadCounter === 1) {
|
|
this.lastfetchduration = (performance.now() - stats.trequest) / 1000;
|
|
this.lastbw = (stats.loaded * 8) / this.lastfetchduration;
|
|
//console.log(`fetchDuration:${this.lastfetchduration},bw:${(this.lastbw/1000).toFixed(0)}/${stats.aborted}`);
|
|
}
|
|
}
|
|
|
|
abandonRulesCheck() {
|
|
/*
|
|
monitor fragment retrieval time...
|
|
we compute expected time of arrival of the complete fragment.
|
|
we compare it to expected time of buffer starvation
|
|
*/
|
|
let hls = this.hls, v = hls.media,frag = this.fragCurrent;
|
|
/* only monitor frag retrieval time if
|
|
(video not paused OR first fragment being loaded(ready state === HAVE_NOTHING = 0)) AND autoswitching enabled AND not lowest level (=> means that we have several levels) */
|
|
if (v && (!v.paused || !v.readyState) && frag.autoLevel && frag.level) {
|
|
let requestDelay = performance.now() - frag.trequest;
|
|
// monitor fragment load progress after half of expected fragment duration,to stabilize bitrate
|
|
if (requestDelay > (500 * frag.duration)) {
|
|
let loadRate = Math.max(1,frag.loaded * 1000 / requestDelay); // byte/s; at least 1 byte/s to avoid division by zero
|
|
if (frag.expectedLen < frag.loaded) {
|
|
frag.expectedLen = frag.loaded;
|
|
}
|
|
let pos = v.currentTime;
|
|
let fragLoadedDelay = (frag.expectedLen - frag.loaded) / loadRate;
|
|
let bufferStarvationDelay = BufferHelper.bufferInfo(v,pos,hls.config.maxBufferHole).end - pos;
|
|
// consider emergency switch down only if we have less than 2 frag buffered AND
|
|
// time to finish loading current fragment is bigger than buffer starvation delay
|
|
// ie if we risk buffer starvation if bw does not increase quickly
|
|
if (bufferStarvationDelay < 2*frag.duration && fragLoadedDelay > bufferStarvationDelay) {
|
|
let fragLevelNextLoadedDelay, nextLoadLevel;
|
|
// lets iterate through lower level and try to find the biggest one that could avoid rebuffering
|
|
// we start from current level - 1 and we step down , until we find a matching level
|
|
for (nextLoadLevel = frag.level - 1 ; nextLoadLevel >=0 ; nextLoadLevel--) {
|
|
// compute time to load next fragment at lower level
|
|
// 0.8 : consider only 80% of current bw to be conservative
|
|
// 8 = bits per byte (bps/Bps)
|
|
fragLevelNextLoadedDelay = frag.duration * hls.levels[nextLoadLevel].bitrate / (8 * 0.8 * loadRate);
|
|
logger.log(`fragLoadedDelay/bufferStarvationDelay/fragLevelNextLoadedDelay[${nextLoadLevel}] :${fragLoadedDelay.toFixed(1)}/${bufferStarvationDelay.toFixed(1)}/${fragLevelNextLoadedDelay.toFixed(1)}`);
|
|
if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
|
|
// we found a lower level that be rebuffering free with current estimated bw !
|
|
break;
|
|
}
|
|
}
|
|
// only emergency switch down if it takes less time to load new fragment at lowest level instead
|
|
// of finishing loading current one ...
|
|
if (fragLevelNextLoadedDelay < fragLoadedDelay) {
|
|
// ensure nextLoadLevel is not negative
|
|
nextLoadLevel = Math.max(0,nextLoadLevel);
|
|
// force next load level in auto mode
|
|
hls.nextLoadLevel = nextLoadLevel;
|
|
// abort fragment loading ...
|
|
logger.warn(`loading too slow, abort fragment loading and switch to level ${nextLoadLevel}`);
|
|
//abort fragment loading
|
|
frag.loader.abort();
|
|
this.clearTimer();
|
|
hls.trigger(Event.FRAG_LOAD_EMERGENCY_ABORTED, {frag: frag});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onFragLoaded(data) {
|
|
// stop monitoring bw once frag loaded
|
|
this.clearTimer();
|
|
// store level id after successful fragment load
|
|
this.lastLoadedFragLevel = data.frag.level;
|
|
// reset forced auto level value so that next level will be selected
|
|
this._nextAutoLevel = -1;
|
|
}
|
|
|
|
onError(data) {
|
|
// stop timer in case of frag loading error
|
|
switch(data.details) {
|
|
case ErrorDetails.FRAG_LOAD_ERROR:
|
|
case ErrorDetails.FRAG_LOAD_TIMEOUT:
|
|
this.clearTimer();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
clearTimer() {
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
}
|
|
|
|
/** Return the capping/max level value that could be used by automatic level selection algorithm **/
|
|
get autoLevelCapping() {
|
|
return this._autoLevelCapping;
|
|
}
|
|
|
|
/** set the capping/max level value that could be used by automatic level selection algorithm **/
|
|
set autoLevelCapping(newLevel) {
|
|
this._autoLevelCapping = newLevel;
|
|
}
|
|
|
|
get nextAutoLevel() {
|
|
var lastbw = this.lastbw, hls = this.hls,adjustedbw, i, maxAutoLevel;
|
|
if (this._autoLevelCapping === -1 && hls.levels && hls.levels.length) {
|
|
maxAutoLevel = hls.levels.length - 1;
|
|
} else {
|
|
maxAutoLevel = this._autoLevelCapping;
|
|
}
|
|
|
|
// in case next auto level has been forced, return it straight-away (but capped)
|
|
if (this._nextAutoLevel !== -1) {
|
|
return Math.min(this._nextAutoLevel,maxAutoLevel);
|
|
}
|
|
|
|
// follow algorithm captured from stagefright :
|
|
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
|
|
// Pick the highest bandwidth stream below or equal to estimated bandwidth.
|
|
for (i = 0; i <= maxAutoLevel; i++) {
|
|
// consider only 80% of the available bandwidth, but if we are switching up,
|
|
// be even more conservative (70%) to avoid overestimating and immediately
|
|
// switching back.
|
|
if (i <= this.lastLoadedFragLevel) {
|
|
adjustedbw = 0.8 * lastbw;
|
|
} else {
|
|
adjustedbw = 0.7 * lastbw;
|
|
}
|
|
if (adjustedbw < hls.levels[i].bitrate) {
|
|
return Math.max(0, i - 1);
|
|
}
|
|
}
|
|
return i - 1;
|
|
}
|
|
|
|
set nextAutoLevel(nextLevel) {
|
|
this._nextAutoLevel = nextLevel;
|
|
}
|
|
}
|
|
|
|
export default AbrController;
|
|
|