421 lines
17 KiB
JavaScript
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;
|