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/level-controller.js
2015-12-23 12:46:01 -05:00

259 lines
8.2 KiB
JavaScript

/*
* Level Controller
*/
import Event from '../events';
import {logger} from '../utils/logger';
import {ErrorTypes, ErrorDetails} from '../errors';
class LevelController {
constructor(hls) {
this.hls = hls;
this.onml = this.onManifestLoaded.bind(this);
this.onll = this.onLevelLoaded.bind(this);
this.onerr = this.onError.bind(this);
this.ontick = this.tick.bind(this);
hls.on(Event.MANIFEST_LOADED, this.onml);
hls.on(Event.LEVEL_LOADED, this.onll);
hls.on(Event.ERROR, this.onerr);
this._manualLevel = this._autoLevelCapping = -1;
}
destroy() {
var hls = this.hls;
hls.off(Event.MANIFEST_LOADED, this.onml);
hls.off(Event.LEVEL_LOADED, this.onll);
hls.off(Event.ERROR, this.onerr);
if (this.timer) {
clearInterval(this.timer);
}
this._manualLevel = -1;
}
onManifestLoaded(event, data) {
var levels0 = [], levels = [], bitrateStart, i, bitrateSet = {}, videoCodecFound = false, audioCodecFound = false, hls = this.hls;
// regroup redundant level together
data.levels.forEach(level => {
if(level.videoCodec) {
videoCodecFound = true;
}
if(level.audioCodec) {
audioCodecFound = true;
}
var redundantLevelId = bitrateSet[level.bitrate];
if (redundantLevelId === undefined) {
bitrateSet[level.bitrate] = levels0.length;
level.url = [level.url];
level.urlId = 0;
levels0.push(level);
} else {
levels0[redundantLevelId].url.push(level.url);
}
});
// remove audio-only level if we also have levels with audio+video codecs signalled
if(videoCodecFound && audioCodecFound) {
levels0.forEach(level => {
if(level.videoCodec) {
levels.push(level);
}
});
} else {
levels = levels0;
}
// only keep level with supported audio/video codecs
levels = levels.filter(function(level) {
var checkSupported = function(codec) { return MediaSource.isTypeSupported(`video/mp4;codecs=${codec}`);};
var audioCodec = level.audioCodec, videoCodec = level.videoCodec;
return (!audioCodec || checkSupported(audioCodec)) &&
(!videoCodec || checkSupported(videoCodec));
});
if(levels.length) {
// start bitrate is the first bitrate of the manifest
bitrateStart = levels[0].bitrate;
// sort level on bitrate
levels.sort(function (a, b) {
return a.bitrate - b.bitrate;
});
this._levels = levels;
// find index of first level in sorted levels
for (i = 0; i < levels.length; i++) {
if (levels[i].bitrate === bitrateStart) {
this._firstLevel = i;
logger.log(`manifest loaded,${levels.length} level(s) found, first bitrate:${bitrateStart}`);
break;
}
}
hls.trigger(Event.MANIFEST_PARSED, {levels: this._levels, firstLevel: this._firstLevel, stats: data.stats});
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: hls.url, reason: 'no compatible level found in manifest'});
}
return;
}
get levels() {
return this._levels;
}
get level() {
return this._level;
}
set level(newLevel) {
if (this._level !== newLevel || this._levels[newLevel].details === undefined) {
this.setLevelInternal(newLevel);
}
}
setLevelInternal(newLevel) {
// check if level idx is valid
if (newLevel >= 0 && newLevel < this._levels.length) {
// stopping live reloading timer if any
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this._level = newLevel;
logger.log(`switching to level ${newLevel}`);
this.hls.trigger(Event.LEVEL_SWITCH, {level: newLevel});
var level = this._levels[newLevel];
// check if we need to load playlist for this level
if (level.details === undefined || level.details.live === true) {
// level not retrieved yet, or live playlist we need to (re)load it
logger.log(`(re)loading playlist for level ${newLevel}`);
var urlId = level.urlId;
this.hls.trigger(Event.LEVEL_LOADING, {url: level.url[urlId], level: newLevel, id: urlId});
}
} else {
// invalid level id given, trigger error
this.hls.trigger(Event.ERROR, {type : ErrorTypes.OTHER_ERROR, details: ErrorDetails.LEVEL_SWITCH_ERROR, level: newLevel, fatal: false, reason: 'invalid level idx'});
}
}
get manualLevel() {
return this._manualLevel;
}
set manualLevel(newLevel) {
this._manualLevel = newLevel;
if (newLevel !== -1) {
this.level = newLevel;
}
}
get firstLevel() {
return this._firstLevel;
}
set firstLevel(newLevel) {
this._firstLevel = newLevel;
}
get startLevel() {
if (this._startLevel === undefined) {
return this._firstLevel;
} else {
return this._startLevel;
}
}
set startLevel(newLevel) {
this._startLevel = newLevel;
}
onError(event, data) {
if(data.fatal) {
return;
}
var details = data.details, hls = this.hls, levelId, level;
// try to recover not fatal errors
switch(details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.FRAG_LOOP_LOADING_ERROR:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
levelId = data.frag.level;
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
levelId = data.level;
break;
default:
break;
}
/* try to switch to a redundant stream if any available.
* if no redundant stream available, emergency switch down (if in auto mode and current level not 0)
* otherwise, we cannot recover this network error ...
* don't raise FRAG_LOAD_ERROR and FRAG_LOAD_TIMEOUT as fatal, as it is handled by mediaController
*/
if (levelId !== undefined) {
level = this._levels[levelId];
if (level.urlId < (level.url.length - 1)) {
level.urlId++;
level.details = undefined;
logger.warn(`level controller,${details} for level ${levelId}: switching to redundant stream id ${level.urlId}`);
} else {
// we could try to recover if in auto mode and current level not lowest level (0)
let recoverable = ((this._manualLevel === -1) && levelId);
if (recoverable) {
logger.warn(`level controller,${details}: emergency switch-down for next fragment`);
hls.abrController.nextAutoLevel = 0;
} else if(level && level.details && level.details.live) {
logger.warn(`level controller,${details} on live stream, discard`);
// FRAG_LOAD_ERROR and FRAG_LOAD_TIMEOUT are handled by mediaController
} else if (details !== ErrorDetails.FRAG_LOAD_ERROR && details !== ErrorDetails.FRAG_LOAD_TIMEOUT) {
logger.error(`cannot recover ${details} error`);
this._level = undefined;
// stopping live reloading timer if any
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
// redispatch same error but with fatal set to true
data.fatal = true;
hls.trigger(event, data);
}
}
}
}
onLevelLoaded(event, data) {
// check if current playlist is a live playlist
if (data.details.live && !this.timer) {
// if live playlist we will have to reload it periodically
// set reload period to playlist target duration
this.timer = setInterval(this.ontick, 1000 * data.details.targetduration);
}
if (!data.details.live && this.timer) {
// playlist is not live and timer is armed : stopping it
clearInterval(this.timer);
this.timer = null;
}
}
tick() {
var levelId = this._level;
if (levelId !== undefined) {
var level = this._levels[levelId], urlId = level.urlId;
this.hls.trigger(Event.LEVEL_LOADING, {url: level.url[urlId], level: levelId, id: urlId});
}
}
nextLoadLevel() {
if (this._manualLevel !== -1) {
return this._manualLevel;
} else {
return this.hls.abrController.nextAutoLevel;
}
}
}
export default LevelController;