UNPKG

61.5 kBJavaScriptView Raw
1/**
2 * @file master-playlist-controller.js
3 */
4'use strict';
5
6Object.defineProperty(exports, '__esModule', {
7 value: true
8});
9
10var _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; }; })();
11
12var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _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 { _x2 = parent; _x3 = property; _x4 = 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); } } };
13
14function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
15
16function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
17
18function _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; }
19
20var _playlistLoader = require('./playlist-loader');
21
22var _playlistLoader2 = _interopRequireDefault(_playlistLoader);
23
24var _segmentLoader = require('./segment-loader');
25
26var _segmentLoader2 = _interopRequireDefault(_segmentLoader);
27
28var _vttSegmentLoader = require('./vtt-segment-loader');
29
30var _vttSegmentLoader2 = _interopRequireDefault(_vttSegmentLoader);
31
32var _ranges = require('./ranges');
33
34var _ranges2 = _interopRequireDefault(_ranges);
35
36var _videoJs = require('video.js');
37
38var _videoJs2 = _interopRequireDefault(_videoJs);
39
40var _adCueTags = require('./ad-cue-tags');
41
42var _adCueTags2 = _interopRequireDefault(_adCueTags);
43
44var _syncController = require('./sync-controller');
45
46var _syncController2 = _interopRequireDefault(_syncController);
47
48var _videojsContribMediaSourcesEs5CodecUtils = require('videojs-contrib-media-sources/es5/codec-utils');
49
50var _webworkify = require('webworkify');
51
52var _webworkify2 = _interopRequireDefault(_webworkify);
53
54var _decrypterWorker = require('./decrypter-worker');
55
56var _decrypterWorker2 = _interopRequireDefault(_decrypterWorker);
57
58var _config = require('./config');
59
60var _config2 = _interopRequireDefault(_config);
61
62var Hls = undefined;
63
64// Default codec parameters if none were provided for video and/or audio
65var defaultCodecs = {
66 videoCodec: 'avc1',
67 videoObjectTypeIndicator: '.4d400d',
68 // AAC-LC
69 audioProfile: '2'
70};
71
72// SegmentLoader stats that need to have each loader's
73// values summed to calculate the final value
74var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred'];
75var sumLoaderStat = function sumLoaderStat(stat) {
76 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat];
77};
78
79/**
80 * determine if an object a is differnt from
81 * and object b. both only having one dimensional
82 * properties
83 *
84 * @param {Object} a object one
85 * @param {Object} b object two
86 * @return {Boolean} if the object has changed or not
87 */
88var objectChanged = function objectChanged(a, b) {
89 if (typeof a !== typeof b) {
90 return true;
91 }
92 // if we have a different number of elements
93 // something has changed
94 if (Object.keys(a).length !== Object.keys(b).length) {
95 return true;
96 }
97
98 for (var prop in a) {
99 if (a[prop] !== b[prop]) {
100 return true;
101 }
102 }
103 return false;
104};
105
106/**
107 * Parses a codec string to retrieve the number of codecs specified,
108 * the video codec and object type indicator, and the audio profile.
109 *
110 * @private
111 */
112var parseCodecs = function parseCodecs(codecs) {
113 var result = {
114 codecCount: 0
115 };
116 var parsed = undefined;
117
118 result.codecCount = codecs.split(',').length;
119 result.codecCount = result.codecCount || 2;
120
121 // parse the video codec
122 parsed = /(^|\s|,)+(avc1)([^ ,]*)/i.exec(codecs);
123 if (parsed) {
124 result.videoCodec = parsed[2];
125 result.videoObjectTypeIndicator = parsed[3];
126 }
127
128 // parse the last field of the audio codec
129 result.audioProfile = /(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i.exec(codecs);
130 result.audioProfile = result.audioProfile && result.audioProfile[2];
131
132 return result;
133};
134
135/**
136 * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the
137 * standard `avc1.<hhhhhh>`.
138 *
139 * @param codecString {String} the codec string
140 * @return {String} the codec string with old apple-style codecs replaced
141 *
142 * @private
143 */
144var mapLegacyAvcCodecs_ = function mapLegacyAvcCodecs_(codecString) {
145 return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) {
146 return (0, _videojsContribMediaSourcesEs5CodecUtils.translateLegacyCodecs)([match])[0];
147 });
148};
149
150exports.mapLegacyAvcCodecs_ = mapLegacyAvcCodecs_;
151/**
152 * Build a media mime-type string from a set of parameters
153 * @param {String} type either 'audio' or 'video'
154 * @param {String} container either 'mp2t' or 'mp4'
155 * @param {Array} codecs an array of codec strings to add
156 * @return {String} a valid media mime-type
157 */
158var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) {
159 // The codecs array is filtered so that falsey values are
160 // dropped and don't cause Array#join to create spurious
161 // commas
162 return type + '/' + container + '; codecs="' + codecs.filter(function (c) {
163 return !!c;
164 }).join(', ') + '"';
165};
166
167/**
168 * Returns the type container based on information in the playlist
169 * @param {Playlist} media the current media playlist
170 * @return {String} a valid media container type
171 */
172var getContainerType = function getContainerType(media) {
173 // An initialization segment means the media playlist is an iframe
174 // playlist or is using the mp4 container. We don't currently
175 // support iframe playlists, so assume this is signalling mp4
176 // fragments.
177 if (media.segments && media.segments.length && media.segments[0].map) {
178 return 'mp4';
179 }
180 return 'mp2t';
181};
182
183/**
184 * Returns a set of codec strings parsed from the playlist or the default
185 * codec strings if no codecs were specified in the playlist
186 * @param {Playlist} media the current media playlist
187 * @return {Object} an object with the video and audio codecs
188 */
189var getCodecs = function getCodecs(media) {
190 // if the codecs were explicitly specified, use them instead of the
191 // defaults
192 var mediaAttributes = media.attributes || {};
193
194 if (mediaAttributes.CODECS) {
195 return parseCodecs(mediaAttributes.CODECS);
196 }
197 return defaultCodecs;
198};
199
200/**
201 * Calculates the MIME type strings for a working configuration of
202 * SourceBuffers to play variant streams in a master playlist. If
203 * there is no possible working configuration, an empty array will be
204 * returned.
205 *
206 * @param master {Object} the m3u8 object for the master playlist
207 * @param media {Object} the m3u8 object for the variant playlist
208 * @return {Array} the MIME type strings. If the array has more than
209 * one entry, the first element should be applied to the video
210 * SourceBuffer and the second to the audio SourceBuffer.
211 *
212 * @private
213 */
214var mimeTypesForPlaylist_ = function mimeTypesForPlaylist_(master, media) {
215 var containerType = getContainerType(media);
216 var codecInfo = getCodecs(media);
217 var mediaAttributes = media.attributes || {};
218 // Default condition for a traditional HLS (no demuxed audio/video)
219 var isMuxed = true;
220 var isMaat = false;
221
222 if (!media) {
223 // Not enough information
224 return [];
225 }
226
227 if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) {
228 var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
229
230 // Handle the case where we are in a multiple-audio track scenario
231 if (audioGroup) {
232 isMaat = true;
233 // Start with the everything demuxed then...
234 isMuxed = false;
235 // ...check to see if any audio group tracks are muxed (ie. lacking a uri)
236 for (var groupId in audioGroup) {
237 if (!audioGroup[groupId].uri) {
238 isMuxed = true;
239 break;
240 }
241 }
242 }
243 }
244
245 // HLS with multiple-audio tracks must always get an audio codec.
246 // Put another way, there is no way to have a video-only multiple-audio HLS!
247 if (isMaat && !codecInfo.audioProfile) {
248 _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)');
249 codecInfo.audioProfile = defaultCodecs.audioProfile;
250 }
251
252 // Generate the final codec strings from the codec object generated above
253 var codecStrings = {};
254
255 if (codecInfo.videoCodec) {
256 codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator;
257 }
258
259 if (codecInfo.audioProfile) {
260 codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile;
261 }
262
263 // Finally, make and return an array with proper mime-types depending on
264 // the configuration
265 var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]);
266 var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]);
267 var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]);
268
269 if (isMaat) {
270 if (!isMuxed && codecStrings.video) {
271 return [justVideo, justAudio];
272 }
273 // There exists the possiblity that this will return a `video/container`
274 // mime-type for the first entry in the array even when there is only audio.
275 // This doesn't appear to be a problem and simplifies the code.
276 return [bothVideoAudio, justAudio];
277 }
278
279 // If there is ano video codec at all, always just return a single
280 // audio/<container> mime-type
281 if (!codecStrings.video) {
282 return [justAudio];
283 }
284
285 // When not using separate audio media groups, audio and video is
286 // *always* muxed
287 return [bothVideoAudio];
288};
289
290exports.mimeTypesForPlaylist_ = mimeTypesForPlaylist_;
291/**
292 * the master playlist controller controller all interactons
293 * between playlists and segmentloaders. At this time this mainly
294 * involves a master playlist and a series of audio playlists
295 * if they are available
296 *
297 * @class MasterPlaylistController
298 * @extends videojs.EventTarget
299 */
300
301var MasterPlaylistController = (function (_videojs$EventTarget) {
302 _inherits(MasterPlaylistController, _videojs$EventTarget);
303
304 function MasterPlaylistController(options) {
305 var _this = this;
306
307 _classCallCheck(this, MasterPlaylistController);
308
309 _get(Object.getPrototypeOf(MasterPlaylistController.prototype), 'constructor', this).call(this);
310
311 var url = options.url;
312 var withCredentials = options.withCredentials;
313 var mode = options.mode;
314 var tech = options.tech;
315 var bandwidth = options.bandwidth;
316 var externHls = options.externHls;
317 var useCueTags = options.useCueTags;
318 var blacklistDuration = options.blacklistDuration;
319
320 if (!url) {
321 throw new Error('A non-empty playlist URL is required');
322 }
323
324 Hls = externHls;
325
326 this.withCredentials = withCredentials;
327 this.tech_ = tech;
328 this.hls_ = tech.hls;
329 this.mode_ = mode;
330 this.useCueTags_ = useCueTags;
331 this.blacklistDuration = blacklistDuration;
332 if (this.useCueTags_) {
333 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues');
334 this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
335 }
336
337 this.requestOptions_ = {
338 withCredentials: this.withCredentials,
339 timeout: null
340 };
341
342 this.audioGroups_ = {};
343 this.subtitleGroups_ = { groups: {}, tracks: {} };
344
345 this.mediaSource = new _videoJs2['default'].MediaSource({ mode: mode });
346 this.audioinfo_ = null;
347 this.mediaSource.on('audioinfo', this.handleAudioinfoUpdate_.bind(this));
348
349 // load the media source into the player
350 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
351
352 this.seekable_ = _videoJs2['default'].createTimeRanges();
353 this.hasPlayed_ = function () {
354 return false;
355 };
356
357 this.syncController_ = new _syncController2['default'](options);
358 this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
359 kind: 'metadata',
360 label: 'segment-metadata'
361 }, true).track;
362
363 this.decrypter_ = (0, _webworkify2['default'])(_decrypterWorker2['default']);
364
365 var segmentLoaderSettings = {
366 hls: this.hls_,
367 mediaSource: this.mediaSource,
368 currentTime: this.tech_.currentTime.bind(this.tech_),
369 seekable: function seekable() {
370 return _this.seekable();
371 },
372 seeking: function seeking() {
373 return _this.tech_.seeking();
374 },
375 duration: function duration() {
376 return _this.mediaSource.duration;
377 },
378 hasPlayed: function hasPlayed() {
379 return _this.hasPlayed_();
380 },
381 goalBufferLength: function goalBufferLength() {
382 return _this.goalBufferLength();
383 },
384 bandwidth: bandwidth,
385 syncController: this.syncController_,
386 decrypter: this.decrypter_
387 };
388
389 // setup playlist loaders
390 this.masterPlaylistLoader_ = new _playlistLoader2['default'](url, this.hls_, this.withCredentials);
391 this.setupMasterPlaylistLoaderListeners_();
392 this.audioPlaylistLoader_ = null;
393 this.subtitlePlaylistLoader_ = null;
394
395 // setup segment loaders
396 // combined audio/video or just video when alternate audio track is selected
397 this.mainSegmentLoader_ = new _segmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
398 segmentMetadataTrack: this.segmentMetadataTrack_,
399 loaderType: 'main'
400 }), options);
401
402 // alternate audio track
403 this.audioSegmentLoader_ = new _segmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
404 loaderType: 'audio'
405 }), options);
406
407 this.subtitleSegmentLoader_ = new _vttSegmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
408 loaderType: 'vtt'
409 }), options);
410
411 this.setupSegmentLoaderListeners_();
412
413 // Create SegmentLoader stat-getters
414 loaderStats.forEach(function (stat) {
415 _this[stat + '_'] = sumLoaderStat.bind(_this, stat);
416 });
417
418 this.masterPlaylistLoader_.load();
419 }
420
421 /**
422 * Register event handlers on the master playlist loader. A helper
423 * function for construction time.
424 *
425 * @private
426 */
427
428 _createClass(MasterPlaylistController, [{
429 key: 'setupMasterPlaylistLoaderListeners_',
430 value: function setupMasterPlaylistLoaderListeners_() {
431 var _this2 = this;
432
433 this.masterPlaylistLoader_.on('loadedmetadata', function () {
434 var media = _this2.masterPlaylistLoader_.media();
435 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000;
436
437 // If we don't have any more available playlists, we don't want to
438 // timeout the request.
439 if (_this2.masterPlaylistLoader_.isLowestEnabledRendition_()) {
440 _this2.requestOptions_.timeout = 0;
441 } else {
442 _this2.requestOptions_.timeout = requestTimeout;
443 }
444
445 // if this isn't a live video and preload permits, start
446 // downloading segments
447 if (media.endList && _this2.tech_.preload() !== 'none') {
448 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_);
449 _this2.mainSegmentLoader_.load();
450 }
451
452 _this2.fillAudioTracks_();
453 _this2.setupAudio();
454
455 _this2.fillSubtitleTracks_();
456 _this2.setupSubtitles();
457
458 _this2.triggerPresenceUsage_(_this2.master(), media);
459
460 try {
461 _this2.setupSourceBuffers_();
462 } catch (e) {
463 _videoJs2['default'].log.warn('Failed to create SourceBuffers', e);
464 return _this2.mediaSource.endOfStream('decode');
465 }
466 _this2.setupFirstPlay();
467
468 _this2.trigger('audioupdate');
469 _this2.trigger('selectedinitialmedia');
470 });
471
472 this.masterPlaylistLoader_.on('loadedplaylist', function () {
473 var updatedPlaylist = _this2.masterPlaylistLoader_.media();
474
475 if (!updatedPlaylist) {
476 // select the initial variant
477 _this2.initialMedia_ = _this2.selectPlaylist();
478 _this2.masterPlaylistLoader_.media(_this2.initialMedia_);
479 return;
480 }
481
482 if (_this2.useCueTags_) {
483 _this2.updateAdCues_(updatedPlaylist);
484 }
485
486 // TODO: Create a new event on the PlaylistLoader that signals
487 // that the segments have changed in some way and use that to
488 // update the SegmentLoader instead of doing it twice here and
489 // on `mediachange`
490 _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_);
491 _this2.updateDuration();
492
493 // If the player isn't paused, ensure that the segment loader is running,
494 // as it is possible that it was temporarily stopped while waiting for
495 // a playlist (e.g., in case the playlist errored and we re-requested it).
496 if (!_this2.tech_.paused()) {
497 _this2.mainSegmentLoader_.load();
498 }
499
500 if (!updatedPlaylist.endList) {
501 (function () {
502 var addSeekableRange = function addSeekableRange() {
503 var seekable = _this2.seekable();
504
505 if (seekable.length !== 0) {
506 _this2.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
507 }
508 };
509
510 if (_this2.duration() !== Infinity) {
511 (function () {
512 var onDurationchange = function onDurationchange() {
513 if (_this2.duration() === Infinity) {
514 addSeekableRange();
515 } else {
516 _this2.tech_.one('durationchange', onDurationchange);
517 }
518 };
519
520 _this2.tech_.one('durationchange', onDurationchange);
521 })();
522 } else {
523 addSeekableRange();
524 }
525 })();
526 }
527 });
528
529 this.masterPlaylistLoader_.on('error', function () {
530 _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error);
531 });
532
533 this.masterPlaylistLoader_.on('mediachanging', function () {
534 _this2.mainSegmentLoader_.abort();
535 _this2.mainSegmentLoader_.pause();
536 });
537
538 this.masterPlaylistLoader_.on('mediachange', function () {
539 var media = _this2.masterPlaylistLoader_.media();
540 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000;
541 var activeAudioGroup = undefined;
542 var activeTrack = undefined;
543
544 // If we don't have any more available playlists, we don't want to
545 // timeout the request.
546 if (_this2.masterPlaylistLoader_.isLowestEnabledRendition_()) {
547 _this2.requestOptions_.timeout = 0;
548 } else {
549 _this2.requestOptions_.timeout = requestTimeout;
550 }
551
552 // TODO: Create a new event on the PlaylistLoader that signals
553 // that the segments have changed in some way and use that to
554 // update the SegmentLoader instead of doing it twice here and
555 // on `loadedplaylist`
556 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_);
557 _this2.mainSegmentLoader_.load();
558
559 // if the audio group has changed, a new audio track has to be
560 // enabled
561 activeAudioGroup = _this2.activeAudioGroup();
562 activeTrack = activeAudioGroup.filter(function (track) {
563 return track.enabled;
564 })[0];
565 if (!activeTrack) {
566 _this2.mediaGroupChanged();
567 _this2.trigger('audioupdate');
568 }
569 _this2.setupSubtitles();
570
571 _this2.tech_.trigger({
572 type: 'mediachange',
573 bubbles: true
574 });
575 });
576
577 this.masterPlaylistLoader_.on('playlistunchanged', function () {
578 var updatedPlaylist = _this2.masterPlaylistLoader_.media();
579 var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist);
580
581 if (playlistOutdated) {
582 // Playlist has stopped updating and we're stuck at its end. Try to
583 // blacklist it and switch to another playlist in the hope that that
584 // one is updating (and give the player a chance to re-adjust to the
585 // safe live point).
586 _this2.blacklistCurrentPlaylist({
587 message: 'Playlist no longer updating.'
588 });
589 // useful for monitoring QoS
590 _this2.tech_.trigger('playliststuck');
591 }
592 });
593
594 this.masterPlaylistLoader_.on('renditiondisabled', function () {
595 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-disabled' });
596 });
597 this.masterPlaylistLoader_.on('renditionenabled', function () {
598 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-enabled' });
599 });
600 }
601
602 /**
603 * A helper function for triggerring presence usage events once per source
604 *
605 * @private
606 */
607 }, {
608 key: 'triggerPresenceUsage_',
609 value: function triggerPresenceUsage_(master, media) {
610 var mediaGroups = master.mediaGroups || {};
611 var defaultDemuxed = true;
612 var audioGroupKeys = Object.keys(mediaGroups.AUDIO);
613
614 for (var mediaGroup in mediaGroups.AUDIO) {
615 for (var label in mediaGroups.AUDIO[mediaGroup]) {
616 var properties = mediaGroups.AUDIO[mediaGroup][label];
617
618 if (!properties.uri) {
619 defaultDemuxed = false;
620 }
621 }
622 }
623
624 if (defaultDemuxed) {
625 this.tech_.trigger({ type: 'usage', name: 'hls-demuxed' });
626 }
627
628 if (Object.keys(mediaGroups.SUBTITLES).length) {
629 this.tech_.trigger({ type: 'usage', name: 'hls-webvtt' });
630 }
631
632 if (Hls.Playlist.isAes(media)) {
633 this.tech_.trigger({ type: 'usage', name: 'hls-aes' });
634 }
635
636 if (Hls.Playlist.isFmp4(media)) {
637 this.tech_.trigger({ type: 'usage', name: 'hls-fmp4' });
638 }
639
640 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
641 this.tech_.trigger({ type: 'usage', name: 'hls-alternate-audio' });
642 }
643
644 if (this.useCueTags_) {
645 this.tech_.trigger({ type: 'usage', name: 'hls-playlist-cue-tags' });
646 }
647 }
648
649 /**
650 * Register event handlers on the segment loaders. A helper function
651 * for construction time.
652 *
653 * @private
654 */
655 }, {
656 key: 'setupSegmentLoaderListeners_',
657 value: function setupSegmentLoaderListeners_() {
658 var _this3 = this;
659
660 this.mainSegmentLoader_.on('bandwidthupdate', function () {
661 var nextPlaylist = _this3.selectPlaylist();
662 var currentPlaylist = _this3.masterPlaylistLoader_.media();
663 var buffered = _this3.tech_.buffered();
664 var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0;
665
666 var bufferLowWaterLine = _this3.bufferLowWaterLine();
667
668 // If the playlist is live, then we want to not take low water line into account.
669 // This is because in LIVE, the player plays 3 segments from the end of the
670 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
671 // in those segments, a viewer will never experience a rendition upswitch.
672 if (!currentPlaylist.endList ||
673 // For the same reason as LIVE, we ignore the low water line when the VOD
674 // duration is below the max potential low water line
675 _this3.duration() < _config2['default'].MAX_BUFFER_LOW_WATER_LINE ||
676 // we want to switch down to lower resolutions quickly to continue playback, but
677 nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH ||
678 // ensure we have some buffer before we switch up to prevent us running out of
679 // buffer while loading a higher rendition.
680 forwardBuffer >= bufferLowWaterLine) {
681 _this3.masterPlaylistLoader_.media(nextPlaylist);
682 }
683
684 _this3.tech_.trigger('bandwidthupdate');
685 });
686 this.mainSegmentLoader_.on('progress', function () {
687 _this3.trigger('progress');
688 });
689
690 this.mainSegmentLoader_.on('error', function () {
691 _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error());
692 });
693
694 this.mainSegmentLoader_.on('syncinfoupdate', function () {
695 _this3.onSyncInfoUpdate_();
696 });
697
698 this.mainSegmentLoader_.on('timestampoffset', function () {
699 _this3.tech_.trigger({ type: 'usage', name: 'hls-timestamp-offset' });
700 });
701 this.audioSegmentLoader_.on('syncinfoupdate', function () {
702 _this3.onSyncInfoUpdate_();
703 });
704
705 this.mainSegmentLoader_.on('ended', function () {
706 _this3.onEndOfStream();
707 });
708
709 this.audioSegmentLoader_.on('ended', function () {
710 _this3.onEndOfStream();
711 });
712
713 this.audioSegmentLoader_.on('error', function () {
714 _videoJs2['default'].log.warn('Problem encountered with the current alternate audio track' + '. Switching back to default.');
715 _this3.audioSegmentLoader_.abort();
716 _this3.audioPlaylistLoader_ = null;
717 _this3.setupAudio();
718 });
719
720 this.subtitleSegmentLoader_.on('error', this.handleSubtitleError_.bind(this));
721 }
722 }, {
723 key: 'handleAudioinfoUpdate_',
724 value: function handleAudioinfoUpdate_(event) {
725 if (Hls.supportsAudioInfoChange_() || !this.audioInfo_ || !objectChanged(this.audioInfo_, event.info)) {
726 this.audioInfo_ = event.info;
727 return;
728 }
729
730 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';
731
732 var enabledIndex = this.activeAudioGroup().map(function (track) {
733 return track.enabled;
734 }).indexOf(true);
735 var enabledTrack = this.activeAudioGroup()[enabledIndex];
736 var defaultTrack = this.activeAudioGroup().filter(function (track) {
737 return track.properties_ && track.properties_['default'];
738 })[0];
739
740 // they did not switch audiotracks
741 // blacklist the current playlist
742 if (!this.audioPlaylistLoader_) {
743 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!';
744 this.blacklistCurrentPlaylist();
745 } else {
746 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!');
747 defaultTrack.enabled = true;
748 this.activeAudioGroup().splice(enabledIndex, 1);
749 this.trigger('audioupdate');
750 }
751
752 _videoJs2['default'].log.warn(error);
753 this.setupAudio();
754 }
755 }, {
756 key: 'mediaSecondsLoaded_',
757 value: function mediaSecondsLoaded_() {
758 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded);
759 }
760
761 /**
762 * fill our internal list of HlsAudioTracks with data from
763 * the master playlist or use a default
764 *
765 * @private
766 */
767 }, {
768 key: 'fillAudioTracks_',
769 value: function fillAudioTracks_() {
770 var master = this.master();
771 var mediaGroups = master.mediaGroups || {};
772
773 // force a default if we have none or we are not
774 // in html5 mode (the only mode to support more than one
775 // audio track)
776 if (!mediaGroups || !mediaGroups.AUDIO || Object.keys(mediaGroups.AUDIO).length === 0 || this.mode_ !== 'html5') {
777 // "main" audio group, track name "default"
778 mediaGroups.AUDIO = { main: { 'default': { 'default': true } } };
779 }
780
781 for (var mediaGroup in mediaGroups.AUDIO) {
782 if (!this.audioGroups_[mediaGroup]) {
783 this.audioGroups_[mediaGroup] = [];
784 }
785
786 for (var label in mediaGroups.AUDIO[mediaGroup]) {
787 var properties = mediaGroups.AUDIO[mediaGroup][label];
788 var track = new _videoJs2['default'].AudioTrack({
789 id: label,
790 kind: this.audioTrackKind_(properties),
791 enabled: false,
792 language: properties.language,
793 label: label
794 });
795
796 track.properties_ = properties;
797 this.audioGroups_[mediaGroup].push(track);
798 }
799 }
800
801 // enable the default active track
802 (this.activeAudioGroup().filter(function (audioTrack) {
803 return audioTrack.properties_['default'];
804 })[0] || this.activeAudioGroup()[0]).enabled = true;
805 }
806
807 /**
808 * Convert the properties of an HLS track into an audioTrackKind.
809 *
810 * @private
811 */
812 }, {
813 key: 'audioTrackKind_',
814 value: function audioTrackKind_(properties) {
815 var kind = properties['default'] ? 'main' : 'alternative';
816
817 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
818 kind = 'main-desc';
819 }
820
821 return kind;
822 }
823
824 /**
825 * fill our internal list of Subtitle Tracks with data from
826 * the master playlist or use a default
827 *
828 * @private
829 */
830 }, {
831 key: 'fillSubtitleTracks_',
832 value: function fillSubtitleTracks_() {
833 var master = this.master();
834 var mediaGroups = master.mediaGroups || {};
835
836 for (var mediaGroup in mediaGroups.SUBTITLES) {
837 if (!this.subtitleGroups_.groups[mediaGroup]) {
838 this.subtitleGroups_.groups[mediaGroup] = [];
839 }
840
841 for (var label in mediaGroups.SUBTITLES[mediaGroup]) {
842 var properties = mediaGroups.SUBTITLES[mediaGroup][label];
843
844 if (!properties.forced) {
845 this.subtitleGroups_.groups[mediaGroup].push(_videoJs2['default'].mergeOptions({ id: label }, properties));
846
847 if (typeof this.subtitleGroups_.tracks[label] === 'undefined') {
848 var track = this.tech_.addRemoteTextTrack({
849 id: label,
850 kind: 'subtitles',
851 enabled: false,
852 language: properties.language,
853 label: label
854 }, true).track;
855
856 this.subtitleGroups_.tracks[label] = track;
857 }
858 }
859 }
860 }
861
862 // Do not enable a default subtitle track. Wait for user interaction instead.
863 }
864
865 /**
866 * Call load on our SegmentLoaders
867 */
868 }, {
869 key: 'load',
870 value: function load() {
871 this.mainSegmentLoader_.load();
872 if (this.audioPlaylistLoader_) {
873 this.audioSegmentLoader_.load();
874 }
875 if (this.subtitlePlaylistLoader_) {
876 this.subtitleSegmentLoader_.load();
877 }
878 }
879
880 /**
881 * Returns the audio group for the currently active primary
882 * media playlist.
883 */
884 }, {
885 key: 'activeAudioGroup',
886 value: function activeAudioGroup() {
887 var videoPlaylist = this.masterPlaylistLoader_.media();
888 var result = undefined;
889
890 if (videoPlaylist.attributes && videoPlaylist.attributes.AUDIO) {
891 result = this.audioGroups_[videoPlaylist.attributes.AUDIO];
892 }
893
894 return result || this.audioGroups_.main;
895 }
896
897 /**
898 * Returns the subtitle group for the currently active primary
899 * media playlist.
900 */
901 }, {
902 key: 'activeSubtitleGroup_',
903 value: function activeSubtitleGroup_() {
904 var videoPlaylist = this.masterPlaylistLoader_.media();
905 var result = undefined;
906
907 if (!videoPlaylist) {
908 return null;
909 }
910
911 if (videoPlaylist.attributes && videoPlaylist.attributes.SUBTITLES) {
912 result = this.subtitleGroups_.groups[videoPlaylist.attributes.SUBTITLES];
913 }
914
915 return result || this.subtitleGroups_.groups.main;
916 }
917 }, {
918 key: 'activeSubtitleTrack_',
919 value: function activeSubtitleTrack_() {
920 for (var trackName in this.subtitleGroups_.tracks) {
921 if (this.subtitleGroups_.tracks[trackName].mode === 'showing') {
922 return this.subtitleGroups_.tracks[trackName];
923 }
924 }
925
926 return null;
927 }
928 }, {
929 key: 'handleSubtitleError_',
930 value: function handleSubtitleError_() {
931 _videoJs2['default'].log.warn('Problem encountered loading the subtitle track' + '. Switching back to default.');
932
933 this.subtitleSegmentLoader_.abort();
934
935 var track = this.activeSubtitleTrack_();
936
937 if (track) {
938 track.mode = 'disabled';
939 }
940
941 this.setupSubtitles();
942 }
943
944 /**
945 * Determine the correct audio renditions based on the active
946 * AudioTrack and initialize a PlaylistLoader and SegmentLoader if
947 * necessary. This method is only called when the media-group changes
948 * and performs non-destructive 'resync' of the SegmentLoader(s) since
949 * the playlist has likely changed
950 */
951 }, {
952 key: 'mediaGroupChanged',
953 value: function mediaGroupChanged() {
954 var track = this.getActiveAudioTrack_();
955
956 this.stopAudioLoaders_();
957 this.resyncAudioLoaders_(track);
958 }
959
960 /**
961 * Determine the correct audio rendition based on the active
962 * AudioTrack and initialize a PlaylistLoader and SegmentLoader if
963 * necessary. This method is called once automatically before
964 * playback begins to enable the default audio track and should be
965 * invoked again if the track is changed. Performs destructive 'reset'
966 * on the SegmentLoaders(s) to ensure we start loading audio as
967 * close to currentTime as possible
968 */
969 }, {
970 key: 'setupAudio',
971 value: function setupAudio() {
972 var track = this.getActiveAudioTrack_();
973
974 this.stopAudioLoaders_();
975 this.resetAudioLoaders_(track);
976 }
977
978 /**
979 * Returns the currently active track or the default track if none
980 * are active
981 */
982 }, {
983 key: 'getActiveAudioTrack_',
984 value: function getActiveAudioTrack_() {
985 // determine whether seperate loaders are required for the audio
986 // rendition
987 var audioGroup = this.activeAudioGroup();
988 var track = audioGroup.filter(function (audioTrack) {
989 return audioTrack.enabled;
990 })[0];
991
992 if (!track) {
993 track = audioGroup.filter(function (audioTrack) {
994 return audioTrack.properties_['default'];
995 })[0] || audioGroup[0];
996 track.enabled = true;
997 }
998
999 return track;
1000 }
1001
1002 /**
1003 * Destroy the PlaylistLoader and pause the SegmentLoader specifically
1004 * for audio when switching audio tracks
1005 */
1006 }, {
1007 key: 'stopAudioLoaders_',
1008 value: function stopAudioLoaders_() {
1009 // stop playlist and segment loading for audio
1010 if (this.audioPlaylistLoader_) {
1011 this.audioPlaylistLoader_.dispose();
1012 this.audioPlaylistLoader_ = null;
1013 }
1014 this.audioSegmentLoader_.pause();
1015 }
1016
1017 /**
1018 * Destructive reset of the mainSegmentLoader (when audio is muxed)
1019 * or audioSegmentLoader (when audio is demuxed) to prepare them
1020 * to start loading new data right at currentTime
1021 */
1022 }, {
1023 key: 'resetAudioLoaders_',
1024 value: function resetAudioLoaders_(track) {
1025 if (!track.properties_.resolvedUri) {
1026 this.mainSegmentLoader_.resetEverything();
1027 return;
1028 }
1029
1030 this.audioSegmentLoader_.resetEverything();
1031 this.setupAudioPlaylistLoader_(track);
1032 }
1033
1034 /**
1035 * Non-destructive resync of the audioSegmentLoader (when audio
1036 * is demuxed) to prepare to continue appending new audio data
1037 * at the end of the current buffered region
1038 */
1039 }, {
1040 key: 'resyncAudioLoaders_',
1041 value: function resyncAudioLoaders_(track) {
1042 if (!track.properties_.resolvedUri) {
1043 return;
1044 }
1045
1046 this.audioSegmentLoader_.resyncLoader();
1047 this.setupAudioPlaylistLoader_(track);
1048 }
1049
1050 /**
1051 * Setup a new audioPlaylistLoader and start the audioSegmentLoader
1052 * to begin loading demuxed audio
1053 */
1054 }, {
1055 key: 'setupAudioPlaylistLoader_',
1056 value: function setupAudioPlaylistLoader_(track) {
1057 var _this4 = this;
1058
1059 // startup playlist and segment loaders for the enabled audio
1060 // track
1061 this.audioPlaylistLoader_ = new _playlistLoader2['default'](track.properties_.resolvedUri, this.hls_, this.withCredentials);
1062 this.audioPlaylistLoader_.load();
1063
1064 this.audioPlaylistLoader_.on('loadedmetadata', function () {
1065 var audioPlaylist = _this4.audioPlaylistLoader_.media();
1066
1067 _this4.audioSegmentLoader_.playlist(audioPlaylist, _this4.requestOptions_);
1068
1069 // if the video is already playing, or if this isn't a live video and preload
1070 // permits, start downloading segments
1071 if (!_this4.tech_.paused() || audioPlaylist.endList && _this4.tech_.preload() !== 'none') {
1072 _this4.audioSegmentLoader_.load();
1073 }
1074
1075 if (!audioPlaylist.endList) {
1076 _this4.audioPlaylistLoader_.trigger('firstplay');
1077 }
1078 });
1079
1080 this.audioPlaylistLoader_.on('loadedplaylist', function () {
1081 var updatedPlaylist = undefined;
1082
1083 if (_this4.audioPlaylistLoader_) {
1084 updatedPlaylist = _this4.audioPlaylistLoader_.media();
1085 }
1086
1087 if (!updatedPlaylist) {
1088 // only one playlist to select
1089 _this4.audioPlaylistLoader_.media(_this4.audioPlaylistLoader_.playlists.master.playlists[0]);
1090 return;
1091 }
1092
1093 _this4.audioSegmentLoader_.playlist(updatedPlaylist, _this4.requestOptions_);
1094 });
1095
1096 this.audioPlaylistLoader_.on('error', function () {
1097 _videoJs2['default'].log.warn('Problem encountered loading the alternate audio track' + '. Switching back to default.');
1098 _this4.audioSegmentLoader_.abort();
1099 _this4.setupAudio();
1100 });
1101 }
1102
1103 /**
1104 * Determine the correct subtitle playlist based on the active
1105 * SubtitleTrack and initialize a PlaylistLoader and SegmentLoader if
1106 * necessary. This method is called once automatically before
1107 * playback begins to enable the default subtitle track and should be
1108 * invoked again if the track is changed.
1109 */
1110 }, {
1111 key: 'setupSubtitles',
1112 value: function setupSubtitles() {
1113 var _this5 = this;
1114
1115 var subtitleGroup = this.activeSubtitleGroup_();
1116 var track = this.activeSubtitleTrack_();
1117
1118 this.subtitleSegmentLoader_.pause();
1119
1120 if (!track) {
1121 // stop playlist and segment loading for subtitles
1122 if (this.subtitlePlaylistLoader_) {
1123 this.subtitlePlaylistLoader_.dispose();
1124 this.subtitlePlaylistLoader_ = null;
1125 }
1126 return;
1127 }
1128
1129 var properties = subtitleGroup.filter(function (subtitleProperties) {
1130 return subtitleProperties.id === track.id;
1131 })[0];
1132
1133 // startup playlist and segment loaders for the enabled subtitle track
1134 if (!this.subtitlePlaylistLoader_ ||
1135 // if the media hasn't loaded yet, we don't have the URI to check, so it is
1136 // easiest to simply recreate the playlist loader
1137 !this.subtitlePlaylistLoader_.media() || this.subtitlePlaylistLoader_.media().resolvedUri !== properties.resolvedUri) {
1138
1139 if (this.subtitlePlaylistLoader_) {
1140 this.subtitlePlaylistLoader_.dispose();
1141 }
1142
1143 // reset the segment loader only when the subtitle playlist is changed instead of
1144 // every time setupSubtitles is called since switching subtitle tracks fires
1145 // multiple `change` events on the TextTrackList
1146 this.subtitleSegmentLoader_.resetEverything();
1147
1148 // can't reuse playlistloader because we're only using single renditions and not a
1149 // proper master
1150 this.subtitlePlaylistLoader_ = new _playlistLoader2['default'](properties.resolvedUri, this.hls_, this.withCredentials);
1151
1152 this.subtitlePlaylistLoader_.on('loadedmetadata', function () {
1153 var subtitlePlaylist = _this5.subtitlePlaylistLoader_.media();
1154
1155 _this5.subtitleSegmentLoader_.playlist(subtitlePlaylist, _this5.requestOptions_);
1156 _this5.subtitleSegmentLoader_.track(_this5.activeSubtitleTrack_());
1157
1158 // if the video is already playing, or if this isn't a live video and preload
1159 // permits, start downloading segments
1160 if (!_this5.tech_.paused() || subtitlePlaylist.endList && _this5.tech_.preload() !== 'none') {
1161 _this5.subtitleSegmentLoader_.load();
1162 }
1163 });
1164
1165 this.subtitlePlaylistLoader_.on('loadedplaylist', function () {
1166 var updatedPlaylist = undefined;
1167
1168 if (_this5.subtitlePlaylistLoader_) {
1169 updatedPlaylist = _this5.subtitlePlaylistLoader_.media();
1170 }
1171
1172 if (!updatedPlaylist) {
1173 return;
1174 }
1175
1176 _this5.subtitleSegmentLoader_.playlist(updatedPlaylist, _this5.requestOptions_);
1177 });
1178
1179 this.subtitlePlaylistLoader_.on('error', this.handleSubtitleError_.bind(this));
1180 }
1181
1182 if (this.subtitlePlaylistLoader_.media() && this.subtitlePlaylistLoader_.media().resolvedUri === properties.resolvedUri) {
1183 this.subtitleSegmentLoader_.load();
1184 } else {
1185 this.subtitlePlaylistLoader_.load();
1186 }
1187 }
1188
1189 /**
1190 * Re-tune playback quality level for the current player
1191 * conditions. This method may perform destructive actions, like
1192 * removing already buffered content, to readjust the currently
1193 * active playlist quickly.
1194 *
1195 * @private
1196 */
1197 }, {
1198 key: 'fastQualityChange_',
1199 value: function fastQualityChange_() {
1200 var media = this.selectPlaylist();
1201
1202 if (media !== this.masterPlaylistLoader_.media()) {
1203 this.masterPlaylistLoader_.media(media);
1204
1205 this.mainSegmentLoader_.resetLoader();
1206 // don't need to reset audio as it is reset when media changes
1207 }
1208 }
1209
1210 /**
1211 * Begin playback.
1212 */
1213 }, {
1214 key: 'play',
1215 value: function play() {
1216 if (this.setupFirstPlay()) {
1217 return;
1218 }
1219
1220 if (this.tech_.ended()) {
1221 this.tech_.setCurrentTime(0);
1222 }
1223
1224 if (this.hasPlayed_()) {
1225 this.load();
1226 }
1227
1228 var seekable = this.tech_.seekable();
1229
1230 // if the viewer has paused and we fell out of the live window,
1231 // seek forward to the live point
1232 if (this.tech_.duration() === Infinity) {
1233 if (this.tech_.currentTime() < seekable.start(0)) {
1234 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
1235 }
1236 }
1237 }
1238
1239 /**
1240 * Seek to the latest media position if this is a live video and the
1241 * player and video are loaded and initialized.
1242 */
1243 }, {
1244 key: 'setupFirstPlay',
1245 value: function setupFirstPlay() {
1246 var seekable = undefined;
1247 var media = this.masterPlaylistLoader_.media();
1248
1249 // check that everything is ready to begin buffering in the live
1250 // scenario
1251 // 1) the active media playlist is available
1252 if (media &&
1253 // 2) the player is not paused
1254 !this.tech_.paused() &&
1255 // 3) the player has not started playing
1256 !this.hasPlayed_()) {
1257
1258 // when the video is a live stream
1259 if (!media.endList) {
1260 this.trigger('firstplay');
1261
1262 // seek to the latest media position for live videos
1263 seekable = this.seekable();
1264 if (seekable.length) {
1265 this.tech_.setCurrentTime(seekable.end(0));
1266 }
1267 }
1268 this.hasPlayed_ = function () {
1269 return true;
1270 };
1271 // now that we are ready, load the segment
1272 this.load();
1273 return true;
1274 }
1275 return false;
1276 }
1277
1278 /**
1279 * handle the sourceopen event on the MediaSource
1280 *
1281 * @private
1282 */
1283 }, {
1284 key: 'handleSourceOpen_',
1285 value: function handleSourceOpen_() {
1286 // Only attempt to create the source buffer if none already exist.
1287 // handleSourceOpen is also called when we are "re-opening" a source buffer
1288 // after `endOfStream` has been called (in response to a seek for instance)
1289 try {
1290 this.setupSourceBuffers_();
1291 } catch (e) {
1292 _videoJs2['default'].log.warn('Failed to create Source Buffers', e);
1293 return this.mediaSource.endOfStream('decode');
1294 }
1295
1296 // if autoplay is enabled, begin playback. This is duplicative of
1297 // code in video.js but is required because play() must be invoked
1298 // *after* the media source has opened.
1299 if (this.tech_.autoplay()) {
1300 this.tech_.play();
1301 }
1302
1303 this.trigger('sourceopen');
1304 }
1305
1306 /**
1307 * Calls endOfStream on the media source when all active stream types have called
1308 * endOfStream
1309 *
1310 * @param {string} streamType
1311 * Stream type of the segment loader that called endOfStream
1312 * @private
1313 */
1314 }, {
1315 key: 'onEndOfStream',
1316 value: function onEndOfStream() {
1317 var isEndOfStream = this.mainSegmentLoader_.ended_;
1318
1319 if (this.audioPlaylistLoader_) {
1320 // if the audio playlist loader exists, then alternate audio is active, so we need
1321 // to wait for both the main and audio segment loaders to call endOfStream
1322 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
1323 }
1324
1325 if (isEndOfStream) {
1326 this.mediaSource.endOfStream();
1327 }
1328 }
1329
1330 /**
1331 * Check if a playlist has stopped being updated
1332 * @param {Object} playlist the media playlist object
1333 * @return {boolean} whether the playlist has stopped being updated or not
1334 */
1335 }, {
1336 key: 'stuckAtPlaylistEnd_',
1337 value: function stuckAtPlaylistEnd_(playlist) {
1338 var seekable = this.seekable();
1339
1340 if (!seekable.length) {
1341 // playlist doesn't have enough information to determine whether we are stuck
1342 return false;
1343 }
1344
1345 var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration);
1346
1347 if (expired === null) {
1348 return false;
1349 }
1350
1351 // does not use the safe live end to calculate playlist end, since we
1352 // don't want to say we are stuck while there is still content
1353 var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired);
1354 var currentTime = this.tech_.currentTime();
1355 var buffered = this.tech_.buffered();
1356
1357 if (!buffered.length) {
1358 // return true if the playhead reached the absolute end of the playlist
1359 return absolutePlaylistEnd - currentTime <= _ranges2['default'].TIME_FUDGE_FACTOR;
1360 }
1361 var bufferedEnd = buffered.end(buffered.length - 1);
1362
1363 // return true if there is too little buffer left and
1364 // buffer has reached absolute end of playlist
1365 return bufferedEnd - currentTime <= _ranges2['default'].TIME_FUDGE_FACTOR && absolutePlaylistEnd - bufferedEnd <= _ranges2['default'].TIME_FUDGE_FACTOR;
1366 }
1367
1368 /**
1369 * Blacklists a playlist when an error occurs for a set amount of time
1370 * making it unavailable for selection by the rendition selection algorithm
1371 * and then forces a new playlist (rendition) selection.
1372 *
1373 * @param {Object=} error an optional error that may include the playlist
1374 * to blacklist
1375 */
1376 }, {
1377 key: 'blacklistCurrentPlaylist',
1378 value: function blacklistCurrentPlaylist() {
1379 var error = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
1380
1381 var currentPlaylist = undefined;
1382 var nextPlaylist = undefined;
1383
1384 // If the `error` was generated by the playlist loader, it will contain
1385 // the playlist we were trying to load (but failed) and that should be
1386 // blacklisted instead of the currently selected playlist which is likely
1387 // out-of-date in this scenario
1388 currentPlaylist = error.playlist || this.masterPlaylistLoader_.media();
1389
1390 // If there is no current playlist, then an error occurred while we were
1391 // trying to load the master OR while we were disposing of the tech
1392 if (!currentPlaylist) {
1393 this.error = error;
1394
1395 try {
1396 return this.mediaSource.endOfStream('network');
1397 } catch (e) {
1398 return this.trigger('error');
1399 }
1400 }
1401
1402 var isFinalRendition = this.masterPlaylistLoader_.isFinalRendition_();
1403
1404 if (isFinalRendition) {
1405 // Never blacklisting this playlist because it's final rendition
1406 _videoJs2['default'].log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the final playlist.');
1407
1408 this.tech_.trigger('retryplaylist');
1409 return this.masterPlaylistLoader_.load(isFinalRendition);
1410 }
1411 // Blacklist this playlist
1412 currentPlaylist.excludeUntil = Date.now() + this.blacklistDuration * 1000;
1413 this.tech_.trigger('blacklistplaylist');
1414 this.tech_.trigger({ type: 'usage', name: 'hls-rendition-blacklisted' });
1415
1416 // Select a new playlist
1417 nextPlaylist = this.selectPlaylist();
1418 _videoJs2['default'].log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.');
1419
1420 return this.masterPlaylistLoader_.media(nextPlaylist);
1421 }
1422
1423 /**
1424 * Pause all segment loaders
1425 */
1426 }, {
1427 key: 'pauseLoading',
1428 value: function pauseLoading() {
1429 this.mainSegmentLoader_.pause();
1430 if (this.audioPlaylistLoader_) {
1431 this.audioSegmentLoader_.pause();
1432 }
1433 if (this.subtitlePlaylistLoader_) {
1434 this.subtitleSegmentLoader_.pause();
1435 }
1436 }
1437
1438 /**
1439 * set the current time on all segment loaders
1440 *
1441 * @param {TimeRange} currentTime the current time to set
1442 * @return {TimeRange} the current time
1443 */
1444 }, {
1445 key: 'setCurrentTime',
1446 value: function setCurrentTime(currentTime) {
1447 var buffered = _ranges2['default'].findRange(this.tech_.buffered(), currentTime);
1448
1449 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) {
1450 // return immediately if the metadata is not ready yet
1451 return 0;
1452 }
1453
1454 // it's clearly an edge-case but don't thrown an error if asked to
1455 // seek within an empty playlist
1456 if (!this.masterPlaylistLoader_.media().segments) {
1457 return 0;
1458 }
1459
1460 // In flash playback, the segment loaders should be reset on every seek, even
1461 // in buffer seeks
1462 var isFlash = this.mode_ === 'flash' || this.mode_ === 'auto' && !_videoJs2['default'].MediaSource.supportsNativeMediaSources();
1463
1464 // if the seek location is already buffered, continue buffering as
1465 // usual
1466 if (buffered && buffered.length && !isFlash) {
1467 return currentTime;
1468 }
1469
1470 // cancel outstanding requests so we begin buffering at the new
1471 // location
1472 this.mainSegmentLoader_.resetEverything();
1473 this.mainSegmentLoader_.abort();
1474 if (this.audioPlaylistLoader_) {
1475 this.audioSegmentLoader_.resetEverything();
1476 this.audioSegmentLoader_.abort();
1477 }
1478 if (this.subtitlePlaylistLoader_) {
1479 this.subtitleSegmentLoader_.resetEverything();
1480 this.subtitleSegmentLoader_.abort();
1481 }
1482
1483 if (!this.tech_.paused()) {
1484 this.mainSegmentLoader_.load();
1485 if (this.audioPlaylistLoader_) {
1486 this.audioSegmentLoader_.load();
1487 }
1488 if (this.subtitlePlaylistLoader_) {
1489 this.subtitleSegmentLoader_.load();
1490 }
1491 }
1492 }
1493
1494 /**
1495 * get the current duration
1496 *
1497 * @return {TimeRange} the duration
1498 */
1499 }, {
1500 key: 'duration',
1501 value: function duration() {
1502 if (!this.masterPlaylistLoader_) {
1503 return 0;
1504 }
1505
1506 if (this.mediaSource) {
1507 return this.mediaSource.duration;
1508 }
1509
1510 return Hls.Playlist.duration(this.masterPlaylistLoader_.media());
1511 }
1512
1513 /**
1514 * check the seekable range
1515 *
1516 * @return {TimeRange} the seekable range
1517 */
1518 }, {
1519 key: 'seekable',
1520 value: function seekable() {
1521 return this.seekable_;
1522 }
1523 }, {
1524 key: 'onSyncInfoUpdate_',
1525 value: function onSyncInfoUpdate_() {
1526 var mainSeekable = undefined;
1527 var audioSeekable = undefined;
1528
1529 if (!this.masterPlaylistLoader_) {
1530 return;
1531 }
1532
1533 var media = this.masterPlaylistLoader_.media();
1534
1535 if (!media) {
1536 return;
1537 }
1538
1539 var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
1540
1541 if (expired === null) {
1542 // not enough information to update seekable
1543 return;
1544 }
1545
1546 mainSeekable = Hls.Playlist.seekable(media, expired);
1547
1548 if (mainSeekable.length === 0) {
1549 return;
1550 }
1551
1552 if (this.audioPlaylistLoader_) {
1553 media = this.audioPlaylistLoader_.media();
1554 expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
1555
1556 if (expired === null) {
1557 return;
1558 }
1559
1560 audioSeekable = Hls.Playlist.seekable(media, expired);
1561
1562 if (audioSeekable.length === 0) {
1563 return;
1564 }
1565 }
1566
1567 if (!audioSeekable) {
1568 // seekable has been calculated based on buffering video data so it
1569 // can be returned directly
1570 this.seekable_ = mainSeekable;
1571 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) {
1572 // seekables are pretty far off, rely on main
1573 this.seekable_ = mainSeekable;
1574 } else {
1575 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)]]);
1576 }
1577
1578 this.tech_.trigger('seekablechanged');
1579 }
1580
1581 /**
1582 * Update the player duration
1583 */
1584 }, {
1585 key: 'updateDuration',
1586 value: function updateDuration() {
1587 var _this6 = this;
1588
1589 var oldDuration = this.mediaSource.duration;
1590 var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media());
1591 var buffered = this.tech_.buffered();
1592 var setDuration = function setDuration() {
1593 _this6.mediaSource.duration = newDuration;
1594 _this6.tech_.trigger('durationchange');
1595
1596 _this6.mediaSource.removeEventListener('sourceopen', setDuration);
1597 };
1598
1599 if (buffered.length > 0) {
1600 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
1601 }
1602
1603 // if the duration has changed, invalidate the cached value
1604 if (oldDuration !== newDuration) {
1605 // update the duration
1606 if (this.mediaSource.readyState !== 'open') {
1607 this.mediaSource.addEventListener('sourceopen', setDuration);
1608 } else {
1609 setDuration();
1610 }
1611 }
1612 }
1613
1614 /**
1615 * dispose of the MasterPlaylistController and everything
1616 * that it controls
1617 */
1618 }, {
1619 key: 'dispose',
1620 value: function dispose() {
1621 this.decrypter_.terminate();
1622 this.masterPlaylistLoader_.dispose();
1623 this.mainSegmentLoader_.dispose();
1624
1625 if (this.audioPlaylistLoader_) {
1626 this.audioPlaylistLoader_.dispose();
1627 }
1628 if (this.subtitlePlaylistLoader_) {
1629 this.subtitlePlaylistLoader_.dispose();
1630 }
1631 this.audioSegmentLoader_.dispose();
1632 this.subtitleSegmentLoader_.dispose();
1633 }
1634
1635 /**
1636 * return the master playlist object if we have one
1637 *
1638 * @return {Object} the master playlist object that we parsed
1639 */
1640 }, {
1641 key: 'master',
1642 value: function master() {
1643 return this.masterPlaylistLoader_.master;
1644 }
1645
1646 /**
1647 * return the currently selected playlist
1648 *
1649 * @return {Object} the currently selected playlist object that we parsed
1650 */
1651 }, {
1652 key: 'media',
1653 value: function media() {
1654 // playlist loader will not return media if it has not been fully loaded
1655 return this.masterPlaylistLoader_.media() || this.initialMedia_;
1656 }
1657
1658 /**
1659 * setup our internal source buffers on our segment Loaders
1660 *
1661 * @private
1662 */
1663 }, {
1664 key: 'setupSourceBuffers_',
1665 value: function setupSourceBuffers_() {
1666 var media = this.masterPlaylistLoader_.media();
1667 var mimeTypes = undefined;
1668
1669 // wait until a media playlist is available and the Media Source is
1670 // attached
1671 if (!media || this.mediaSource.readyState !== 'open') {
1672 return;
1673 }
1674
1675 mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media);
1676 if (mimeTypes.length < 1) {
1677 this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri;
1678 return this.mediaSource.endOfStream('decode');
1679 }
1680 this.mainSegmentLoader_.mimeType(mimeTypes[0]);
1681 if (mimeTypes[1]) {
1682 this.audioSegmentLoader_.mimeType(mimeTypes[1]);
1683 }
1684
1685 // exclude any incompatible variant streams from future playlist
1686 // selection
1687 this.excludeIncompatibleVariants_(media);
1688 }
1689
1690 /**
1691 * Blacklist playlists that are known to be codec or
1692 * stream-incompatible with the SourceBuffer configuration. For
1693 * instance, Media Source Extensions would cause the video element to
1694 * stall waiting for video data if you switched from a variant with
1695 * video and audio to an audio-only one.
1696 *
1697 * @param {Object} media a media playlist compatible with the current
1698 * set of SourceBuffers. Variants in the current master playlist that
1699 * do not appear to have compatible codec or stream configurations
1700 * will be excluded from the default playlist selection algorithm
1701 * indefinitely.
1702 * @private
1703 */
1704 }, {
1705 key: 'excludeIncompatibleVariants_',
1706 value: function excludeIncompatibleVariants_(media) {
1707 var master = this.masterPlaylistLoader_.master;
1708 var codecCount = 2;
1709 var videoCodec = null;
1710 var codecs = undefined;
1711
1712 if (media.attributes && media.attributes.CODECS) {
1713 codecs = parseCodecs(media.attributes.CODECS);
1714 videoCodec = codecs.videoCodec;
1715 codecCount = codecs.codecCount;
1716 }
1717 master.playlists.forEach(function (variant) {
1718 var variantCodecs = {
1719 codecCount: 2,
1720 videoCodec: null
1721 };
1722
1723 if (variant.attributes && variant.attributes.CODECS) {
1724 var codecString = variant.attributes.CODECS;
1725
1726 variantCodecs = parseCodecs(codecString);
1727
1728 if (window.MediaSource && window.MediaSource.isTypeSupported && !window.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs_(codecString) + '"')) {
1729 variant.excludeUntil = Infinity;
1730 }
1731 }
1732
1733 // if the streams differ in the presence or absence of audio or
1734 // video, they are incompatible
1735 if (variantCodecs.codecCount !== codecCount) {
1736 variant.excludeUntil = Infinity;
1737 }
1738
1739 // if h.264 is specified on the current playlist, some flavor of
1740 // it must be specified on all compatible variants
1741 if (variantCodecs.videoCodec !== videoCodec) {
1742 variant.excludeUntil = Infinity;
1743 }
1744 });
1745 }
1746 }, {
1747 key: 'updateAdCues_',
1748 value: function updateAdCues_(media) {
1749 var offset = 0;
1750 var seekable = this.seekable();
1751
1752 if (seekable.length) {
1753 offset = seekable.start(0);
1754 }
1755
1756 _adCueTags2['default'].updateAdCues(media, this.cueTagsTrack_, offset);
1757 }
1758
1759 /**
1760 * Calculates the desired forward buffer length based on current time
1761 *
1762 * @return {Number} Desired forward buffer length in seconds
1763 */
1764 }, {
1765 key: 'goalBufferLength',
1766 value: function goalBufferLength() {
1767 var currentTime = this.tech_.currentTime();
1768 var initial = _config2['default'].GOAL_BUFFER_LENGTH;
1769 var rate = _config2['default'].GOAL_BUFFER_LENGTH_RATE;
1770 var max = Math.max(initial, _config2['default'].MAX_GOAL_BUFFER_LENGTH);
1771
1772 return Math.min(initial + currentTime * rate, max);
1773 }
1774
1775 /**
1776 * Calculates the desired buffer low water line based on current time
1777 *
1778 * @return {Number} Desired buffer low water line in seconds
1779 */
1780 }, {
1781 key: 'bufferLowWaterLine',
1782 value: function bufferLowWaterLine() {
1783 var currentTime = this.tech_.currentTime();
1784 var initial = _config2['default'].BUFFER_LOW_WATER_LINE;
1785 var rate = _config2['default'].BUFFER_LOW_WATER_LINE_RATE;
1786 var max = Math.max(initial, _config2['default'].MAX_BUFFER_LOW_WATER_LINE);
1787
1788 return Math.min(initial + currentTime * rate, max);
1789 }
1790 }]);
1791
1792 return MasterPlaylistController;
1793})(_videoJs2['default'].EventTarget);
1794
1795exports.MasterPlaylistController = MasterPlaylistController;
\No newline at end of file