1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
jellyfin-web/dashboard-ui/bower_components/hls.js/src/controller/abr-controller.js
2016-07-02 14:05:40 -04:00

214 lines
8.5 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';
import EwmaBandWidthEstimator from './ewma-bandwidth-estimator';
class AbrController extends EventHandler {
constructor(hls) {
super(hls, Event.FRAG_LOADING,
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) {
if (!this.timer) {
this.timer = setInterval(this.onCheck, 100);
}
// lazy init of bw Estimator, rationale is that we use different params for Live/VoD
// so we need to wait for stream manifest / playlist type to instantiate it.
if (!this.bwEstimator) {
let hls = this.hls,
level = data.frag.level,
isLive = hls.levels[level].details.live,
config = hls.config,
ewmaFast, ewmaSlow;
if (isLive) {
ewmaFast = config.abrEwmaFastLive;
ewmaSlow = config.abrEwmaSlowLive;
} else {
ewmaFast = config.abrEwmaFastVoD;
ewmaSlow = config.abrEwmaSlowVoD;
}
this.bwEstimator = new EwmaBandWidthEstimator(hls,ewmaSlow,ewmaFast,config.abrEwmaDefaultEstimate);
}
let frag = data.frag;
frag.trequest = performance.now();
this.fragCurrent = frag;
}
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;
// if loader has been destroyed or loading has been aborted, stop timer and return
if(!frag.loader || ( frag.loader.stats && frag.loader.stats.aborted)) {
logger.warn(`frag loader destroy or aborted, disarm abandonRulesCheck`);
this.clearTimer();
return;
}
/* 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 levels = hls.levels,
loadRate = Math.max(1,frag.loaded * 1000 / requestDelay), // byte/s; at least 1 byte/s to avoid division by zero
// compute expected fragment length using frag duration and level bitrate. also ensure that expected len is gte than already loaded size
expectedLen = Math.max(frag.loaded, Math.round(frag.duration * levels[frag.level].bitrate / 8));
let pos = v.currentTime;
let fragLoadedDelay = (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 * 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;
// update bw estimate for this fragment before cancelling load (this will help reducing the bw)
this.bwEstimator.sample(requestDelay,frag.loaded);
// 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) {
var stats = data.stats;
// only update stats on 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.bwEstimator.sample(performance.now() - stats.trequest,stats.loaded);
}
// 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 hls = this.hls, i, maxAutoLevel, levels = hls.levels, config = hls.config;
if (this._autoLevelCapping === -1 && levels && levels.length) {
maxAutoLevel = 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);
}
let avgbw = this.bwEstimator ? this.bwEstimator.getEstimate() : config.abrEwmaDefaultEstimate,
adjustedbw;
// 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 = config.abrBandWidthFactor * avgbw;
} else {
adjustedbw = config.abrBandWidthUpFactor * avgbw;
}
if (adjustedbw < levels[i].bitrate) {
return Math.max(0, i - 1);
}
}
return i - 1;
}
set nextAutoLevel(nextLevel) {
this._nextAutoLevel = nextLevel;
}
}
export default AbrController;