/** * @file videojs-contrib-hls.js * * The main file for the HLS project. * License: https://github.com/videojs/videojs-contrib-hls/blob/master/LICENSE */ 'use strict'; 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 _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 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 _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; } var _globalDocument = require('global/document'); var _globalDocument2 = _interopRequireDefault(_globalDocument); var _playlistLoader = require('./playlist-loader'); var _playlistLoader2 = _interopRequireDefault(_playlistLoader); var _playlist = require('./playlist'); var _playlist2 = _interopRequireDefault(_playlist); var _xhr = require('./xhr'); var _xhr2 = _interopRequireDefault(_xhr); var _aesDecrypter = require('aes-decrypter'); var _binUtils = require('./bin-utils'); var _binUtils2 = _interopRequireDefault(_binUtils); var _videojsContribMediaSources = require('videojs-contrib-media-sources'); var _m3u8Parser = require('m3u8-parser'); var _m3u8Parser2 = _interopRequireDefault(_m3u8Parser); var _videoJs = require('video.js'); var _videoJs2 = _interopRequireDefault(_videoJs); var _masterPlaylistController = require('./master-playlist-controller'); var _config = require('./config'); var _config2 = _interopRequireDefault(_config); var _renditionMixin = require('./rendition-mixin'); var _renditionMixin2 = _interopRequireDefault(_renditionMixin); var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); var _playbackWatcher = require('./playback-watcher'); var _playbackWatcher2 = _interopRequireDefault(_playbackWatcher); var _reloadSourceOnError = require('./reload-source-on-error'); var _reloadSourceOnError2 = _interopRequireDefault(_reloadSourceOnError); var _playlistSelectorsJs = require('./playlist-selectors.js'); var Hls = { PlaylistLoader: _playlistLoader2['default'], Playlist: _playlist2['default'], Decrypter: _aesDecrypter.Decrypter, AsyncStream: _aesDecrypter.AsyncStream, decrypt: _aesDecrypter.decrypt, utils: _binUtils2['default'], STANDARD_PLAYLIST_SELECTOR: _playlistSelectorsJs.lastBandwidthSelector, INITIAL_PLAYLIST_SELECTOR: _playlistSelectorsJs.lowestBitrateCompatibleVariantSelector, comparePlaylistBandwidth: _playlistSelectorsJs.comparePlaylistBandwidth, comparePlaylistResolution: _playlistSelectorsJs.comparePlaylistResolution, xhr: (0, _xhr2['default'])() }; // 0.5 MB/s var INITIAL_BANDWIDTH = 4194304; // Define getter/setters for config properites ['GOAL_BUFFER_LENGTH', 'MAX_GOAL_BUFFER_LENGTH', 'GOAL_BUFFER_LENGTH_RATE', 'BUFFER_LOW_WATER_LINE', 'MAX_BUFFER_LOW_WATER_LINE', 'BUFFER_LOW_WATER_LINE_RATE', 'BANDWIDTH_VARIANCE'].forEach(function (prop) { Object.defineProperty(Hls, prop, { get: function get() { _videoJs2['default'].log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); return _config2['default'][prop]; }, set: function set(value) { _videoJs2['default'].log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); if (typeof value !== 'number' || value < 0) { _videoJs2['default'].log.warn('value of Hls.' + prop + ' must be greater than or equal to 0'); return; } _config2['default'][prop] = value; } }); }); /** * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls. * * @param {QualityLevelList} qualityLevels The QualityLevelList to update. * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. * @function handleHlsMediaChange */ var handleHlsMediaChange = function handleHlsMediaChange(qualityLevels, playlistLoader) { var newPlaylist = playlistLoader.media(); var selectedIndex = -1; for (var i = 0; i < qualityLevels.length; i++) { if (qualityLevels[i].id === newPlaylist.uri) { selectedIndex = i; break; } } qualityLevels.selectedIndex_ = selectedIndex; qualityLevels.trigger({ selectedIndex: selectedIndex, type: 'change' }); }; /** * Adds quality levels to list once playlist metadata is available * * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. * @param {Object} hls Hls object to listen to for media events. * @function handleHlsLoadedMetadata */ var handleHlsLoadedMetadata = function handleHlsLoadedMetadata(qualityLevels, hls) { hls.representations().forEach(function (rep) { qualityLevels.addQualityLevel(rep); }); handleHlsMediaChange(qualityLevels, hls.playlists); }; // HLS is a source handler, not a tech. Make sure attempts to use it // as one do not cause exceptions. Hls.canPlaySource = function () { return _videoJs2['default'].log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); }; /** * Whether the browser has built-in HLS support. */ Hls.supportsNativeHls = (function () { var video = _globalDocument2['default'].createElement('video'); // native HLS is definitely not supported if HTML5 video isn't if (!_videoJs2['default'].getTech('Html5').isSupported()) { return false; } // HLS manifests can go by many mime-types var canPlay = [ // Apple santioned 'application/vnd.apple.mpegurl', // Apple sanctioned for backwards compatibility 'audio/mpegurl', // Very common 'audio/x-mpegurl', // Very common 'application/x-mpegurl', // Included for completeness 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; return canPlay.some(function (canItPlay) { return (/maybe|probably/i.test(video.canPlayType(canItPlay)) ); }); })(); /** * HLS is a source handler, not a tech. Make sure attempts to use it * as one do not cause exceptions. */ Hls.isSupported = function () { return _videoJs2['default'].log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); }; var Component = _videoJs2['default'].getComponent('Component'); /** * The Hls Handler object, where we orchestrate all of the parts * of HLS to interact with video.js * * @class HlsHandler * @extends videojs.Component * @param {Object} source the soruce object * @param {Tech} tech the parent tech object * @param {Object} options optional and required options */ var HlsHandler = (function (_Component) { _inherits(HlsHandler, _Component); function HlsHandler(source, tech, options) { var _this = this; _classCallCheck(this, HlsHandler); _get(Object.getPrototypeOf(HlsHandler.prototype), 'constructor', this).call(this, tech, options.hls); // tech.player() is deprecated but setup a reference to HLS for // backwards-compatibility if (tech.options_ && tech.options_.playerId) { var _player = (0, _videoJs2['default'])(tech.options_.playerId); if (!_player.hasOwnProperty('hls')) { Object.defineProperty(_player, 'hls', { get: function get() { _videoJs2['default'].log.warn('player.hls is deprecated. Use player.tech_.hls instead.'); tech.trigger({ type: 'usage', name: 'hls-player-access' }); return _this; } }); } } this.tech_ = tech; this.source_ = source; this.stats = {}; this.ignoreNextSeekingEvent_ = false; this.setOptions_(); // overriding native HLS only works if audio tracks have been emulated // error early if we're misconfigured: if (this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { throw new Error('Overriding native HLS requires emulated tracks. ' + 'See https://git.io/vMpjB'); } // listen for fullscreenchange events for this player so that we // can adjust our quality selection quickly this.on(_globalDocument2['default'], ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], function (event) { var fullscreenElement = _globalDocument2['default'].fullscreenElement || _globalDocument2['default'].webkitFullscreenElement || _globalDocument2['default'].mozFullScreenElement || _globalDocument2['default'].msFullscreenElement; if (fullscreenElement && fullscreenElement.contains(_this.tech_.el())) { _this.masterPlaylistController_.fastQualityChange_(); } }); this.on(this.tech_, 'seeking', function () { if (this.ignoreNextSeekingEvent_) { this.ignoreNextSeekingEvent_ = false; return; } this.setCurrentTime(this.tech_.currentTime()); }); this.on(this.tech_, 'error', function () { if (this.masterPlaylistController_) { this.masterPlaylistController_.pauseLoading(); } }); this.on(this.tech_, 'play', this.play); } /** * The Source Handler object, which informs video.js what additional * MIME types are supported and sets up playback. It is registered * automatically to the appropriate tech based on the capabilities of * the browser it is running in. It is not necessary to use or modify * this object in normal usage. */ _createClass(HlsHandler, [{ key: 'setOptions_', value: function setOptions_() { var _this2 = this; // defaults this.options_.withCredentials = this.options_.withCredentials || false; if (typeof this.options_.blacklistDuration !== 'number') { this.options_.blacklistDuration = 5 * 60; } // start playlist selection at a reasonable bandwidth for // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s) if (typeof this.options_.bandwidth !== 'number') { this.options_.bandwidth = INITIAL_BANDWIDTH; } // If the bandwidth number is unchanged from the initial setting // then this takes precedence over the enableLowInitialPlaylist option this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === INITIAL_BANDWIDTH; // grab options passed to player.src ['withCredentials', 'bandwidth', 'handleManifestRedirects'].forEach(function (option) { if (typeof _this2.source_[option] !== 'undefined') { _this2.options_[option] = _this2.source_[option]; } }); this.bandwidth = this.options_.bandwidth; } /** * called when player.src gets called, handle a new source * * @param {Object} src the source object to handle */ }, { key: 'src', value: function src(_src) { var _this3 = this; // do nothing if the src is falsey if (!_src) { return; } this.setOptions_(); // add master playlist controller options this.options_.url = this.source_.src; this.options_.tech = this.tech_; this.options_.externHls = Hls; this.masterPlaylistController_ = new _masterPlaylistController.MasterPlaylistController(this.options_); this.playbackWatcher_ = new _playbackWatcher2['default'](_videoJs2['default'].mergeOptions(this.options_, { seekable: function seekable() { return _this3.seekable(); } })); this.masterPlaylistController_.on('error', function () { var player = _videoJs2['default'].players[_this3.tech_.options_.playerId]; player.error(_this3.masterPlaylistController_.error); }); // `this` in selectPlaylist should be the HlsHandler for backwards // compatibility with < v2 this.masterPlaylistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls.STANDARD_PLAYLIST_SELECTOR.bind(this); this.masterPlaylistController_.selectInitialPlaylist = Hls.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; this.mediaSource = this.masterPlaylistController_.mediaSource; // Proxy assignment of some properties to the master playlist // controller. Using a custom property for backwards compatibility // with < v2 Object.defineProperties(this, { selectPlaylist: { get: function get() { return this.masterPlaylistController_.selectPlaylist; }, set: function set(selectPlaylist) { this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this); } }, throughput: { get: function get() { return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate; }, set: function set(throughput) { this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value // for the cumulative average this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1; } }, bandwidth: { get: function get() { return this.masterPlaylistController_.mainSegmentLoader_.bandwidth; }, set: function set(bandwidth) { this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter // `count` is set to zero that current value of `rate` isn't included // in the cumulative average this.masterPlaylistController_.mainSegmentLoader_.throughput = { rate: 0, count: 0 }; } }, /** * `systemBandwidth` is a combination of two serial processes bit-rates. The first * is the network bitrate provided by `bandwidth` and the second is the bitrate of * the entire process after that - decryption, transmuxing, and appending - provided * by `throughput`. * * Since the two process are serial, the overall system bandwidth is given by: * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) */ systemBandwidth: { get: function get() { var invBandwidth = 1 / (this.bandwidth || 1); var invThroughput = undefined; if (this.throughput > 0) { invThroughput = 1 / this.throughput; } else { invThroughput = 0; } var systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); return systemBitrate; }, set: function set() { _videoJs2['default'].log.error('The "systemBandwidth" property is read-only'); } } }); Object.defineProperties(this.stats, { bandwidth: { get: function get() { return _this3.bandwidth || 0; }, enumerable: true }, mediaRequests: { get: function get() { return _this3.masterPlaylistController_.mediaRequests_() || 0; }, enumerable: true }, mediaRequestsAborted: { get: function get() { return _this3.masterPlaylistController_.mediaRequestsAborted_() || 0; }, enumerable: true }, mediaRequestsTimedout: { get: function get() { return _this3.masterPlaylistController_.mediaRequestsTimedout_() || 0; }, enumerable: true }, mediaRequestsErrored: { get: function get() { return _this3.masterPlaylistController_.mediaRequestsErrored_() || 0; }, enumerable: true }, mediaTransferDuration: { get: function get() { return _this3.masterPlaylistController_.mediaTransferDuration_() || 0; }, enumerable: true }, mediaBytesTransferred: { get: function get() { return _this3.masterPlaylistController_.mediaBytesTransferred_() || 0; }, enumerable: true }, mediaSecondsLoaded: { get: function get() { return _this3.masterPlaylistController_.mediaSecondsLoaded_() || 0; }, enumerable: true } }); this.tech_.one('canplay', this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_)); this.masterPlaylistController_.on('selectedinitialmedia', function () { // Add the manual rendition mix-in to HlsHandler (0, _renditionMixin2['default'])(_this3); }); // the bandwidth of the primary segment loader is our best // estimate of overall bandwidth this.on(this.masterPlaylistController_, 'progress', function () { this.tech_.trigger('progress'); }); // In the live case, we need to ignore the very first `seeking` event since // that will be the result of the seek-to-live behavior this.on(this.masterPlaylistController_, 'firstplay', function () { this.ignoreNextSeekingEvent_ = true; }); this.tech_.ready(function () { return _this3.setupQualityLevels_(); }); // do nothing if the tech has been disposed already // this can occur if someone sets the src in player.ready(), for instance if (!this.tech_.el()) { return; } this.tech_.src(_videoJs2['default'].URL.createObjectURL(this.masterPlaylistController_.mediaSource)); } /** * Initializes the quality levels and sets listeners to update them. * * @method setupQualityLevels_ * @private */ }, { key: 'setupQualityLevels_', value: function setupQualityLevels_() { var _this4 = this; var player = _videoJs2['default'].players[this.tech_.options_.playerId]; if (player && player.qualityLevels) { this.qualityLevels_ = player.qualityLevels(); this.masterPlaylistController_.on('selectedinitialmedia', function () { handleHlsLoadedMetadata(_this4.qualityLevels_, _this4); }); this.playlists.on('mediachange', function () { handleHlsMediaChange(_this4.qualityLevels_, _this4.playlists); }); } } /** * Begin playing the video. */ }, { key: 'play', value: function play() { this.masterPlaylistController_.play(); } /** * a wrapper around the function in MasterPlaylistController */ }, { key: 'setCurrentTime', value: function setCurrentTime(currentTime) { this.masterPlaylistController_.setCurrentTime(currentTime); } /** * a wrapper around the function in MasterPlaylistController */ }, { key: 'duration', value: function duration() { return this.masterPlaylistController_.duration(); } /** * a wrapper around the function in MasterPlaylistController */ }, { key: 'seekable', value: function seekable() { return this.masterPlaylistController_.seekable(); } /** * Abort all outstanding work and cleanup. */ }, { key: 'dispose', value: function dispose() { if (this.playbackWatcher_) { this.playbackWatcher_.dispose(); } if (this.masterPlaylistController_) { this.masterPlaylistController_.dispose(); } if (this.qualityLevels_) { this.qualityLevels_.dispose(); } _get(Object.getPrototypeOf(HlsHandler.prototype), 'dispose', this).call(this); } }]); return HlsHandler; })(Component); var HlsSourceHandler = function HlsSourceHandler(mode) { return { canHandleSource: function canHandleSource(srcObj) { var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; var localOptions = _videoJs2['default'].mergeOptions(_videoJs2['default'].options, options); // this forces video.js to skip this tech/mode if its not the one we have been // overriden to use, by returing that we cannot handle the source. if (localOptions.hls && localOptions.hls.mode && localOptions.hls.mode !== mode) { return false; } return HlsSourceHandler.canPlayType(srcObj.type, localOptions); }, handleSource: function handleSource(source, tech) { var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; var localOptions = _videoJs2['default'].mergeOptions(_videoJs2['default'].options, options, { hls: { mode: mode } }); if (mode === 'flash') { // We need to trigger this asynchronously to give others the chance // to bind to the event when a source is set at player creation tech.setTimeout(function () { tech.trigger('loadstart'); }, 1); } tech.hls = new HlsHandler(source, tech, localOptions); tech.hls.xhr = (0, _xhr2['default'])(); tech.hls.src(source.src); return tech.hls; }, canPlayType: function canPlayType(type) { var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; var localOptions = _videoJs2['default'].mergeOptions(_videoJs2['default'].options, options); if (HlsSourceHandler.canPlayType(type, localOptions)) { return 'maybe'; } return ''; } }; }; HlsSourceHandler.canPlayType = function (type, options) { // No support for IE 10 or below if (_videoJs2['default'].browser.IE_VERSION && _videoJs2['default'].browser.IE_VERSION <= 10) { return false; } var mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; // favor native HLS support if it's available if (!options.hls.overrideNative && Hls.supportsNativeHls) { return false; } return mpegurlRE.test(type); }; if (typeof _videoJs2['default'].MediaSource === 'undefined' || typeof _videoJs2['default'].URL === 'undefined') { _videoJs2['default'].MediaSource = _videojsContribMediaSources.MediaSource; _videoJs2['default'].URL = _videojsContribMediaSources.URL; } var flashTech = _videoJs2['default'].getTech('Flash'); // register source handlers with the appropriate techs if (_videojsContribMediaSources.MediaSource.supportsNativeMediaSources()) { _videoJs2['default'].getTech('Html5').registerSourceHandler(HlsSourceHandler('html5'), 0); } if (_globalWindow2['default'].Uint8Array && flashTech) { flashTech.registerSourceHandler(HlsSourceHandler('flash')); } _videoJs2['default'].HlsHandler = HlsHandler; _videoJs2['default'].HlsSourceHandler = HlsSourceHandler; _videoJs2['default'].Hls = Hls; if (!_videoJs2['default'].use) { _videoJs2['default'].registerComponent('Hls', Hls); } _videoJs2['default'].m3u8 = _m3u8Parser2['default']; _videoJs2['default'].options.hls = _videoJs2['default'].options.hls || {}; if (_videoJs2['default'].registerPlugin) { _videoJs2['default'].registerPlugin('reloadSourceOnError', _reloadSourceOnError2['default']); } else { _videoJs2['default'].plugin('reloadSourceOnError', _reloadSourceOnError2['default']); } module.exports = { Hls: Hls, HlsHandler: HlsHandler, HlsSourceHandler: HlsSourceHandler };