Files
Zos/Skills/@be/node_modules/hls.js/lib/controller/level-controller.js

421 lines
17 KiB
JavaScript

'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _events = require('../events');
var _events2 = _interopRequireDefault(_events);
var _eventHandler = require('../event-handler');
var _eventHandler2 = _interopRequireDefault(_eventHandler);
var _logger = require('../utils/logger');
var _errors = require('../errors');
var _bufferHelper = require('../helper/buffer-helper');
var _bufferHelper2 = _interopRequireDefault(_bufferHelper);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /*
* Level Controller
*/
var LevelController = function (_EventHandler) {
_inherits(LevelController, _EventHandler);
function LevelController(hls) {
_classCallCheck(this, LevelController);
var _this = _possibleConstructorReturn(this, (LevelController.__proto__ || Object.getPrototypeOf(LevelController)).call(this, hls, _events2.default.MANIFEST_LOADED, _events2.default.LEVEL_LOADED, _events2.default.FRAG_LOADED, _events2.default.ERROR));
_this.ontick = _this.tick.bind(_this);
_this._manualLevel = -1;
return _this;
}
_createClass(LevelController, [{
key: 'destroy',
value: function destroy() {
this.cleanTimer();
this._manualLevel = -1;
}
}, {
key: 'cleanTimer',
value: function cleanTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}, {
key: 'startLoad',
value: function startLoad() {
this.canload = true;
var levels = this._levels;
// clean up live level details to force reload them, and reset load errors
if (levels) {
levels.forEach(function (level) {
level.loadError = 0;
var levelDetails = level.details;
if (levelDetails && levelDetails.live) {
level.details = undefined;
}
});
}
// speed up live playlist refresh if timer exists
if (this.timer) {
this.tick();
}
}
}, {
key: 'stopLoad',
value: function stopLoad() {
this.canload = false;
}
}, {
key: 'onManifestLoaded',
value: function onManifestLoaded(data) {
var levels0 = [],
levels = [],
bitrateStart,
bitrateSet = {},
videoCodecFound = false,
audioCodecFound = false,
hls = this.hls,
brokenmp4inmp3 = /chrome|firefox/.test(navigator.userAgent.toLowerCase()),
checkSupported = function checkSupported(type, codec) {
return MediaSource.isTypeSupported(type + '/mp4;codecs=' + codec);
};
// regroup redundant level together
data.levels.forEach(function (level) {
if (level.videoCodec) {
videoCodecFound = true;
}
// erase audio codec info if browser does not support mp4a.40.34. demuxer will autodetect codec and fallback to mpeg/audio
if (brokenmp4inmp3 && level.audioCodec && level.audioCodec.indexOf('mp4a.40.34') !== -1) {
level.audioCodec = undefined;
}
if (level.audioCodec || level.attrs && level.attrs.AUDIO) {
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(function (level) {
if (level.videoCodec) {
levels.push(level);
}
});
} else {
levels = levels0;
}
// only keep level with supported audio/video codecs
levels = levels.filter(function (level) {
var audioCodec = level.audioCodec,
videoCodec = level.videoCodec;
return (!audioCodec || checkSupported('audio', audioCodec)) && (!videoCodec || checkSupported('video', 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 (var i = 0; i < levels.length; i++) {
if (levels[i].bitrate === bitrateStart) {
this._firstLevel = i;
_logger.logger.log('manifest loaded,' + levels.length + ' level(s) found, first bitrate:' + bitrateStart);
break;
}
}
hls.trigger(_events2.default.MANIFEST_PARSED, { levels: levels, firstLevel: this._firstLevel, stats: data.stats, audio: audioCodecFound, video: videoCodecFound, altAudio: data.audioTracks.length > 0 });
} else {
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, fatal: true, url: hls.url, reason: 'no level with compatible codecs found in manifest' });
}
return;
}
}, {
key: 'setLevelInternal',
value: function setLevelInternal(newLevel) {
var levels = this._levels;
var hls = this.hls;
// check if level idx is valid
if (newLevel >= 0 && newLevel < levels.length) {
// stopping live reloading timer if any
this.cleanTimer();
if (this._level !== newLevel) {
_logger.logger.log('switching to level ' + newLevel);
this._level = newLevel;
var levelProperties = levels[newLevel];
levelProperties.level = newLevel;
// LEVEL_SWITCH to be deprecated in next major release
hls.trigger(_events2.default.LEVEL_SWITCH, levelProperties);
hls.trigger(_events2.default.LEVEL_SWITCHING, levelProperties);
}
var level = levels[newLevel],
levelDetails = level.details;
// check if we need to load playlist for this level
if (!levelDetails || levelDetails.live === true) {
// level not retrieved yet, or live playlist we need to (re)load it
var urlId = level.urlId;
hls.trigger(_events2.default.LEVEL_LOADING, { url: level.url[urlId], level: newLevel, id: urlId });
}
} else {
// invalid level id given, trigger error
hls.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.OTHER_ERROR, details: _errors.ErrorDetails.LEVEL_SWITCH_ERROR, level: newLevel, fatal: false, reason: 'invalid level idx' });
}
}
}, {
key: 'onError',
value: function onError(data) {
if (data.fatal) {
if (data.type === _errors.ErrorTypes.NETWORK_ERROR) {
this.cleanTimer();
}
return;
}
var details = data.details,
hls = this.hls,
levelId = void 0,
level = void 0,
levelError = false;
// try to recover not fatal errors
switch (details) {
case _errors.ErrorDetails.FRAG_LOAD_ERROR:
case _errors.ErrorDetails.FRAG_LOAD_TIMEOUT:
case _errors.ErrorDetails.FRAG_LOOP_LOADING_ERROR:
case _errors.ErrorDetails.KEY_LOAD_ERROR:
case _errors.ErrorDetails.KEY_LOAD_TIMEOUT:
levelId = data.frag.level;
break;
case _errors.ErrorDetails.LEVEL_LOAD_ERROR:
case _errors.ErrorDetails.LEVEL_LOAD_TIMEOUT:
levelId = data.context.level;
levelError = true;
break;
case _errors.ErrorDetails.REMUX_ALLOC_ERROR:
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 ...
*/
if (levelId !== undefined) {
level = this._levels[levelId];
if (!level.loadError) {
level.loadError = 1;
} else {
level.loadError++;
}
// if any redundant streams available and if we haven't try them all (level.loadError is reseted on successful frag/level load.
// if level.loadError reaches nbRedundantLevel it means that we tried them all, no hope => let's switch down
var nbRedundantLevel = level.url.length;
if (nbRedundantLevel > 1 && level.loadError < nbRedundantLevel) {
level.urlId = (level.urlId + 1) % nbRedundantLevel;
level.details = undefined;
_logger.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)
var recoverable = this._manualLevel === -1 && levelId;
if (recoverable) {
_logger.logger.warn('level controller,' + details + ': switch-down for next fragment');
hls.nextAutoLevel = Math.max(0, levelId - 1);
} else if (level && level.details && level.details.live) {
_logger.logger.warn('level controller,' + details + ' on live stream, discard');
if (levelError) {
// reset this._level so that another call to set level() will retrigger a frag load
this._level = undefined;
}
// other errors are handled by stream controller
} else if (details === _errors.ErrorDetails.LEVEL_LOAD_ERROR || details === _errors.ErrorDetails.LEVEL_LOAD_TIMEOUT) {
var media = hls.media,
// 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end
mediaBuffered = media && _bufferHelper2.default.isBuffered(media, media.currentTime) && _bufferHelper2.default.isBuffered(media, media.currentTime + 0.5);
if (mediaBuffered) {
var retryDelay = hls.config.levelLoadingRetryDelay;
_logger.logger.warn('level controller,' + details + ', but media buffered, retry in ' + retryDelay + 'ms');
this.timer = setTimeout(this.ontick, retryDelay);
// boolean used to inform stream controller not to switch back to IDLE on non fatal error
data.levelRetry = true;
} else {
_logger.logger.error('cannot recover ' + details + ' error');
this._level = undefined;
// stopping live reloading timer if any
this.cleanTimer();
// switch error to fatal
data.fatal = true;
}
}
}
}
}
// reset level load error counter on successful frag loaded
}, {
key: 'onFragLoaded',
value: function onFragLoaded(data) {
var fragLoaded = data.frag;
if (fragLoaded && fragLoaded.type === 'main') {
var level = this._levels[fragLoaded.level];
if (level) {
level.loadError = 0;
}
}
}
}, {
key: 'onLevelLoaded',
value: function onLevelLoaded(data) {
var levelId = data.level;
// only process level loaded events matching with expected level
if (levelId === this._level) {
var curLevel = this._levels[levelId];
// reset level load error counter on successful level loaded
curLevel.loadError = 0;
var newDetails = data.details;
// if current playlist is a live playlist, arm a timer to reload it
if (newDetails.live) {
var reloadInterval = 1000 * (newDetails.averagetargetduration ? newDetails.averagetargetduration : newDetails.targetduration),
curDetails = curLevel.details;
if (curDetails && newDetails.endSN === curDetails.endSN) {
// follow HLS Spec, If the client reloads a Playlist file and finds that it has not
// changed then it MUST wait for a period of one-half the target
// duration before retrying.
reloadInterval /= 2;
_logger.logger.log('same live playlist, reload twice faster');
}
// decrement reloadInterval with level loading delay
reloadInterval -= performance.now() - data.stats.trequest;
// in any case, don't reload more than every second
reloadInterval = Math.max(1000, Math.round(reloadInterval));
_logger.logger.log('live playlist, reload in ' + reloadInterval + ' ms');
this.timer = setTimeout(this.ontick, reloadInterval);
} else {
this.timer = null;
}
}
}
}, {
key: 'tick',
value: function tick() {
var levelId = this._level;
if (levelId !== undefined && this.canload) {
var level = this._levels[levelId];
if (level && level.url) {
var urlId = level.urlId;
this.hls.trigger(_events2.default.LEVEL_LOADING, { url: level.url[urlId], level: levelId, id: urlId });
}
}
}
}, {
key: 'levels',
get: function get() {
return this._levels;
}
}, {
key: 'level',
get: function get() {
return this._level;
},
set: function set(newLevel) {
var levels = this._levels;
if (levels && levels.length > newLevel) {
if (this._level !== newLevel || levels[newLevel].details === undefined) {
this.setLevelInternal(newLevel);
}
}
}
}, {
key: 'manualLevel',
get: function get() {
return this._manualLevel;
},
set: function set(newLevel) {
this._manualLevel = newLevel;
if (this._startLevel === undefined) {
this._startLevel = newLevel;
}
if (newLevel !== -1) {
this.level = newLevel;
}
}
}, {
key: 'firstLevel',
get: function get() {
return this._firstLevel;
},
set: function set(newLevel) {
this._firstLevel = newLevel;
}
}, {
key: 'startLevel',
get: function get() {
// hls.startLevel takes precedence over config.startLevel
// if none of these values are defined, fallback on this._firstLevel (first quality level appearing in variant manifest)
if (this._startLevel === undefined) {
var configStartLevel = this.hls.config.startLevel;
if (configStartLevel !== undefined) {
return configStartLevel;
} else {
return this._firstLevel;
}
} else {
return this._startLevel;
}
},
set: function set(newLevel) {
this._startLevel = newLevel;
}
}, {
key: 'nextLoadLevel',
get: function get() {
if (this._manualLevel !== -1) {
return this._manualLevel;
} else {
return this.hls.nextAutoLevel;
}
},
set: function set(nextLevel) {
this.level = nextLevel;
if (this._manualLevel === -1) {
this.hls.nextAutoLevel = nextLevel;
}
}
}]);
return LevelController;
}(_eventHandler2.default);
exports.default = LevelController;