1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363 |
- /**
- * @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(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _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 { _x = parent; _x2 = property; _x3 = 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 _playlistJs = require('./playlist.js');
- var _segmentLoader = require('./segment-loader');
- var _segmentLoader2 = _interopRequireDefault(_segmentLoader);
- var _vttSegmentLoader = require('./vtt-segment-loader');
- var _vttSegmentLoader2 = _interopRequireDefault(_vttSegmentLoader);
- var _ranges = require('./ranges');
- var _ranges2 = _interopRequireDefault(_ranges);
- var _videoJs = require('video.js');
- var _videoJs2 = _interopRequireDefault(_videoJs);
- var _adCueTags = require('./ad-cue-tags');
- var _adCueTags2 = _interopRequireDefault(_adCueTags);
- var _syncController = require('./sync-controller');
- var _syncController2 = _interopRequireDefault(_syncController);
- var _videojsContribMediaSourcesEs5CodecUtils = require('videojs-contrib-media-sources/es5/codec-utils');
- var _webwackify = require('webwackify');
- var _webwackify2 = _interopRequireDefault(_webwackify);
- var _decrypterWorker = require('./decrypter-worker');
- var _decrypterWorker2 = _interopRequireDefault(_decrypterWorker);
- var _config = require('./config');
- var _config2 = _interopRequireDefault(_config);
- var _utilCodecsJs = require('./util/codecs.js');
- var _mediaGroups = require('./media-groups');
- var ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2;
- var Hls = undefined;
- // Default codec parameters if none were provided for video and/or audio
- var defaultCodecs = {
- videoCodec: 'avc1',
- videoObjectTypeIndicator: '.4d400d',
- // AAC-LC
- audioProfile: '2'
- };
- // SegmentLoader stats that need to have each loader's
- // values summed to calculate the final value
- var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred'];
- var sumLoaderStat = function sumLoaderStat(stat) {
- return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat];
- };
- var resolveDecrypterWorker = function resolveDecrypterWorker() {
- var result = undefined;
- try {
- result = require.resolve('./decrypter-worker');
- } catch (e) {
- // no result
- }
- return result;
- };
- /**
- * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the
- * standard `avc1.<hhhhhh>`.
- *
- * @param codecString {String} the codec string
- * @return {String} the codec string with old apple-style codecs replaced
- *
- * @private
- */
- var mapLegacyAvcCodecs_ = function mapLegacyAvcCodecs_(codecString) {
- return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) {
- return (0, _videojsContribMediaSourcesEs5CodecUtils.translateLegacyCodecs)([match])[0];
- });
- };
- exports.mapLegacyAvcCodecs_ = mapLegacyAvcCodecs_;
- /**
- * Build a media mime-type string from a set of parameters
- * @param {String} type either 'audio' or 'video'
- * @param {String} container either 'mp2t' or 'mp4'
- * @param {Array} codecs an array of codec strings to add
- * @return {String} a valid media mime-type
- */
- var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) {
- // The codecs array is filtered so that falsey values are
- // dropped and don't cause Array#join to create spurious
- // commas
- return type + '/' + container + '; codecs="' + codecs.filter(function (c) {
- return !!c;
- }).join(', ') + '"';
- };
- /**
- * Returns the type container based on information in the playlist
- * @param {Playlist} media the current media playlist
- * @return {String} a valid media container type
- */
- var getContainerType = function getContainerType(media) {
- // An initialization segment means the media playlist is an iframe
- // playlist or is using the mp4 container. We don't currently
- // support iframe playlists, so assume this is signalling mp4
- // fragments.
- if (media.segments && media.segments.length && media.segments[0].map) {
- return 'mp4';
- }
- return 'mp2t';
- };
- /**
- * Returns a set of codec strings parsed from the playlist or the default
- * codec strings if no codecs were specified in the playlist
- * @param {Playlist} media the current media playlist
- * @return {Object} an object with the video and audio codecs
- */
- var getCodecs = function getCodecs(media) {
- // if the codecs were explicitly specified, use them instead of the
- // defaults
- var mediaAttributes = media.attributes || {};
- if (mediaAttributes.CODECS) {
- return (0, _utilCodecsJs.parseCodecs)(mediaAttributes.CODECS);
- }
- return defaultCodecs;
- };
- /**
- * 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 containerType = getContainerType(media);
- var codecInfo = getCodecs(media);
- var mediaAttributes = media.attributes || {};
- // Default condition for a traditional HLS (no demuxed audio/video)
- var isMuxed = true;
- var isMaat = false;
- if (!media) {
- // Not enough information
- return [];
- }
- if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) {
- var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
- // Handle the case where we are in a multiple-audio track scenario
- if (audioGroup) {
- isMaat = true;
- // Start with the everything demuxed then...
- isMuxed = false;
- // ...check to see if any audio group tracks are muxed (ie. lacking a uri)
- for (var groupId in audioGroup) {
- if (!audioGroup[groupId].uri) {
- isMuxed = true;
- break;
- }
- }
- }
- }
- // HLS with multiple-audio tracks must always get an audio codec.
- // Put another way, there is no way to have a video-only multiple-audio HLS!
- if (isMaat && !codecInfo.audioProfile) {
- _videoJs2['default'].log.warn('Multiple audio tracks present but no audio codec string is specified. ' + 'Attempting to use the default audio codec (mp4a.40.2)');
- codecInfo.audioProfile = defaultCodecs.audioProfile;
- }
- // Generate the final codec strings from the codec object generated above
- var codecStrings = {};
- if (codecInfo.videoCodec) {
- codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator;
- }
- if (codecInfo.audioProfile) {
- codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile;
- }
- // Finally, make and return an array with proper mime-types depending on
- // the configuration
- var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]);
- var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]);
- var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]);
- if (isMaat) {
- if (!isMuxed && codecStrings.video) {
- return [justVideo, justAudio];
- }
- // There exists the possiblity that this will return a `video/container`
- // mime-type for the first entry in the array even when there is only audio.
- // This doesn't appear to be a problem and simplifies the code.
- return [bothVideoAudio, justAudio];
- }
- // If there is ano video codec at all, always just return a single
- // audio/<container> mime-type
- if (!codecStrings.video) {
- return [justAudio];
- }
- // When not using separate audio media groups, audio and video is
- // *always* muxed
- return [bothVideoAudio];
- };
- exports.mimeTypesForPlaylist_ = mimeTypesForPlaylist_;
- /**
- * the master playlist controller controls 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 handleManifestRedirects = options.handleManifestRedirects;
- var withCredentials = options.withCredentials;
- var mode = options.mode;
- var tech = options.tech;
- var bandwidth = options.bandwidth;
- var externHls = options.externHls;
- var useCueTags = options.useCueTags;
- var blacklistDuration = options.blacklistDuration;
- var enableLowInitialPlaylist = options.enableLowInitialPlaylist;
- if (!url) {
- throw new Error('A non-empty playlist URL is required');
- }
- Hls = externHls;
- this.tech_ = tech;
- this.hls_ = tech.hls;
- this.mode_ = mode;
- this.useCueTags_ = useCueTags;
- this.blacklistDuration = blacklistDuration;
- this.enableLowInitialPlaylist = enableLowInitialPlaylist;
- if (this.useCueTags_) {
- this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues');
- this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
- }
- this.requestOptions_ = {
- withCredentials: withCredentials,
- handleManifestRedirects: handleManifestRedirects,
- timeout: null
- };
- this.mediaTypes_ = (0, _mediaGroups.createMediaTypes)();
- this.mediaSource = new _videoJs2['default'].MediaSource({ mode: mode });
- // load the media source into the player
- this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
- this.seekable_ = _videoJs2['default'].createTimeRanges();
- this.hasPlayed_ = function () {
- return false;
- };
- this.syncController_ = new _syncController2['default'](options);
- this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
- kind: 'metadata',
- label: 'segment-metadata'
- }, false).track;
- this.decrypter_ = (0, _webwackify2['default'])(_decrypterWorker2['default'], resolveDecrypterWorker());
- var segmentLoaderSettings = {
- 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();
- },
- duration: function duration() {
- return _this.mediaSource.duration;
- },
- hasPlayed: function hasPlayed() {
- return _this.hasPlayed_();
- },
- goalBufferLength: function goalBufferLength() {
- return _this.goalBufferLength();
- },
- bandwidth: bandwidth,
- syncController: this.syncController_,
- decrypter: this.decrypter_
- };
- // setup playlist loaders
- this.masterPlaylistLoader_ = new _playlistLoader2['default'](url, this.hls_, this.requestOptions_);
- this.setupMasterPlaylistLoaderListeners_();
- // setup segment loaders
- // combined audio/video or just video when alternate audio track is selected
- this.mainSegmentLoader_ = new _segmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
- segmentMetadataTrack: this.segmentMetadataTrack_,
- loaderType: 'main'
- }), options);
- // alternate audio track
- this.audioSegmentLoader_ = new _segmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
- loaderType: 'audio'
- }), options);
- this.subtitleSegmentLoader_ = new _vttSegmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
- loaderType: 'vtt'
- }), options);
- this.setupSegmentLoaderListeners_();
- // Create SegmentLoader stat-getters
- loaderStats.forEach(function (stat) {
- _this[stat + '_'] = sumLoaderStat.bind(_this, stat);
- });
- this.masterPlaylistLoader_.load();
- }
- /**
- * 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;
- // If we don't have any more available playlists, we don't want to
- // timeout the request.
- if ((0, _playlistJs.isLowestEnabledRendition)(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) {
- _this2.requestOptions_.timeout = 0;
- } else {
- _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_.load();
- }
- (0, _mediaGroups.setupMediaGroups)({
- segmentLoaders: {
- AUDIO: _this2.audioSegmentLoader_,
- SUBTITLES: _this2.subtitleSegmentLoader_,
- main: _this2.mainSegmentLoader_
- },
- tech: _this2.tech_,
- requestOptions: _this2.requestOptions_,
- masterPlaylistLoader: _this2.masterPlaylistLoader_,
- mode: _this2.mode_,
- hls: _this2.hls_,
- master: _this2.master(),
- mediaTypes: _this2.mediaTypes_,
- blacklistCurrentPlaylist: _this2.blacklistCurrentPlaylist.bind(_this2)
- });
- _this2.triggerPresenceUsage_(_this2.master(), media);
- try {
- _this2.setupSourceBuffers_();
- } catch (e) {
- _videoJs2['default'].log.warn('Failed to create SourceBuffers', e);
- return _this2.mediaSource.endOfStream('decode');
- }
- _this2.setupFirstPlay();
- _this2.trigger('selectedinitialmedia');
- });
- this.masterPlaylistLoader_.on('loadedplaylist', function () {
- var updatedPlaylist = _this2.masterPlaylistLoader_.media();
- if (!updatedPlaylist) {
- var selectedMedia = undefined;
- if (_this2.enableLowInitialPlaylist) {
- selectedMedia = _this2.selectInitialPlaylist();
- }
- if (!selectedMedia) {
- selectedMedia = _this2.selectPlaylist();
- }
- _this2.initialMedia_ = selectedMedia;
- _this2.masterPlaylistLoader_.media(_this2.initialMedia_);
- return;
- }
- if (_this2.useCueTags_) {
- _this2.updateAdCues_(updatedPlaylist);
- }
- // 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.updateDuration();
- // If the player isn't paused, ensure that the segment loader is running,
- // as it is possible that it was temporarily stopped while waiting for
- // a playlist (e.g., in case the playlist errored and we re-requested it).
- if (!_this2.tech_.paused()) {
- _this2.mainSegmentLoader_.load();
- }
- if (!updatedPlaylist.endList) {
- (function () {
- var addSeekableRange = function addSeekableRange() {
- var seekable = _this2.seekable();
- if (seekable.length !== 0) {
- _this2.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
- }
- };
- if (_this2.duration() !== Infinity) {
- (function () {
- var onDurationchange = function onDurationchange() {
- if (_this2.duration() === Infinity) {
- addSeekableRange();
- } else {
- _this2.tech_.one('durationchange', onDurationchange);
- }
- };
- _this2.tech_.one('durationchange', onDurationchange);
- })();
- } else {
- addSeekableRange();
- }
- })();
- }
- });
- 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;
- // If we don't have any more available playlists, we don't want to
- // timeout the request.
- if ((0, _playlistJs.isLowestEnabledRendition)(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) {
- _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_.load();
- _this2.tech_.trigger({
- type: 'mediachange',
- bubbles: true
- });
- });
- this.masterPlaylistLoader_.on('playlistunchanged', function () {
- var updatedPlaylist = _this2.masterPlaylistLoader_.media();
- var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist);
- if (playlistOutdated) {
- // Playlist has stopped updating and we're stuck at its end. Try to
- // blacklist it and switch to another playlist in the hope that that
- // one is updating (and give the player a chance to re-adjust to the
- // safe live point).
- _this2.blacklistCurrentPlaylist({
- message: 'Playlist no longer updating.'
- });
- // useful for monitoring QoS
- _this2.tech_.trigger('playliststuck');
- }
- });
- this.masterPlaylistLoader_.on('renditiondisabled', function () {
- _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-disabled' });
- });
- this.masterPlaylistLoader_.on('renditionenabled', function () {
- _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-enabled' });
- });
- }
- /**
- * A helper function for triggerring presence usage events once per source
- *
- * @private
- */
- }, {
- key: 'triggerPresenceUsage_',
- value: function triggerPresenceUsage_(master, media) {
- var mediaGroups = master.mediaGroups || {};
- var defaultDemuxed = true;
- var audioGroupKeys = Object.keys(mediaGroups.AUDIO);
- for (var mediaGroup in mediaGroups.AUDIO) {
- for (var label in mediaGroups.AUDIO[mediaGroup]) {
- var properties = mediaGroups.AUDIO[mediaGroup][label];
- if (!properties.uri) {
- defaultDemuxed = false;
- }
- }
- }
- if (defaultDemuxed) {
- this.tech_.trigger({ type: 'usage', name: 'hls-demuxed' });
- }
- if (Object.keys(mediaGroups.SUBTITLES).length) {
- this.tech_.trigger({ type: 'usage', name: 'hls-webvtt' });
- }
- if (Hls.Playlist.isAes(media)) {
- this.tech_.trigger({ type: 'usage', name: 'hls-aes' });
- }
- if (Hls.Playlist.isFmp4(media)) {
- this.tech_.trigger({ type: 'usage', name: 'hls-fmp4' });
- }
- if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
- this.tech_.trigger({ type: 'usage', name: 'hls-alternate-audio' });
- }
- if (this.useCueTags_) {
- this.tech_.trigger({ type: 'usage', name: 'hls-playlist-cue-tags' });
- }
- }
- /**
- * 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('bandwidthupdate', function () {
- var nextPlaylist = _this3.selectPlaylist();
- var currentPlaylist = _this3.masterPlaylistLoader_.media();
- var buffered = _this3.tech_.buffered();
- var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0;
- var bufferLowWaterLine = _this3.bufferLowWaterLine();
- // If the playlist is live, then we want to not take low water line into account.
- // This is because in LIVE, the player plays 3 segments from the end of the
- // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
- // in those segments, a viewer will never experience a rendition upswitch.
- if (!currentPlaylist.endList ||
- // For the same reason as LIVE, we ignore the low water line when the VOD
- // duration is below the max potential low water line
- _this3.duration() < _config2['default'].MAX_BUFFER_LOW_WATER_LINE ||
- // we want to switch down to lower resolutions quickly to continue playback, but
- nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH ||
- // ensure we have some buffer before we switch up to prevent us running out of
- // buffer while loading a higher rendition.
- forwardBuffer >= bufferLowWaterLine) {
- _this3.masterPlaylistLoader_.media(nextPlaylist);
- }
- _this3.tech_.trigger('bandwidthupdate');
- });
- this.mainSegmentLoader_.on('progress', function () {
- _this3.trigger('progress');
- });
- this.mainSegmentLoader_.on('error', function () {
- _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error());
- });
- this.mainSegmentLoader_.on('syncinfoupdate', function () {
- _this3.onSyncInfoUpdate_();
- });
- this.mainSegmentLoader_.on('timestampoffset', function () {
- _this3.tech_.trigger({ type: 'usage', name: 'hls-timestamp-offset' });
- });
- this.audioSegmentLoader_.on('syncinfoupdate', function () {
- _this3.onSyncInfoUpdate_();
- });
- this.mainSegmentLoader_.on('ended', function () {
- _this3.onEndOfStream();
- });
- this.mainSegmentLoader_.on('earlyabort', function () {
- _this3.blacklistCurrentPlaylist({
- message: 'Aborted early because there isn\'t enough bandwidth to complete the ' + 'request without rebuffering.'
- }, ABORT_EARLY_BLACKLIST_SECONDS);
- });
- this.mainSegmentLoader_.on('reseteverything', function () {
- // If playing an MTS stream, a videojs.MediaSource is listening for
- // hls-reset to reset caption parsing state in the transmuxer
- _this3.tech_.trigger('hls-reset');
- });
- this.mainSegmentLoader_.on('segmenttimemapping', function (event) {
- // If playing an MTS stream in html, a videojs.MediaSource is listening for
- // hls-segment-time-mapping update its internal mapping of stream to display time
- _this3.tech_.trigger({
- type: 'hls-segment-time-mapping',
- mapping: event.mapping
- });
- });
- this.audioSegmentLoader_.on('ended', function () {
- _this3.onEndOfStream();
- });
- }
- }, {
- key: 'mediaSecondsLoaded_',
- value: function mediaSecondsLoaded_() {
- return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded);
- }
- /**
- * Call load on our SegmentLoaders
- */
- }, {
- key: 'load',
- value: function load() {
- this.mainSegmentLoader_.load();
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- this.audioSegmentLoader_.load();
- }
- if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
- this.subtitleSegmentLoader_.load();
- }
- }
- /**
- * 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_.resetLoader();
- // don't need to reset audio as it is reset when media changes
- }
- }
- /**
- * Begin playback.
- */
- }, {
- key: 'play',
- value: function play() {
- if (this.setupFirstPlay()) {
- return;
- }
- if (this.tech_.ended()) {
- this.tech_.setCurrentTime(0);
- }
- if (this.hasPlayed_()) {
- this.load();
- }
- var seekable = this.tech_.seekable();
- // if the viewer has paused and we fell out of the live window,
- // seek forward to the live point
- if (this.tech_.duration() === Infinity) {
- if (this.tech_.currentTime() < seekable.start(0)) {
- return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
- }
- }
- }
- /**
- * 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 _this4 = this;
- var media = this.masterPlaylistLoader_.media();
- // Check that everything is ready to begin buffering for the first call to play
- // If 1) there is no active media
- // 2) the player is paused
- // 3) the first play has already been setup
- // then exit early
- if (!media || this.tech_.paused() || this.hasPlayed_()) {
- return false;
- }
- // when the video is a live stream
- if (!media.endList) {
- var _ret3 = (function () {
- var seekable = _this4.seekable();
- if (!seekable.length) {
- // without a seekable range, the player cannot seek to begin buffering at the live
- // point
- return {
- v: false
- };
- }
- if (_videoJs2['default'].browser.IE_VERSION && _this4.mode_ === 'html5' && _this4.tech_.readyState() === 0) {
- // IE11 throws an InvalidStateError if you try to set currentTime while the
- // readyState is 0, so it must be delayed until the tech fires loadedmetadata.
- _this4.tech_.one('loadedmetadata', function () {
- _this4.trigger('firstplay');
- _this4.tech_.setCurrentTime(seekable.end(0));
- _this4.hasPlayed_ = function () {
- return true;
- };
- });
- return {
- v: false
- };
- }
- // trigger firstplay to inform the source handler to ignore the next seek event
- _this4.trigger('firstplay');
- // seek to the live point
- _this4.tech_.setCurrentTime(seekable.end(0));
- })();
- if (typeof _ret3 === 'object') return _ret3.v;
- }
- this.hasPlayed_ = function () {
- return true;
- };
- // we can begin loading now that everything is ready
- this.load();
- return true;
- }
- /**
- * 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()) {
- var playPromise = this.tech_.play();
- // Catch/silence error when a pause interrupts a play request
- // on browsers which return a promise
- if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') {
- playPromise.then(null, function (e) {});
- }
- }
- this.trigger('sourceopen');
- }
- /**
- * Calls endOfStream on the media source when all active stream types have called
- * endOfStream
- *
- * @param {string} streamType
- * Stream type of the segment loader that called endOfStream
- * @private
- */
- }, {
- key: 'onEndOfStream',
- value: function onEndOfStream() {
- var isEndOfStream = this.mainSegmentLoader_.ended_;
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- // if the audio playlist loader exists, then alternate audio is active, so we need
- // to wait for both the main and audio segment loaders to call endOfStream
- isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
- }
- if (isEndOfStream) {
- this.mediaSource.endOfStream();
- }
- }
- /**
- * Check if a playlist has stopped being updated
- * @param {Object} playlist the media playlist object
- * @return {boolean} whether the playlist has stopped being updated or not
- */
- }, {
- key: 'stuckAtPlaylistEnd_',
- value: function stuckAtPlaylistEnd_(playlist) {
- var seekable = this.seekable();
- if (!seekable.length) {
- // playlist doesn't have enough information to determine whether we are stuck
- return false;
- }
- var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration);
- if (expired === null) {
- return false;
- }
- // does not use the safe live end to calculate playlist end, since we
- // don't want to say we are stuck while there is still content
- var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired);
- var currentTime = this.tech_.currentTime();
- var buffered = this.tech_.buffered();
- if (!buffered.length) {
- // return true if the playhead reached the absolute end of the playlist
- return absolutePlaylistEnd - currentTime <= _ranges2['default'].SAFE_TIME_DELTA;
- }
- var bufferedEnd = buffered.end(buffered.length - 1);
- // return true if there is too little buffer left and buffer has reached absolute
- // end of playlist
- return bufferedEnd - currentTime <= _ranges2['default'].SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= _ranges2['default'].SAFE_TIME_DELTA;
- }
- /**
- * 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
- * @param {Number=} blacklistDuration an optional number of seconds to blacklist the
- * playlist
- */
- }, {
- key: 'blacklistCurrentPlaylist',
- value: function blacklistCurrentPlaylist(error, blacklistDuration) {
- if (error === undefined) error = {};
- 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();
- blacklistDuration = blacklistDuration || error.blacklistDuration || this.blacklistDuration;
- // 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;
- try {
- return this.mediaSource.endOfStream('network');
- } catch (e) {
- return this.trigger('error');
- }
- }
- var isFinalRendition = this.masterPlaylistLoader_.master.playlists.filter(_playlistJs.isEnabled).length === 1;
- if (isFinalRendition) {
- // Never blacklisting this playlist because it's final rendition
- _videoJs2['default'].log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the final playlist.');
- this.tech_.trigger('retryplaylist');
- return this.masterPlaylistLoader_.load(isFinalRendition);
- }
- // Blacklist this playlist
- currentPlaylist.excludeUntil = Date.now() + blacklistDuration * 1000;
- this.tech_.trigger('blacklistplaylist');
- this.tech_.trigger({ type: 'usage', name: 'hls-rendition-blacklisted' });
- // Select a new playlist
- nextPlaylist = this.selectPlaylist();
- _videoJs2['default'].log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.');
- return this.masterPlaylistLoader_.media(nextPlaylist);
- }
- /**
- * Pause all segment loaders
- */
- }, {
- key: 'pauseLoading',
- value: function pauseLoading() {
- this.mainSegmentLoader_.pause();
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- this.audioSegmentLoader_.pause();
- }
- if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
- this.subtitleSegmentLoader_.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;
- }
- // In flash playback, the segment loaders should be reset on every seek, even
- // in buffer seeks. If the seek location is already buffered, continue buffering as
- // usual
- if (buffered && buffered.length && this.mode_ !== 'flash') {
- return currentTime;
- }
- // cancel outstanding requests so we begin buffering at the new
- // location
- this.mainSegmentLoader_.resetEverything();
- this.mainSegmentLoader_.abort();
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- this.audioSegmentLoader_.resetEverything();
- this.audioSegmentLoader_.abort();
- }
- if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
- this.subtitleSegmentLoader_.resetEverything();
- this.subtitleSegmentLoader_.abort();
- }
- // start segment loader loading in case they are paused
- this.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() {
- return this.seekable_;
- }
- }, {
- key: 'onSyncInfoUpdate_',
- value: function onSyncInfoUpdate_() {
- var mainSeekable = undefined;
- var audioSeekable = undefined;
- if (!this.masterPlaylistLoader_) {
- return;
- }
- var media = this.masterPlaylistLoader_.media();
- if (!media) {
- return;
- }
- var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
- if (expired === null) {
- // not enough information to update seekable
- return;
- }
- mainSeekable = Hls.Playlist.seekable(media, expired);
- if (mainSeekable.length === 0) {
- return;
- }
- if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
- media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
- expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
- if (expired === null) {
- return;
- }
- audioSeekable = Hls.Playlist.seekable(media, expired);
- if (audioSeekable.length === 0) {
- return;
- }
- }
- if (!audioSeekable) {
- // seekable has been calculated based on buffering video data so it
- // can be returned directly
- this.seekable_ = mainSeekable;
- } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) {
- // seekables are pretty far off, rely on main
- this.seekable_ = mainSeekable;
- } else {
- this.seekable_ = _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)]]);
- }
- this.tech_.trigger('seekablechanged');
- }
- /**
- * 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() {
- var _this6 = this;
- this.decrypter_.terminate();
- this.masterPlaylistLoader_.dispose();
- this.mainSegmentLoader_.dispose();
- ['AUDIO', 'SUBTITLES'].forEach(function (type) {
- var groups = _this6.mediaTypes_[type].groups;
- for (var id in groups) {
- groups[id].forEach(function (group) {
- if (group.playlistLoader) {
- group.playlistLoader.dispose();
- }
- });
- }
- });
- this.audioSegmentLoader_.dispose();
- this.subtitleSegmentLoader_.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;
- }
- /**
- * return the currently selected playlist
- *
- * @return {Object} the currently selected playlist object that we parsed
- */
- }, {
- key: 'media',
- value: function media() {
- // playlist loader will not return media if it has not been fully loaded
- return this.masterPlaylistLoader_.media() || this.initialMedia_;
- }
- /**
- * setup our internal source buffers on our segment Loaders
- *
- * @private
- */
- }, {
- key: 'setupSourceBuffers_',
- value: function setupSourceBuffers_() {
- var media = this.masterPlaylistLoader_.media();
- var mimeTypes = undefined;
- // wait until a media playlist is available and the Media Source is
- // attached
- if (!media || this.mediaSource.readyState !== 'open') {
- return;
- }
- mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media);
- if (mimeTypes.length < 1) {
- this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri;
- return this.mediaSource.endOfStream('decode');
- }
- this.mainSegmentLoader_.mimeType(mimeTypes[0]);
- if (mimeTypes[1]) {
- this.audioSegmentLoader_.mimeType(mimeTypes[1]);
- }
- // exclude any incompatible variant streams from future playlist
- // selection
- this.excludeIncompatibleVariants_(media);
- }
- /**
- * Blacklist playlists that are known to be codec or
- * stream-incompatible with the SourceBuffer configuration. For
- * instance, Media Source Extensions would cause the video element to
- * stall waiting for video data if you switched from a variant with
- * video and audio to an audio-only one.
- *
- * @param {Object} media a media playlist compatible with the current
- * set of SourceBuffers. Variants in the current master playlist that
- * do not appear to have compatible codec or stream configurations
- * will be excluded from the default playlist selection algorithm
- * indefinitely.
- * @private
- */
- }, {
- key: 'excludeIncompatibleVariants_',
- value: function excludeIncompatibleVariants_(media) {
- var master = this.masterPlaylistLoader_.master;
- var codecCount = 2;
- var videoCodec = null;
- var codecs = undefined;
- if (media.attributes.CODECS) {
- codecs = (0, _utilCodecsJs.parseCodecs)(media.attributes.CODECS);
- videoCodec = codecs.videoCodec;
- codecCount = codecs.codecCount;
- }
- master.playlists.forEach(function (variant) {
- var variantCodecs = {
- codecCount: 2,
- videoCodec: null
- };
- if (variant.attributes.CODECS) {
- var codecString = variant.attributes.CODECS;
- variantCodecs = (0, _utilCodecsJs.parseCodecs)(codecString);
- if (window.MediaSource && window.MediaSource.isTypeSupported && !window.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs_(codecString) + '"')) {
- variant.excludeUntil = Infinity;
- }
- }
- // if the streams differ in the presence or absence of audio or
- // video, they are incompatible
- if (variantCodecs.codecCount !== codecCount) {
- variant.excludeUntil = Infinity;
- }
- // if h.264 is specified on the current playlist, some flavor of
- // it must be specified on all compatible variants
- if (variantCodecs.videoCodec !== videoCodec) {
- variant.excludeUntil = Infinity;
- }
- });
- }
- }, {
- key: 'updateAdCues_',
- value: function updateAdCues_(media) {
- var offset = 0;
- var seekable = this.seekable();
- if (seekable.length) {
- offset = seekable.start(0);
- }
- _adCueTags2['default'].updateAdCues(media, this.cueTagsTrack_, offset);
- }
- /**
- * Calculates the desired forward buffer length based on current time
- *
- * @return {Number} Desired forward buffer length in seconds
- */
- }, {
- key: 'goalBufferLength',
- value: function goalBufferLength() {
- var currentTime = this.tech_.currentTime();
- var initial = _config2['default'].GOAL_BUFFER_LENGTH;
- var rate = _config2['default'].GOAL_BUFFER_LENGTH_RATE;
- var max = Math.max(initial, _config2['default'].MAX_GOAL_BUFFER_LENGTH);
- return Math.min(initial + currentTime * rate, max);
- }
- /**
- * Calculates the desired buffer low water line based on current time
- *
- * @return {Number} Desired buffer low water line in seconds
- */
- }, {
- key: 'bufferLowWaterLine',
- value: function bufferLowWaterLine() {
- var currentTime = this.tech_.currentTime();
- var initial = _config2['default'].BUFFER_LOW_WATER_LINE;
- var rate = _config2['default'].BUFFER_LOW_WATER_LINE_RATE;
- var max = Math.max(initial, _config2['default'].MAX_BUFFER_LOW_WATER_LINE);
- return Math.min(initial + currentTime * rate, max);
- }
- }]);
- return MasterPlaylistController;
- })(_videoJs2['default'].EventTarget);
- exports.MasterPlaylistController = MasterPlaylistController;
|