UNPKG

videojs-contrib-hls

Version:

Play back HLS with video.js, even where it's not natively supported

1,459 lines (1,228 loc) 519 kB
/** * videojs-contrib-hls * @version 3.6.2 * @copyright 2016 Brightcove, Inc * @license Apache-2.0 */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.videojsContribHls = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /** * @file ad-cue-tags.js */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })(); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _globalWindow = require('global/window'); var _globalWindow2 = _interopRequireDefault(_globalWindow); /** * Searches for an ad cue that overlaps with the given mediaTime */ var findAdCue = function findAdCue(track, mediaTime) { var cues = track.cues; for (var i = 0; i < cues.length; i++) { var cue = cues[i]; if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { return cue; } } return null; }; var updateAdCues = function updateAdCues(media, track) { var offset = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; if (!media.segments) { return; } var mediaTime = offset; var cue = undefined; for (var i = 0; i < media.segments.length; i++) { var segment = media.segments[i]; if (!cue) { // Since the cues will span for at least the segment duration, adding a fudge // factor of half segment duration will prevent duplicate cues from being // created when timing info is not exact (e.g. cue start time initialized // at 10.006677, but next call mediaTime is 10.003332 ) cue = findAdCue(track, mediaTime + segment.duration / 2); } if (cue) { if ('cueIn' in segment) { // Found a CUE-IN so end the cue cue.endTime = mediaTime; cue.adEndTime = mediaTime; mediaTime += segment.duration; cue = null; continue; } if (mediaTime < cue.endTime) { // Already processed this mediaTime for this cue mediaTime += segment.duration; continue; } // otherwise extend cue until a CUE-IN is found cue.endTime += segment.duration; } else { if ('cueOut' in segment) { cue = new _globalWindow2['default'].VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); cue.adStartTime = mediaTime; // Assumes tag format to be // #EXT-X-CUE-OUT:30 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); track.addCue(cue); } if ('cueOutCont' in segment) { // Entered into the middle of an ad cue var adOffset = undefined; var adTotal = undefined; // Assumes tag formate to be // #EXT-X-CUE-OUT-CONT:10/30 var _segment$cueOutCont$split$map = segment.cueOutCont.split('/').map(parseFloat); var _segment$cueOutCont$split$map2 = _slicedToArray(_segment$cueOutCont$split$map, 2); adOffset = _segment$cueOutCont$split$map2[0]; adTotal = _segment$cueOutCont$split$map2[1]; cue = new _globalWindow2['default'].VTTCue(mediaTime, mediaTime + segment.duration, ''); cue.adStartTime = mediaTime - adOffset; cue.adEndTime = cue.adStartTime + adTotal; track.addCue(cue); } } mediaTime += segment.duration; } }; exports['default'] = { updateAdCues: updateAdCues, findAdCue: findAdCue }; module.exports = exports['default']; },{"global/window":25}],2:[function(require,module,exports){ /** * @file bin-utils.js */ /** * convert a TimeRange to text * * @param {TimeRange} range the timerange to use for conversion * @param {Number} i the iterator on the range to convert */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var textRange = function textRange(range, i) { return range.start(i) + '-' + range.end(i); }; /** * format a number as hex string * * @param {Number} e The number * @param {Number} i the iterator */ var formatHexString = function formatHexString(e, i) { var value = e.toString(16); return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); }; var formatAsciiString = function formatAsciiString(e) { if (e >= 0x20 && e < 0x7e) { return String.fromCharCode(e); } return '.'; }; /** * utils to help dump binary data to the console */ var utils = { hexDump: function hexDump(data) { var bytes = Array.prototype.slice.call(data); var step = 16; var result = ''; var hex = undefined; var ascii = undefined; for (var j = 0; j < bytes.length / step; j++) { hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); result += hex + ' ' + ascii + '\n'; } return result; }, tagDump: function tagDump(tag) { return utils.hexDump(tag.bytes); }, textRanges: function textRanges(ranges) { var result = ''; var i = undefined; for (i = 0; i < ranges.length; i++) { result += textRange(ranges, i) + ' '; } return result; } }; exports['default'] = utils; module.exports = exports['default']; },{}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = { GOAL_BUFFER_LENGTH: 30 }; module.exports = exports["default"]; },{}],4:[function(require,module,exports){ (function (global){ /** * @file gap-skipper.js */ '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; }; })(); 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'); } } var _ranges = require('./ranges'); var _ranges2 = _interopRequireDefault(_ranges); var _videoJs = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null); var _videoJs2 = _interopRequireDefault(_videoJs); // Set of events that reset the gap-skipper logic and clear the timeout var timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; /** * The gap skipper object handles all scenarios * where the player runs into the end of a buffered * region and there is a buffered region ahead. * * It then handles the skipping behavior by setting a * timer to the size (in time) of the gap. This gives * the hls segment fetcher time to close the gap and * resume playing before the timer is triggered and * the gap skipper simply seeks over the gap as a * last resort to resume playback. * * @class GapSkipper */ var GapSkipper = (function () { /** * Represents a GapSKipper object. * @constructor * @param {object} options an object that includes the tech and settings */ function GapSkipper(options) { var _this = this; _classCallCheck(this, GapSkipper); this.tech_ = options.tech; this.consecutiveUpdates = 0; this.lastRecordedTime = null; this.timer_ = null; if (options.debug) { this.logger_ = _videoJs2['default'].log.bind(_videoJs2['default'], 'gap-skipper ->'); } this.logger_('initialize'); var waitingHandler = function waitingHandler() { return _this.waiting_(); }; var timeupdateHandler = function timeupdateHandler() { return _this.timeupdate_(); }; var cancelTimerHandler = function cancelTimerHandler() { return _this.cancelTimer_(); }; this.tech_.on('waiting', waitingHandler); this.tech_.on('timeupdate', timeupdateHandler); this.tech_.on(timerCancelEvents, cancelTimerHandler); // Define the dispose function to clean up our events this.dispose = function () { _this.logger_('dispose'); _this.tech_.off('waiting', waitingHandler); _this.tech_.off('timeupdate', timeupdateHandler); _this.tech_.off(timerCancelEvents, cancelTimerHandler); _this.cancelTimer_(); }; } /** * Handler for `waiting` events from the player * * @private */ _createClass(GapSkipper, [{ key: 'waiting_', value: function waiting_() { if (!this.tech_.seeking()) { this.setTimer_(); } } /** * The purpose of this function is to emulate the "waiting" event on * browsers that do not emit it when they are waiting for more * data to continue playback * * @private */ }, { key: 'timeupdate_', value: function timeupdate_() { if (this.tech_.paused() || this.tech_.seeking()) { return; } var currentTime = this.tech_.currentTime(); if (this.consecutiveUpdates === 5 && currentTime === this.lastRecordedTime) { this.consecutiveUpdates++; this.waiting_(); } else if (currentTime === this.lastRecordedTime) { this.consecutiveUpdates++; } else { this.consecutiveUpdates = 0; this.lastRecordedTime = currentTime; } } /** * Cancels any pending timers and resets the 'timeupdate' mechanism * designed to detect that we are stalled * * @private */ }, { key: 'cancelTimer_', value: function cancelTimer_() { this.consecutiveUpdates = 0; if (this.timer_) { this.logger_('cancelTimer_'); clearTimeout(this.timer_); } this.timer_ = null; } /** * Timer callback. If playback still has not proceeded, then we seek * to the start of the next buffered region. * * @private */ }, { key: 'skipTheGap_', value: function skipTheGap_(scheduledCurrentTime) { var buffered = this.tech_.buffered(); var currentTime = this.tech_.currentTime(); var nextRange = _ranges2['default'].findNextRange(buffered, currentTime); this.consecutiveUpdates = 0; this.timer_ = null; if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { return; } this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played this.tech_.setCurrentTime(nextRange.start(0) + _ranges2['default'].TIME_FUDGE_FACTOR); } }, { key: 'gapFromVideoUnderflow_', value: function gapFromVideoUnderflow_(buffered, currentTime) { // At least in Chrome, if there is a gap in the video buffer, the audio will continue // playing for ~3 seconds after the video gap starts. This is done to account for // video buffer underflow/underrun (note that this is not done when there is audio // buffer underflow/underrun -- in that case the video will stop as soon as it // encounters the gap, as audio stalls are more noticeable/jarring to a user than // video stalls). The player's time will reflect the playthrough of audio, so the // time will appear as if we are in a buffered region, even if we are stuck in a // "gap." // // Example: // video buffer: 0 => 10.1, 10.2 => 20 // audio buffer: 0 => 20 // overall buffer: 0 => 10.1, 10.2 => 20 // current time: 13 // // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, // however, the audio continued playing until it reached ~3 seconds past the gap // (13 seconds), at which point it stops as well. Since current time is past the // gap, findNextRange will return no ranges. // // To check for this issue, we see if there is a gap that starts somewhere within // a 3 second range (3 seconds +/- 1 second) back from our current time. var gaps = _ranges2['default'].findGaps(buffered); for (var i = 0; i < gaps.length; i++) { var start = gaps.start(i); var end = gaps.end(i); // gap is starts no more than 4 seconds back if (currentTime - start < 4 && currentTime - start > 2) { return { start: start, end: end }; } } return null; } /** * Set a timer to skip the unbuffered region. * * @private */ }, { key: 'setTimer_', value: function setTimer_() { var buffered = this.tech_.buffered(); var currentTime = this.tech_.currentTime(); var nextRange = _ranges2['default'].findNextRange(buffered, currentTime); if (this.timer_ !== null) { return; } if (nextRange.length === 0) { // Even if there is no available next range, there is still a possibility we are // stuck in a gap due to video underflow. var gap = this.gapFromVideoUnderflow_(buffered, currentTime); if (gap) { this.logger_('setTimer_:', 'Encountered a gap in video', 'from: ', gap.start, 'to: ', gap.end, 'seeking to current time: ', currentTime); // Even though the video underflowed and was stuck in a gap, the audio overplayed // the gap, leading currentTime into a buffered range. Seeking to currentTime // allows the video to catch up to the audio position without losing any audio // (only suffering ~3 seconds of frozen video and a pause in audio playback). this.tech_.setCurrentTime(currentTime); } return; } var difference = nextRange.start(0) - currentTime; this.logger_('setTimer_:', 'stopped at:', currentTime, 'setting timer for:', difference, 'seeking to:', nextRange.start(0)); this.timer_ = setTimeout(this.skipTheGap_.bind(this), difference * 1000, currentTime); } /** * A debugging logger noop that is set to console.log only if debugging * is enabled globally * * @private */ }, { key: 'logger_', value: function logger_() {} }]); return GapSkipper; })(); exports['default'] = GapSkipper; module.exports = exports['default']; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./ranges":8}],5:[function(require,module,exports){ (function (global){ /** * @file master-playlist-controller.js */ '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 _get = function get(_x3, _x4, _x5) { var _again = true; _function: while (_again) { var object = _x3, property = _x4, receiver = _x5; _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 { _x3 = parent; _x4 = property; _x5 = 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 _playlistLoader = require('./playlist-loader'); var _playlistLoader2 = _interopRequireDefault(_playlistLoader); var _segmentLoader = require('./segment-loader'); var _segmentLoader2 = _interopRequireDefault(_segmentLoader); var _ranges = require('./ranges'); var _ranges2 = _interopRequireDefault(_ranges); var _videoJs = (typeof window !== "undefined" ? window['videojs'] : typeof global !== "undefined" ? global['videojs'] : null); var _videoJs2 = _interopRequireDefault(_videoJs); var _adCueTags = require('./ad-cue-tags'); var _adCueTags2 = _interopRequireDefault(_adCueTags); // 5 minute blacklist var BLACKLIST_DURATION = 5 * 60 * 1000; var Hls = undefined; /** * determine if an object a is differnt from * and object b. both only having one dimensional * properties * * @param {Object} a object one * @param {Object} b object two * @return {Boolean} if the object has changed or not */ var objectChanged = function objectChanged(a, b) { if (typeof a !== typeof b) { return true; } // if we have a different number of elements // something has changed if (Object.keys(a).length !== Object.keys(b).length) { return true; } for (var prop in a) { if (a[prop] !== b[prop]) { return true; } } return false; }; /** * Parses a codec string to retrieve the number of codecs specified, * the video codec and object type indicator, and the audio profile. * * @private */ var parseCodecs = function parseCodecs(codecs) { var result = { codecCount: 0, videoCodec: null, videoObjectTypeIndicator: null, audioProfile: null }; var parsed = undefined; result.codecCount = codecs.split(',').length; result.codecCount = result.codecCount || 2; // parse the video codec parsed = /(^|\s|,)+(avc1)([^ ,]*)/i.exec(codecs); if (parsed) { result.videoCodec = parsed[2]; result.videoObjectTypeIndicator = parsed[3]; } // parse the last field of the audio codec result.audioProfile = /(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i.exec(codecs); result.audioProfile = result.audioProfile && result.audioProfile[2]; return result; }; /** * Calculates the MIME type strings for a working configuration of * SourceBuffers to play variant streams in a master playlist. If * there is no possible working configuration, an empty array will be * returned. * * @param master {Object} the m3u8 object for the master playlist * @param media {Object} the m3u8 object for the variant playlist * @return {Array} the MIME type strings. If the array has more than * one entry, the first element should be applied to the video * SourceBuffer and the second to the audio SourceBuffer. * * @private */ var mimeTypesForPlaylist_ = function mimeTypesForPlaylist_(master, media) { var container = 'mp2t'; var codecs = { videoCodec: 'avc1', videoObjectTypeIndicator: '.4d400d', audioProfile: '2' }; var audioGroup = []; var mediaAttributes = undefined; var previousGroup = null; if (!media) { // not enough information, return an error return []; } // An initialization segment means the media playlists is an iframe // playlist or is using the mp4 container. We don't currently // support iframe playlists, so assume this is signalling mp4 // fragments. // the existence check for segments can be removed once // https://github.com/videojs/m3u8-parser/issues/8 is closed if (media.segments && media.segments.length && media.segments[0].map) { container = 'mp4'; } // if the codecs were explicitly specified, use them instead of the // defaults mediaAttributes = media.attributes || {}; if (mediaAttributes.CODECS) { (function () { var parsedCodecs = parseCodecs(mediaAttributes.CODECS); Object.keys(parsedCodecs).forEach(function (key) { codecs[key] = parsedCodecs[key] || codecs[key]; }); })(); } if (master.mediaGroups.AUDIO) { audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; } // if audio could be muxed or unmuxed, use mime types appropriate // for both scenarios for (var groupId in audioGroup) { if (previousGroup && !!audioGroup[groupId].uri !== !!previousGroup.uri) { // one source buffer with muxed video and audio and another for // the alternate audio return ['video/' + container + '; codecs="' + codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"', 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"']; } previousGroup = audioGroup[groupId]; } // if all video and audio is unmuxed, use two single-codec mime // types if (previousGroup && previousGroup.uri) { return ['video/' + container + '; codecs="' + codecs.videoCodec + codecs.videoObjectTypeIndicator + '"', 'audio/' + container + '; codecs="mp4a.40.' + codecs.audioProfile + '"']; } // all video and audio are muxed, use a dual-codec mime type return ['video/' + container + '; codecs="' + codecs.videoCodec + codecs.videoObjectTypeIndicator + ', mp4a.40.' + codecs.audioProfile + '"']; }; exports.mimeTypesForPlaylist_ = mimeTypesForPlaylist_; /** * the master playlist controller controller all interactons * between playlists and segmentloaders. At this time this mainly * involves a master playlist and a series of audio playlists * if they are available * * @class MasterPlaylistController * @extends videojs.EventTarget */ var MasterPlaylistController = (function (_videojs$EventTarget) { _inherits(MasterPlaylistController, _videojs$EventTarget); function MasterPlaylistController(options) { var _this = this; _classCallCheck(this, MasterPlaylistController); _get(Object.getPrototypeOf(MasterPlaylistController.prototype), 'constructor', this).call(this); var url = options.url; var withCredentials = options.withCredentials; var mode = options.mode; var tech = options.tech; var bandwidth = options.bandwidth; var externHls = options.externHls; var useCueTags = options.useCueTags; if (!url) { throw new Error('A non-empty playlist URL is required'); } Hls = externHls; this.withCredentials = withCredentials; this.tech_ = tech; this.hls_ = tech.hls; this.mode_ = mode; this.useCueTags_ = useCueTags; if (this.useCueTags_) { this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; this.tech_.textTracks().addTrack_(this.cueTagsTrack_); } this.audioTracks_ = []; this.requestOptions_ = { withCredentials: this.withCredentials, timeout: null }; this.audioGroups_ = {}; this.mediaSource = new _videoJs2['default'].MediaSource({ mode: mode }); this.audioinfo_ = null; this.mediaSource.on('audioinfo', this.handleAudioinfoUpdate_.bind(this)); // load the media source into the player this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this)); var segmentLoaderOptions = { hls: this.hls_, mediaSource: this.mediaSource, currentTime: this.tech_.currentTime.bind(this.tech_), seekable: function seekable() { return _this.seekable(); }, seeking: function seeking() { return _this.tech_.seeking(); }, setCurrentTime: function setCurrentTime(a) { return _this.tech_.setCurrentTime(a); }, hasPlayed: function hasPlayed() { return _this.tech_.played().length !== 0; }, bandwidth: bandwidth }; // setup playlist loaders this.masterPlaylistLoader_ = new _playlistLoader2['default'](url, this.hls_, this.withCredentials); this.setupMasterPlaylistLoaderListeners_(); this.audioPlaylistLoader_ = null; // setup segment loaders // combined audio/video or just video when alternate audio track is selected this.mainSegmentLoader_ = new _segmentLoader2['default'](segmentLoaderOptions); // alternate audio track this.audioSegmentLoader_ = new _segmentLoader2['default'](segmentLoaderOptions); this.setupSegmentLoaderListeners_(); this.masterPlaylistLoader_.start(); } /** * Register event handlers on the master playlist loader. A helper * function for construction time. * * @private */ _createClass(MasterPlaylistController, [{ key: 'setupMasterPlaylistLoaderListeners_', value: function setupMasterPlaylistLoaderListeners_() { var _this2 = this; this.masterPlaylistLoader_.on('loadedmetadata', function () { var media = _this2.masterPlaylistLoader_.media(); var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000; _this2.requestOptions_.timeout = requestTimeout; // if this isn't a live video and preload permits, start // downloading segments if (media.endList && _this2.tech_.preload() !== 'none') { _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); _this2.mainSegmentLoader_.expired(_this2.masterPlaylistLoader_.expired_); _this2.mainSegmentLoader_.load(); } try { _this2.setupSourceBuffers_(); } catch (e) { _videoJs2['default'].log.warn('Failed to create SourceBuffers', e); return _this2.mediaSource.endOfStream('decode'); } _this2.setupFirstPlay(); _this2.fillAudioTracks_(); _this2.setupAudio(); _this2.trigger('audioupdate'); _this2.trigger('selectedinitialmedia'); }); this.masterPlaylistLoader_.on('loadedplaylist', function () { var updatedPlaylist = _this2.masterPlaylistLoader_.media(); var seekable = undefined; if (!updatedPlaylist) { // select the initial variant _this2.initialMedia_ = _this2.selectPlaylist(); _this2.masterPlaylistLoader_.media(_this2.initialMedia_); return; } if (_this2.useCueTags_) { _this2.updateAdCues_(updatedPlaylist, _this2.masterPlaylistLoader_.expired_); } // TODO: Create a new event on the PlaylistLoader that signals // that the segments have changed in some way and use that to // update the SegmentLoader instead of doing it twice here and // on `mediachange` _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_); _this2.mainSegmentLoader_.expired(_this2.masterPlaylistLoader_.expired_); _this2.updateDuration(); // update seekable seekable = _this2.seekable(); if (!updatedPlaylist.endList && seekable.length !== 0) { _this2.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0)); } }); this.masterPlaylistLoader_.on('error', function () { _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error); }); this.masterPlaylistLoader_.on('mediachanging', function () { _this2.mainSegmentLoader_.abort(); _this2.mainSegmentLoader_.pause(); }); this.masterPlaylistLoader_.on('mediachange', function () { var media = _this2.masterPlaylistLoader_.media(); var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000; var activeAudioGroup = undefined; var activeTrack = undefined; // If we don't have any more available playlists, we don't want to // timeout the request. if (_this2.masterPlaylistLoader_.isLowestEnabledRendition_()) { _this2.requestOptions_.timeout = 0; } else { _this2.requestOptions_.timeout = requestTimeout; } // TODO: Create a new event on the PlaylistLoader that signals // that the segments have changed in some way and use that to // update the SegmentLoader instead of doing it twice here and // on `loadedplaylist` _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); _this2.mainSegmentLoader_.expired(_this2.masterPlaylistLoader_.expired_); _this2.mainSegmentLoader_.load(); // if the audio group has changed, a new audio track has to be // enabled activeAudioGroup = _this2.activeAudioGroup(); activeTrack = activeAudioGroup.filter(function (track) { return track.enabled; })[0]; if (!activeTrack) { _this2.setupAudio(); _this2.trigger('audioupdate'); } _this2.tech_.trigger({ type: 'mediachange', bubbles: true }); }); } /** * Register event handlers on the segment loaders. A helper function * for construction time. * * @private */ }, { key: 'setupSegmentLoaderListeners_', value: function setupSegmentLoaderListeners_() { var _this3 = this; this.mainSegmentLoader_.on('progress', function () { // figure out what stream the next segment should be downloaded from // with the updated bandwidth information _this3.masterPlaylistLoader_.media(_this3.selectPlaylist()); _this3.trigger('progress'); }); this.mainSegmentLoader_.on('error', function () { _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error()); }); this.audioSegmentLoader_.on('error', function () { _videoJs2['default'].log.warn('Problem encountered with the current alternate audio track' + '. Switching back to default.'); _this3.audioSegmentLoader_.abort(); _this3.audioPlaylistLoader_ = null; _this3.setupAudio(); }); } }, { key: 'handleAudioinfoUpdate_', value: function handleAudioinfoUpdate_(event) { if (Hls.supportsAudioInfoChange_() || !this.audioInfo_ || !objectChanged(this.audioInfo_, event.info)) { this.audioInfo_ = event.info; return; } var error = 'had different audio properties (channels, sample rate, etc.) ' + 'or changed in some other way. This behavior is currently ' + 'unsupported in Firefox 48 and below due to an issue: \n\n' + 'https://bugzilla.mozilla.org/show_bug.cgi?id=1247138\n\n'; var enabledIndex = this.activeAudioGroup().map(function (track) { return track.enabled; }).indexOf(true); var enabledTrack = this.activeAudioGroup()[enabledIndex]; var defaultTrack = this.activeAudioGroup().filter(function (track) { return track.properties_ && track.properties_['default']; })[0]; // they did not switch audiotracks // blacklist the current playlist if (!this.audioPlaylistLoader_) { error = 'The rendition that we tried to switch to ' + error + 'Unfortunately that means we will have to blacklist ' + 'the current playlist and switch to another. Sorry!'; this.blacklistCurrentPlaylist(); } else { error = 'The audio track \'' + enabledTrack.label + '\' that we tried to ' + ('switch to ' + error + ' Unfortunately this means we will have to ') + ('return you to the main track \'' + defaultTrack.label + '\'. Sorry!'); defaultTrack.enabled = true; this.activeAudioGroup().splice(enabledIndex, 1); this.trigger('audioupdate'); } _videoJs2['default'].log.warn(error); this.setupAudio(); } /** * get the total number of media requests from the `audiosegmentloader_` * and the `mainSegmentLoader_` * * @private */ }, { key: 'mediaRequests_', value: function mediaRequests_() { return this.audioSegmentLoader_.mediaRequests + this.mainSegmentLoader_.mediaRequests; } /** * get the total time that media requests have spent trnasfering * from the `audiosegmentloader_` and the `mainSegmentLoader_` * * @private */ }, { key: 'mediaTransferDuration_', value: function mediaTransferDuration_() { return this.audioSegmentLoader_.mediaTransferDuration + this.mainSegmentLoader_.mediaTransferDuration; } /** * get the total number of bytes transfered during media requests * from the `audiosegmentloader_` and the `mainSegmentLoader_` * * @private */ }, { key: 'mediaBytesTransferred_', value: function mediaBytesTransferred_() { return this.audioSegmentLoader_.mediaBytesTransferred + this.mainSegmentLoader_.mediaBytesTransferred; } /** * fill our internal list of HlsAudioTracks with data from * the master playlist or use a default * * @private */ }, { key: 'fillAudioTracks_', value: function fillAudioTracks_() { var master = this.master(); var mediaGroups = master.mediaGroups || {}; // force a default if we have none or we are not // in html5 mode (the only mode to support more than one // audio track) if (!mediaGroups || !mediaGroups.AUDIO || Object.keys(mediaGroups.AUDIO).length === 0 || this.mode_ !== 'html5') { // "main" audio group, track name "default" mediaGroups.AUDIO = { main: { 'default': { 'default': true } } }; } for (var mediaGroup in mediaGroups.AUDIO) { if (!this.audioGroups_[mediaGroup]) { this.audioGroups_[mediaGroup] = []; } for (var label in mediaGroups.AUDIO[mediaGroup]) { var properties = mediaGroups.AUDIO[mediaGroup][label]; var track = new _videoJs2['default'].AudioTrack({ id: label, kind: properties['default'] ? 'main' : 'alternative', enabled: false, language: properties.language, label: label }); track.properties_ = properties; this.audioGroups_[mediaGroup].push(track); } } // enable the default active track (this.activeAudioGroup().filter(function (audioTrack) { return audioTrack.properties_['default']; })[0] || this.activeAudioGroup()[0]).enabled = true; } /** * Call load on our SegmentLoaders */ }, { key: 'load', value: function load() { this.mainSegmentLoader_.load(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.load(); } } /** * Returns the audio group for the currently active primary * media playlist. */ }, { key: 'activeAudioGroup', value: function activeAudioGroup() { var videoPlaylist = this.masterPlaylistLoader_.media(); var result = undefined; if (videoPlaylist.attributes && videoPlaylist.attributes.AUDIO) { result = this.audioGroups_[videoPlaylist.attributes.AUDIO]; } return result || this.audioGroups_.main; } /** * Determine the correct audio rendition based on the active * AudioTrack and initialize a PlaylistLoader and SegmentLoader if * necessary. This method is called once automatically before * playback begins to enable the default audio track and should be * invoked again if the track is changed. */ }, { key: 'setupAudio', value: function setupAudio() { var _this4 = this; // determine whether seperate loaders are required for the audio // rendition var audioGroup = this.activeAudioGroup(); var track = audioGroup.filter(function (audioTrack) { return audioTrack.enabled; })[0]; if (!track) { track = audioGroup.filter(function (audioTrack) { return audioTrack.properties_['default']; })[0] || audioGroup[0]; track.enabled = true; } // stop playlist and segment loading for audio if (this.audioPlaylistLoader_) { this.audioPlaylistLoader_.dispose(); this.audioPlaylistLoader_ = null; } this.audioSegmentLoader_.pause(); this.audioSegmentLoader_.clearBuffer(); if (!track.properties_.resolvedUri) { return; } // startup playlist and segment loaders for the enabled audio // track this.audioPlaylistLoader_ = new _playlistLoader2['default'](track.properties_.resolvedUri, this.hls_, this.withCredentials); this.audioPlaylistLoader_.start(); this.audioPlaylistLoader_.on('loadedmetadata', function () { var audioPlaylist = _this4.audioPlaylistLoader_.media(); _this4.audioSegmentLoader_.playlist(audioPlaylist, _this4.requestOptions_); // if the video is already playing, or if this isn't a live video and preload // permits, start downloading segments if (!_this4.tech_.paused() || audioPlaylist.endList && _this4.tech_.preload() !== 'none') { _this4.audioSegmentLoader_.load(); } if (!audioPlaylist.endList) { // trigger the playlist loader to start "expired time"-tracking _this4.audioPlaylistLoader_.trigger('firstplay'); } }); this.audioPlaylistLoader_.on('loadedplaylist', function () { var updatedPlaylist = undefined; if (_this4.audioPlaylistLoader_) { updatedPlaylist = _this4.audioPlaylistLoader_.media(); } if (!updatedPlaylist) { // only one playlist to select _this4.audioPlaylistLoader_.media(_this4.audioPlaylistLoader_.playlists.master.playlists[0]); return; } _this4.audioSegmentLoader_.playlist(updatedPlaylist, _this4.requestOptions_); }); this.audioPlaylistLoader_.on('error', function () { _videoJs2['default'].log.warn('Problem encountered loading the alternate audio track' + '. Switching back to default.'); _this4.audioSegmentLoader_.abort(); _this4.setupAudio(); }); } /** * Re-tune playback quality level for the current player * conditions. This method may perform destructive actions, like * removing already buffered content, to readjust the currently * active playlist quickly. * * @private */ }, { key: 'fastQualityChange_', value: function fastQualityChange_() { var media = this.selectPlaylist(); if (media !== this.masterPlaylistLoader_.media()) { this.masterPlaylistLoader_.media(media); this.mainSegmentLoader_.sourceUpdater_.remove(this.tech_.currentTime() + 5, Infinity); } } /** * Begin playback. */ }, { key: 'play', value: function play() { if (this.setupFirstPlay()) { return; } if (this.tech_.ended()) { this.tech_.setCurrentTime(0); } this.load(); // if the viewer has paused and we fell out of the live window, // seek forward to the earliest available position if (this.tech_.duration() === Infinity) { if (this.tech_.currentTime() < this.tech_.seekable().start(0)) { return this.tech_.setCurrentTime(this.tech_.seekable().start(0)); } } } /** * Seek to the latest media position if this is a live video and the * player and video are loaded and initialized. */ }, { key: 'setupFirstPlay', value: function setupFirstPlay() { var seekable = undefined; var media = this.masterPlaylistLoader_.media(); // check that everything is ready to begin buffering // 1) the active media playlist is available if (media && // 2) the video is a live stream !media.endList && // 3) the player is not paused !this.tech_.paused() && // 4) the player has not started playing !this.hasPlayed_) { // trigger the playlist loader to start "expired time"-tracking this.masterPlaylistLoader_.trigger('firstplay'); this.hasPlayed_ = true; // seek to the latest media position for live videos seekable = this.seekable(); if (seekable.length) { this.tech_.setCurrentTime(seekable.end(0)); } // now that we seeked to the current time, load the segment this.load(); return true; } return false; } /** * handle the sourceopen event on the MediaSource * * @private */ }, { key: 'handleSourceOpen_', value: function handleSourceOpen_() { // Only attempt to create the source buffer if none already exist. // handleSourceOpen is also called when we are "re-opening" a source buffer // after `endOfStream` has been called (in response to a seek for instance) try { this.setupSourceBuffers_(); } catch (e) { _videoJs2['default'].log.warn('Failed to create Source Buffers', e); return this.mediaSource.endOfStream('decode'); } // if autoplay is enabled, begin playback. This is duplicative of // code in video.js but is required because play() must be invoked // *after* the media source has opened. if (this.tech_.autoplay()) { this.tech_.play(); } this.trigger('sourceopen'); } /** * Blacklists a playlist when an error occurs for a set amount of time * making it unavailable for selection by the rendition selection algorithm * and then forces a new playlist (rendition) selection. * * @param {Object=} error an optional error that may include the playlist * to blacklist */ }, { key: 'blacklistCurrentPlaylist', value: function blacklistCurrentPlaylist() { var error = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; var currentPlaylist = undefined; var nextPlaylist = undefined; // If the `error` was generated by the playlist loader, it will contain // the playlist we were trying to load (but failed) and that should be // blacklisted instead of the currently selected playlist which is likely // out-of-date in this scenario currentPlaylist = error.playlist || this.masterPlaylistLoader_.media(); // If there is no current playlist, then an error occurred while we were // trying to load the master OR while we were disposing of the tech if (!currentPlaylist) { this.error = error; return this.mediaSource.endOfStream('network'); } // Blacklist this playlist currentPlaylist.excludeUntil = Date.now() + BLACKLIST_DURATION; // Select a new playlist nextPlaylist = this.selectPlaylist(); if (nextPlaylist) { _videoJs2['default'].log.warn('Problem encountered with the current ' + 'HLS playlist. Switching to another playlist.'); return this.masterPlaylistLoader_.media(nextPlaylist); } _videoJs2['default'].log.warn('Problem encountered with the current ' + 'HLS playlist. No suitable alternatives found.'); // We have no more playlists we can select so we must fail this.error = error; return this.mediaSource.endOfStream('network'); } /** * Pause all segment loaders */ }, { key: 'pauseLoading', value: function pauseLoading() { this.mainSegmentLoader_.pause(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.pause(); } } /** * set the current time on all segment loaders * * @param {TimeRange} currentTime the current time to set * @return {TimeRange} the current time */ }, { key: 'setCurrentTime', value: function setCurrentTime(currentTime) { var buffered = _ranges2['default'].findRange(this.tech_.buffered(), currentTime); if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) { // return immediately if the metadata is not ready yet return 0; } // it's clearly an edge-case but don't thrown an error if asked to // seek within an empty playlist if (!this.masterPlaylistLoader_.media().segments) { return 0; } // if the seek location is already buffered, continue buffering as // usual if (buffered && buffered.length) { return currentTime; } // cancel outstanding requests so we begin buffering at the new // location this.mainSegmentLoader_.abort(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.abort(); } if (!this.tech_.paused()) { this.mainSegmentLoader_.load(); if (this.audioPlaylistLoader_) { this.audioSegmentLoader_.load(); } } } /** * get the current duration * * @return {TimeRange} the duration */ }, { key: 'duration', value: function duration() { if (!this.masterPlaylistLoader_) { return 0; } if (this.mediaSource) { return this.mediaSource.duration; } return Hls.Playlist.duration(this.masterPlaylistLoader_.media()); } /** * check the seekable range * * @return {TimeRange} the seekable range */ }, { key: 'seekable', value: function seekable() { var media = undefined; var mainSeekable = undefined; var audioSeekable = undefined; if (!this.masterPlaylistLoader_) { return _videoJs2['default'].createTimeRanges(); } media = this.masterPlaylistLoader_.media(); if (!media) { return _videoJs2['default'].createTimeRanges(); } mainSeekable = Hls.Playlist.seekable(media, this.masterPlaylistLoader_.expired_); if (mainSeekable.length === 0) { return mainSeekable; } if (this.audioPlaylistLoader_) { audioSeekable = Hls.Playlist.seekable(this.audioPlaylistLoader_.media(), this.audioPlaylistLoader_.expired_); if (audioSeekable.length === 0) { return audioSeekable; } } if (!audioSeekable) { // seekable has been calculated based on buffering video data so it // can be returned directly return mainSeekable; } return _videoJs2['default'].createTimeRanges([[audioSeekable.start(0) > mainSeekable.start(0) ? audioSeekable.start(0) : mainSeekable.start(0), audioSeekable.end(0) < mainSeekable.end(0) ? audioSeekable.end(0) : mainSeekable.end(0)]]); } /** * Update the player duration */ }, { key: 'updateDuration', value: function updateDuration() { var _this5 = this; var oldDuration = this.mediaSource.duration; var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media()); var buffered = this.tech_.buffered(); var setDuration = function setDuration() { _this5.mediaSource.duration = newDuration; _this5.tech_.trigger('durationchange'); _this5.mediaSource.removeEventListener('sourceopen', setDuration); }; if (buffered.length > 0) { newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); } // if the duration has changed, invalidate the cached value if (oldDuration !== newDuration) { // update the duration if (this.mediaSource.readyState !== 'open') { this.mediaSource.addEventListener('sourceopen', setDuration); } else { setDuration(); } } } /** * dispose of the MasterPlaylistController and everything * that it controls */ }, { key: 'dispose', value: function dispose() { this.masterPlaylistLoader_.dispose(); this.mainSegmentLoader_.dispose(); this.audioSegmentLoader_.dispose(); } /** * return the master playlist object if we have one * * @return {Object} the master playlist object that we parsed */ }, { key: 'master', value: function master() { return this.masterPlaylistLoader_.master; } /** * ret