mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
update components
This commit is contained in:
parent
beae29d407
commit
b0d117ad5b
16 changed files with 800 additions and 382 deletions
|
@ -1,23 +1,39 @@
|
|||
/*
|
||||
* 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_LOAD_PROGRESS);
|
||||
this.lastfetchlevel = 0;
|
||||
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
|
||||
|
@ -25,12 +41,96 @@ class AbrController extends EventHandler {
|
|||
// and leading to wrong bw estimation
|
||||
if (stats.aborted === undefined && data.frag.loadCounter === 1) {
|
||||
this.lastfetchduration = (performance.now() - stats.trequest) / 1000;
|
||||
this.lastfetchlevel = data.frag.level;
|
||||
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;
|
||||
|
@ -49,13 +149,9 @@ class AbrController extends EventHandler {
|
|||
maxAutoLevel = this._autoLevelCapping;
|
||||
}
|
||||
|
||||
// in case next auto level has been forced, return it straight-away (but capped)
|
||||
if (this._nextAutoLevel !== -1) {
|
||||
var nextLevel = Math.min(this._nextAutoLevel,maxAutoLevel);
|
||||
if (nextLevel === this.lastfetchlevel) {
|
||||
this._nextAutoLevel = -1;
|
||||
} else {
|
||||
return nextLevel;
|
||||
}
|
||||
return Math.min(this._nextAutoLevel,maxAutoLevel);
|
||||
}
|
||||
|
||||
// follow algorithm captured from stagefright :
|
||||
|
@ -65,7 +161,7 @@ class AbrController extends EventHandler {
|
|||
// 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.lastfetchlevel) {
|
||||
if (i <= this.lastLoadedFragLevel) {
|
||||
adjustedbw = 0.8 * lastbw;
|
||||
} else {
|
||||
adjustedbw = 0.7 * lastbw;
|
||||
|
|
106
dashboard-ui/bower_components/hls.js/src/controller/cap-level-controller.js
vendored
Normal file
106
dashboard-ui/bower_components/hls.js/src/controller/cap-level-controller.js
vendored
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* cap stream level to media size dimension controller
|
||||
*/
|
||||
|
||||
import Event from '../events';
|
||||
import EventHandler from '../event-handler';
|
||||
|
||||
class CapLevelController extends EventHandler {
|
||||
constructor(hls) {
|
||||
super(hls,
|
||||
Event.MEDIA_ATTACHING,
|
||||
Event.MANIFEST_PARSED);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.hls.config.capLevelToPlayerSize) {
|
||||
this.media = null;
|
||||
this.autoLevelCapping = Number.POSITIVE_INFINITY;
|
||||
if (this.timer) {
|
||||
this.timer = clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMediaAttaching(data) {
|
||||
this.media = data.media instanceof HTMLVideoElement ? data.media : null;
|
||||
}
|
||||
|
||||
onManifestParsed(data) {
|
||||
if (this.hls.config.capLevelToPlayerSize) {
|
||||
this.autoLevelCapping = Number.POSITIVE_INFINITY;
|
||||
this.levels = data.levels;
|
||||
this.hls.firstLevel = this.getMaxLevel(data.firstLevel);
|
||||
clearInterval(this.timer);
|
||||
this.timer = setInterval(this.detectPlayerSize.bind(this), 1000);
|
||||
this.detectPlayerSize();
|
||||
}
|
||||
}
|
||||
|
||||
detectPlayerSize() {
|
||||
if (this.media) {
|
||||
let levelsLength = this.levels ? this.levels.length : 0;
|
||||
if (levelsLength) {
|
||||
this.hls.autoLevelCapping = this.getMaxLevel(levelsLength - 1);
|
||||
if (this.hls.autoLevelCapping > this.autoLevelCapping) {
|
||||
// if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch
|
||||
// usually happen when the user go to the fullscreen mode.
|
||||
this.hls.streamController.nextLevelSwitch();
|
||||
}
|
||||
this.autoLevelCapping = this.hls.autoLevelCapping;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
|
||||
*/
|
||||
getMaxLevel(capLevelIndex) {
|
||||
let result,
|
||||
i,
|
||||
level,
|
||||
mWidth = this.mediaWidth,
|
||||
mHeight = this.mediaHeight,
|
||||
lWidth = 0,
|
||||
lHeight = 0;
|
||||
|
||||
for (i = 0; i <= capLevelIndex; i++) {
|
||||
level = this.levels[i];
|
||||
result = i;
|
||||
lWidth = level.width;
|
||||
lHeight = level.height;
|
||||
if (mWidth <= lWidth || mHeight <= lHeight) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get contentScaleFactor() {
|
||||
let pixelRatio = 1;
|
||||
try {
|
||||
pixelRatio = window.devicePixelRatio;
|
||||
} catch(e) {}
|
||||
return pixelRatio;
|
||||
}
|
||||
|
||||
get mediaWidth() {
|
||||
let width;
|
||||
if (this.media) {
|
||||
width = this.media.width || this.media.clientWidth || this.media.offsetWidth;
|
||||
width *= this.contentScaleFactor;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
get mediaHeight() {
|
||||
let height;
|
||||
if (this.media) {
|
||||
height = this.media.height || this.media.clientHeight || this.media.offsetHeight;
|
||||
height *= this.contentScaleFactor;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
export default CapLevelController;
|
|
@ -7,6 +7,7 @@ import Event from '../events';
|
|||
import EventHandler from '../event-handler';
|
||||
import {logger} from '../utils/logger';
|
||||
import BinarySearch from '../utils/binary-search';
|
||||
import BufferHelper from '../helper/buffer-helper';
|
||||
import LevelHelper from '../helper/level-helper';
|
||||
import {ErrorTypes, ErrorDetails} from '../errors';
|
||||
|
||||
|
@ -36,6 +37,7 @@ class StreamController extends EventHandler {
|
|||
Event.LEVEL_LOADED,
|
||||
Event.KEY_LOADED,
|
||||
Event.FRAG_LOADED,
|
||||
Event.FRAG_LOAD_EMERGENCY_ABORTED,
|
||||
Event.FRAG_PARSING_INIT_SEGMENT,
|
||||
Event.FRAG_PARSING_DATA,
|
||||
Event.FRAG_PARSED,
|
||||
|
@ -162,7 +164,7 @@ class StreamController extends EventHandler {
|
|||
// we are not at playback start, get next load level from level Controller
|
||||
level = hls.nextLoadLevel;
|
||||
}
|
||||
var bufferInfo = this.bufferInfo(pos,config.maxBufferHole),
|
||||
var bufferInfo = BufferHelper.bufferInfo(this.media,pos,config.maxBufferHole),
|
||||
bufferLen = bufferInfo.len,
|
||||
bufferEnd = bufferInfo.end,
|
||||
fragPrevious = this.fragPrevious,
|
||||
|
@ -330,63 +332,6 @@ class StreamController extends EventHandler {
|
|||
this.state = State.IDLE;
|
||||
}
|
||||
break;
|
||||
case State.FRAG_LOADING:
|
||||
/*
|
||||
monitor fragment retrieval time...
|
||||
we compute expected time of arrival of the complete fragment.
|
||||
we compare it to expected time of buffer starvation
|
||||
*/
|
||||
let v = this.media,frag = this.fragCurrent;
|
||||
/* only monitor frag retrieval time if
|
||||
(video not paused OR first fragment being loaded) AND autoswitching enabled AND not lowest level AND multiple levels */
|
||||
if (v && (!v.paused || this.loadedmetadata === false) && frag.autoLevel && this.level && this.levels.length > 1) {
|
||||
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;
|
||||
}
|
||||
pos = v.currentTime;
|
||||
let fragLoadedDelay = (frag.expectedLen - frag.loaded) / loadRate;
|
||||
let bufferStarvationDelay = this.bufferInfo(pos,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 = this.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 * this.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();
|
||||
hls.trigger(Event.FRAG_LOAD_EMERGENCY_ABORTED, {frag: frag});
|
||||
// switch back to IDLE state to request new fragment at lower level
|
||||
this.state = State.IDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case State.FRAG_LOADING_WAITING_RETRY:
|
||||
var now = performance.now();
|
||||
var retryDate = this.retryDate;
|
||||
|
@ -398,12 +343,10 @@ class StreamController extends EventHandler {
|
|||
this.state = State.IDLE;
|
||||
}
|
||||
break;
|
||||
case State.STOPPED:
|
||||
case State.FRAG_LOADING:
|
||||
case State.PARSING:
|
||||
// nothing to do, wait for fragment being parsed
|
||||
break;
|
||||
case State.PARSED:
|
||||
// nothing to do, wait for all buffers to be appended
|
||||
break;
|
||||
case State.ENDED:
|
||||
break;
|
||||
default:
|
||||
|
@ -416,73 +359,7 @@ class StreamController extends EventHandler {
|
|||
}
|
||||
|
||||
|
||||
bufferInfo(pos,maxHoleDuration) {
|
||||
var media = this.media;
|
||||
if (media) {
|
||||
var vbuffered = media.buffered, buffered = [],i;
|
||||
for (i = 0; i < vbuffered.length; i++) {
|
||||
buffered.push({start: vbuffered.start(i), end: vbuffered.end(i)});
|
||||
}
|
||||
return this.bufferedInfo(buffered,pos,maxHoleDuration);
|
||||
} else {
|
||||
return {len: 0, start: 0, end: 0, nextStart : undefined} ;
|
||||
}
|
||||
}
|
||||
|
||||
bufferedInfo(buffered,pos,maxHoleDuration) {
|
||||
var buffered2 = [],
|
||||
// bufferStart and bufferEnd are buffer boundaries around current video position
|
||||
bufferLen,bufferStart, bufferEnd,bufferStartNext,i;
|
||||
// sort on buffer.start/smaller end (IE does not always return sorted buffered range)
|
||||
buffered.sort(function (a, b) {
|
||||
var diff = a.start - b.start;
|
||||
if (diff) {
|
||||
return diff;
|
||||
} else {
|
||||
return b.end - a.end;
|
||||
}
|
||||
});
|
||||
// there might be some small holes between buffer time range
|
||||
// consider that holes smaller than maxHoleDuration are irrelevant and build another
|
||||
// buffer time range representations that discards those holes
|
||||
for (i = 0; i < buffered.length; i++) {
|
||||
var buf2len = buffered2.length;
|
||||
if(buf2len) {
|
||||
var buf2end = buffered2[buf2len - 1].end;
|
||||
// if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
|
||||
if((buffered[i].start - buf2end) < maxHoleDuration) {
|
||||
// merge overlapping time ranges
|
||||
// update lastRange.end only if smaller than item.end
|
||||
// e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
|
||||
// whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
|
||||
if(buffered[i].end > buf2end) {
|
||||
buffered2[buf2len - 1].end = buffered[i].end;
|
||||
}
|
||||
} else {
|
||||
// big hole
|
||||
buffered2.push(buffered[i]);
|
||||
}
|
||||
} else {
|
||||
// first value
|
||||
buffered2.push(buffered[i]);
|
||||
}
|
||||
}
|
||||
for (i = 0, bufferLen = 0, bufferStart = bufferEnd = pos; i < buffered2.length; i++) {
|
||||
var start = buffered2[i].start,
|
||||
end = buffered2[i].end;
|
||||
//logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i));
|
||||
if ((pos + maxHoleDuration) >= start && pos < end) {
|
||||
// play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
|
||||
bufferStart = start;
|
||||
bufferEnd = end;
|
||||
bufferLen = bufferEnd - pos;
|
||||
} else if ((pos + maxHoleDuration) < start) {
|
||||
bufferStartNext = start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {len: bufferLen, start: bufferStart, end: bufferEnd, nextStart : bufferStartNext};
|
||||
}
|
||||
|
||||
getBufferRange(position) {
|
||||
var i, range,
|
||||
|
@ -712,7 +589,7 @@ class StreamController extends EventHandler {
|
|||
if (this.state === State.FRAG_LOADING) {
|
||||
// check if currently loaded fragment is inside buffer.
|
||||
//if outside, cancel fragment loading, otherwise do nothing
|
||||
if (this.bufferInfo(this.media.currentTime,this.config.maxBufferHole).len === 0) {
|
||||
if (BufferHelper.bufferInfo(this.media,this.media.currentTime,this.config.maxBufferHole).len === 0) {
|
||||
logger.log('seeking outside of buffer while fragment load in progress, cancel fragment load');
|
||||
var fragCurrent = this.fragCurrent;
|
||||
if (fragCurrent) {
|
||||
|
@ -1114,7 +991,7 @@ _checkBuffer() {
|
|||
currentTime = targetSeekPosition;
|
||||
logger.log(`target seek position:${targetSeekPosition}`);
|
||||
}
|
||||
var bufferInfo = this.bufferInfo(currentTime,0),
|
||||
var bufferInfo = BufferHelper.bufferInfo(media,currentTime,0),
|
||||
expectedPlaying = !(media.paused || media.ended || media.seeking || readyState < 2),
|
||||
jumpThreshold = 0.4, // tolerance needed as some browsers stalls playback before reaching buffered range end
|
||||
playheadMoving = currentTime > media.playbackRate*this.lastCurrentTime;
|
||||
|
@ -1163,6 +1040,11 @@ _checkBuffer() {
|
|||
}
|
||||
}
|
||||
|
||||
onFragLoadEmergencyAborted() {
|
||||
this.state = State.IDLE;
|
||||
this.tick();
|
||||
}
|
||||
|
||||
onBufferFlushed() {
|
||||
/* after successful buffer flushing, rebuild buffer Range array
|
||||
loop through existing buffer range and check if
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue