/* * 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;