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
JavaScript
/**
* 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