UNPKG

46.7 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(_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); } } };
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 _utilCodecsJs = require('./util/codecs.js');
63
64var _mediaGroups = require('./media-groups');
65
66var ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2;
67
68var Hls = undefined;
69
70// Default codec parameters if none were provided for video and/or audio
71var defaultCodecs = {
72 videoCodec: 'avc1',
73 videoObjectTypeIndicator: '.4d400d',
74 // AAC-LC
75 audioProfile: '2'
76};
77
78// SegmentLoader stats that need to have each loader's
79// values summed to calculate the final value
80var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred'];
81var sumLoaderStat = function sumLoaderStat(stat) {
82 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat];
83};
84
85/**
86 * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the
87 * standard `avc1.<hhhhhh>`.
88 *
89 * @param codecString {String} the codec string
90 * @return {String} the codec string with old apple-style codecs replaced
91 *
92 * @private
93 */
94var mapLegacyAvcCodecs_ = function mapLegacyAvcCodecs_(codecString) {
95 return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) {
96 return (0, _videojsContribMediaSourcesEs5CodecUtils.translateLegacyCodecs)([match])[0];
97 });
98};
99
100exports.mapLegacyAvcCodecs_ = mapLegacyAvcCodecs_;
101/**
102 * Build a media mime-type string from a set of parameters
103 * @param {String} type either 'audio' or 'video'
104 * @param {String} container either 'mp2t' or 'mp4'
105 * @param {Array} codecs an array of codec strings to add
106 * @return {String} a valid media mime-type
107 */
108var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) {
109 // The codecs array is filtered so that falsey values are
110 // dropped and don't cause Array#join to create spurious
111 // commas
112 return type + '/' + container + '; codecs="' + codecs.filter(function (c) {
113 return !!c;
114 }).join(', ') + '"';
115};
116
117/**
118 * Returns the type container based on information in the playlist
119 * @param {Playlist} media the current media playlist
120 * @return {String} a valid media container type
121 */
122var getContainerType = function getContainerType(media) {
123 // An initialization segment means the media playlist is an iframe
124 // playlist or is using the mp4 container. We don't currently
125 // support iframe playlists, so assume this is signalling mp4
126 // fragments.
127 if (media.segments && media.segments.length && media.segments[0].map) {
128 return 'mp4';
129 }
130 return 'mp2t';
131};
132
133/**
134 * Returns a set of codec strings parsed from the playlist or the default
135 * codec strings if no codecs were specified in the playlist
136 * @param {Playlist} media the current media playlist
137 * @return {Object} an object with the video and audio codecs
138 */
139var getCodecs = function getCodecs(media) {
140 // if the codecs were explicitly specified, use them instead of the
141 // defaults
142 var mediaAttributes = media.attributes || {};
143
144 if (mediaAttributes.CODECS) {
145 return (0, _utilCodecsJs.parseCodecs)(mediaAttributes.CODECS);
146 }
147 return defaultCodecs;
148};
149
150/**
151 * Calculates the MIME type strings for a working configuration of
152 * SourceBuffers to play variant streams in a master playlist. If
153 * there is no possible working configuration, an empty array will be
154 * returned.
155 *
156 * @param master {Object} the m3u8 object for the master playlist
157 * @param media {Object} the m3u8 object for the variant playlist
158 * @return {Array} the MIME type strings. If the array has more than
159 * one entry, the first element should be applied to the video
160 * SourceBuffer and the second to the audio SourceBuffer.
161 *
162 * @private
163 */
164var mimeTypesForPlaylist_ = function mimeTypesForPlaylist_(master, media) {
165 var containerType = getContainerType(media);
166 var codecInfo = getCodecs(media);
167 var mediaAttributes = media.attributes || {};
168 // Default condition for a traditional HLS (no demuxed audio/video)
169 var isMuxed = true;
170 var isMaat = false;
171
172 if (!media) {
173 // Not enough information
174 return [];
175 }
176
177 if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) {
178 var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO];
179
180 // Handle the case where we are in a multiple-audio track scenario
181 if (audioGroup) {
182 isMaat = true;
183 // Start with the everything demuxed then...
184 isMuxed = false;
185 // ...check to see if any audio group tracks are muxed (ie. lacking a uri)
186 for (var groupId in audioGroup) {
187 if (!audioGroup[groupId].uri) {
188 isMuxed = true;
189 break;
190 }
191 }
192 }
193 }
194
195 // HLS with multiple-audio tracks must always get an audio codec.
196 // Put another way, there is no way to have a video-only multiple-audio HLS!
197 if (isMaat && !codecInfo.audioProfile) {
198 _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)');
199 codecInfo.audioProfile = defaultCodecs.audioProfile;
200 }
201
202 // Generate the final codec strings from the codec object generated above
203 var codecStrings = {};
204
205 if (codecInfo.videoCodec) {
206 codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator;
207 }
208
209 if (codecInfo.audioProfile) {
210 codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile;
211 }
212
213 // Finally, make and return an array with proper mime-types depending on
214 // the configuration
215 var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]);
216 var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]);
217 var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]);
218
219 if (isMaat) {
220 if (!isMuxed && codecStrings.video) {
221 return [justVideo, justAudio];
222 }
223 // There exists the possiblity that this will return a `video/container`
224 // mime-type for the first entry in the array even when there is only audio.
225 // This doesn't appear to be a problem and simplifies the code.
226 return [bothVideoAudio, justAudio];
227 }
228
229 // If there is ano video codec at all, always just return a single
230 // audio/<container> mime-type
231 if (!codecStrings.video) {
232 return [justAudio];
233 }
234
235 // When not using separate audio media groups, audio and video is
236 // *always* muxed
237 return [bothVideoAudio];
238};
239
240exports.mimeTypesForPlaylist_ = mimeTypesForPlaylist_;
241/**
242 * the master playlist controller controller all interactons
243 * between playlists and segmentloaders. At this time this mainly
244 * involves a master playlist and a series of audio playlists
245 * if they are available
246 *
247 * @class MasterPlaylistController
248 * @extends videojs.EventTarget
249 */
250
251var MasterPlaylistController = (function (_videojs$EventTarget) {
252 _inherits(MasterPlaylistController, _videojs$EventTarget);
253
254 function MasterPlaylistController(options) {
255 var _this = this;
256
257 _classCallCheck(this, MasterPlaylistController);
258
259 _get(Object.getPrototypeOf(MasterPlaylistController.prototype), 'constructor', this).call(this);
260
261 var url = options.url;
262 var withCredentials = options.withCredentials;
263 var mode = options.mode;
264 var tech = options.tech;
265 var bandwidth = options.bandwidth;
266 var externHls = options.externHls;
267 var useCueTags = options.useCueTags;
268 var blacklistDuration = options.blacklistDuration;
269 var enableLowInitialPlaylist = options.enableLowInitialPlaylist;
270
271 if (!url) {
272 throw new Error('A non-empty playlist URL is required');
273 }
274
275 Hls = externHls;
276
277 this.withCredentials = withCredentials;
278 this.tech_ = tech;
279 this.hls_ = tech.hls;
280 this.mode_ = mode;
281 this.useCueTags_ = useCueTags;
282 this.blacklistDuration = blacklistDuration;
283 this.enableLowInitialPlaylist = enableLowInitialPlaylist;
284 if (this.useCueTags_) {
285 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues');
286 this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
287 }
288
289 this.requestOptions_ = {
290 withCredentials: this.withCredentials,
291 timeout: null
292 };
293
294 this.mediaTypes_ = (0, _mediaGroups.createMediaTypes)();
295
296 this.mediaSource = new _videoJs2['default'].MediaSource({ mode: mode });
297
298 // load the media source into the player
299 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_.bind(this));
300
301 this.seekable_ = _videoJs2['default'].createTimeRanges();
302 this.hasPlayed_ = function () {
303 return false;
304 };
305
306 this.syncController_ = new _syncController2['default'](options);
307 this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
308 kind: 'metadata',
309 label: 'segment-metadata'
310 }, false).track;
311
312 this.decrypter_ = (0, _webworkify2['default'])(_decrypterWorker2['default']);
313
314 var segmentLoaderSettings = {
315 hls: this.hls_,
316 mediaSource: this.mediaSource,
317 currentTime: this.tech_.currentTime.bind(this.tech_),
318 seekable: function seekable() {
319 return _this.seekable();
320 },
321 seeking: function seeking() {
322 return _this.tech_.seeking();
323 },
324 duration: function duration() {
325 return _this.mediaSource.duration;
326 },
327 hasPlayed: function hasPlayed() {
328 return _this.hasPlayed_();
329 },
330 goalBufferLength: function goalBufferLength() {
331 return _this.goalBufferLength();
332 },
333 bandwidth: bandwidth,
334 syncController: this.syncController_,
335 decrypter: this.decrypter_
336 };
337
338 // setup playlist loaders
339 this.masterPlaylistLoader_ = new _playlistLoader2['default'](url, this.hls_, this.withCredentials);
340 this.setupMasterPlaylistLoaderListeners_();
341
342 // setup segment loaders
343 // combined audio/video or just video when alternate audio track is selected
344 this.mainSegmentLoader_ = new _segmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
345 segmentMetadataTrack: this.segmentMetadataTrack_,
346 loaderType: 'main'
347 }), options);
348
349 // alternate audio track
350 this.audioSegmentLoader_ = new _segmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
351 loaderType: 'audio'
352 }), options);
353
354 this.subtitleSegmentLoader_ = new _vttSegmentLoader2['default'](_videoJs2['default'].mergeOptions(segmentLoaderSettings, {
355 loaderType: 'vtt'
356 }), options);
357
358 this.setupSegmentLoaderListeners_();
359
360 // Create SegmentLoader stat-getters
361 loaderStats.forEach(function (stat) {
362 _this[stat + '_'] = sumLoaderStat.bind(_this, stat);
363 });
364
365 this.masterPlaylistLoader_.load();
366 }
367
368 /**
369 * Register event handlers on the master playlist loader. A helper
370 * function for construction time.
371 *
372 * @private
373 */
374
375 _createClass(MasterPlaylistController, [{
376 key: 'setupMasterPlaylistLoaderListeners_',
377 value: function setupMasterPlaylistLoaderListeners_() {
378 var _this2 = this;
379
380 this.masterPlaylistLoader_.on('loadedmetadata', function () {
381 var media = _this2.masterPlaylistLoader_.media();
382 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000;
383
384 // If we don't have any more available playlists, we don't want to
385 // timeout the request.
386 if (_this2.masterPlaylistLoader_.isLowestEnabledRendition_()) {
387 _this2.requestOptions_.timeout = 0;
388 } else {
389 _this2.requestOptions_.timeout = requestTimeout;
390 }
391
392 // if this isn't a live video and preload permits, start
393 // downloading segments
394 if (media.endList && _this2.tech_.preload() !== 'none') {
395 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_);
396 _this2.mainSegmentLoader_.load();
397 }
398
399 (0, _mediaGroups.setupMediaGroups)({
400 segmentLoaders: {
401 AUDIO: _this2.audioSegmentLoader_,
402 SUBTITLES: _this2.subtitleSegmentLoader_,
403 main: _this2.mainSegmentLoader_
404 },
405 tech: _this2.tech_,
406 requestOptions: _this2.requestOptions_,
407 masterPlaylistLoader: _this2.masterPlaylistLoader_,
408 mode: _this2.mode_,
409 hls: _this2.hls_,
410 master: _this2.master(),
411 mediaTypes: _this2.mediaTypes_,
412 blacklistCurrentPlaylist: _this2.blacklistCurrentPlaylist.bind(_this2)
413 });
414
415 _this2.triggerPresenceUsage_(_this2.master(), media);
416
417 try {
418 _this2.setupSourceBuffers_();
419 } catch (e) {
420 _videoJs2['default'].log.warn('Failed to create SourceBuffers', e);
421 return _this2.mediaSource.endOfStream('decode');
422 }
423 _this2.setupFirstPlay();
424
425 _this2.trigger('selectedinitialmedia');
426 });
427
428 this.masterPlaylistLoader_.on('loadedplaylist', function () {
429 var updatedPlaylist = _this2.masterPlaylistLoader_.media();
430
431 if (!updatedPlaylist) {
432 var selectedMedia = undefined;
433
434 if (_this2.enableLowInitialPlaylist) {
435 selectedMedia = _this2.selectInitialPlaylist();
436 }
437
438 if (!selectedMedia) {
439 selectedMedia = _this2.selectPlaylist();
440 }
441
442 _this2.initialMedia_ = selectedMedia;
443 _this2.masterPlaylistLoader_.media(_this2.initialMedia_);
444 return;
445 }
446
447 if (_this2.useCueTags_) {
448 _this2.updateAdCues_(updatedPlaylist);
449 }
450
451 // TODO: Create a new event on the PlaylistLoader that signals
452 // that the segments have changed in some way and use that to
453 // update the SegmentLoader instead of doing it twice here and
454 // on `mediachange`
455 _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_);
456 _this2.updateDuration();
457
458 // If the player isn't paused, ensure that the segment loader is running,
459 // as it is possible that it was temporarily stopped while waiting for
460 // a playlist (e.g., in case the playlist errored and we re-requested it).
461 if (!_this2.tech_.paused()) {
462 _this2.mainSegmentLoader_.load();
463 }
464
465 if (!updatedPlaylist.endList) {
466 (function () {
467 var addSeekableRange = function addSeekableRange() {
468 var seekable = _this2.seekable();
469
470 if (seekable.length !== 0) {
471 _this2.mediaSource.addSeekableRange_(seekable.start(0), seekable.end(0));
472 }
473 };
474
475 if (_this2.duration() !== Infinity) {
476 (function () {
477 var onDurationchange = function onDurationchange() {
478 if (_this2.duration() === Infinity) {
479 addSeekableRange();
480 } else {
481 _this2.tech_.one('durationchange', onDurationchange);
482 }
483 };
484
485 _this2.tech_.one('durationchange', onDurationchange);
486 })();
487 } else {
488 addSeekableRange();
489 }
490 })();
491 }
492 });
493
494 this.masterPlaylistLoader_.on('error', function () {
495 _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error);
496 });
497
498 this.masterPlaylistLoader_.on('mediachanging', function () {
499 _this2.mainSegmentLoader_.abort();
500 _this2.mainSegmentLoader_.pause();
501 });
502
503 this.masterPlaylistLoader_.on('mediachange', function () {
504 var media = _this2.masterPlaylistLoader_.media();
505 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000;
506
507 // If we don't have any more available playlists, we don't want to
508 // timeout the request.
509 if (_this2.masterPlaylistLoader_.isLowestEnabledRendition_()) {
510 _this2.requestOptions_.timeout = 0;
511 } else {
512 _this2.requestOptions_.timeout = requestTimeout;
513 }
514
515 // TODO: Create a new event on the PlaylistLoader that signals
516 // that the segments have changed in some way and use that to
517 // update the SegmentLoader instead of doing it twice here and
518 // on `loadedplaylist`
519 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_);
520 _this2.mainSegmentLoader_.load();
521
522 _this2.tech_.trigger({
523 type: 'mediachange',
524 bubbles: true
525 });
526 });
527
528 this.masterPlaylistLoader_.on('playlistunchanged', function () {
529 var updatedPlaylist = _this2.masterPlaylistLoader_.media();
530 var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist);
531
532 if (playlistOutdated) {
533 // Playlist has stopped updating and we're stuck at its end. Try to
534 // blacklist it and switch to another playlist in the hope that that
535 // one is updating (and give the player a chance to re-adjust to the
536 // safe live point).
537 _this2.blacklistCurrentPlaylist({
538 message: 'Playlist no longer updating.'
539 });
540 // useful for monitoring QoS
541 _this2.tech_.trigger('playliststuck');
542 }
543 });
544
545 this.masterPlaylistLoader_.on('renditiondisabled', function () {
546 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-disabled' });
547 });
548 this.masterPlaylistLoader_.on('renditionenabled', function () {
549 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-enabled' });
550 });
551 }
552
553 /**
554 * A helper function for triggerring presence usage events once per source
555 *
556 * @private
557 */
558 }, {
559 key: 'triggerPresenceUsage_',
560 value: function triggerPresenceUsage_(master, media) {
561 var mediaGroups = master.mediaGroups || {};
562 var defaultDemuxed = true;
563 var audioGroupKeys = Object.keys(mediaGroups.AUDIO);
564
565 for (var mediaGroup in mediaGroups.AUDIO) {
566 for (var label in mediaGroups.AUDIO[mediaGroup]) {
567 var properties = mediaGroups.AUDIO[mediaGroup][label];
568
569 if (!properties.uri) {
570 defaultDemuxed = false;
571 }
572 }
573 }
574
575 if (defaultDemuxed) {
576 this.tech_.trigger({ type: 'usage', name: 'hls-demuxed' });
577 }
578
579 if (Object.keys(mediaGroups.SUBTITLES).length) {
580 this.tech_.trigger({ type: 'usage', name: 'hls-webvtt' });
581 }
582
583 if (Hls.Playlist.isAes(media)) {
584 this.tech_.trigger({ type: 'usage', name: 'hls-aes' });
585 }
586
587 if (Hls.Playlist.isFmp4(media)) {
588 this.tech_.trigger({ type: 'usage', name: 'hls-fmp4' });
589 }
590
591 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
592 this.tech_.trigger({ type: 'usage', name: 'hls-alternate-audio' });
593 }
594
595 if (this.useCueTags_) {
596 this.tech_.trigger({ type: 'usage', name: 'hls-playlist-cue-tags' });
597 }
598 }
599
600 /**
601 * Register event handlers on the segment loaders. A helper function
602 * for construction time.
603 *
604 * @private
605 */
606 }, {
607 key: 'setupSegmentLoaderListeners_',
608 value: function setupSegmentLoaderListeners_() {
609 var _this3 = this;
610
611 this.mainSegmentLoader_.on('bandwidthupdate', function () {
612 var nextPlaylist = _this3.selectPlaylist();
613 var currentPlaylist = _this3.masterPlaylistLoader_.media();
614 var buffered = _this3.tech_.buffered();
615 var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0;
616
617 var bufferLowWaterLine = _this3.bufferLowWaterLine();
618
619 // If the playlist is live, then we want to not take low water line into account.
620 // This is because in LIVE, the player plays 3 segments from the end of the
621 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
622 // in those segments, a viewer will never experience a rendition upswitch.
623 if (!currentPlaylist.endList ||
624 // For the same reason as LIVE, we ignore the low water line when the VOD
625 // duration is below the max potential low water line
626 _this3.duration() < _config2['default'].MAX_BUFFER_LOW_WATER_LINE ||
627 // we want to switch down to lower resolutions quickly to continue playback, but
628 nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH ||
629 // ensure we have some buffer before we switch up to prevent us running out of
630 // buffer while loading a higher rendition.
631 forwardBuffer >= bufferLowWaterLine) {
632 _this3.masterPlaylistLoader_.media(nextPlaylist);
633 }
634
635 _this3.tech_.trigger('bandwidthupdate');
636 });
637 this.mainSegmentLoader_.on('progress', function () {
638 _this3.trigger('progress');
639 });
640
641 this.mainSegmentLoader_.on('error', function () {
642 _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error());
643 });
644
645 this.mainSegmentLoader_.on('syncinfoupdate', function () {
646 _this3.onSyncInfoUpdate_();
647 });
648
649 this.mainSegmentLoader_.on('timestampoffset', function () {
650 _this3.tech_.trigger({ type: 'usage', name: 'hls-timestamp-offset' });
651 });
652 this.audioSegmentLoader_.on('syncinfoupdate', function () {
653 _this3.onSyncInfoUpdate_();
654 });
655
656 this.mainSegmentLoader_.on('ended', function () {
657 _this3.onEndOfStream();
658 });
659
660 this.mainSegmentLoader_.on('earlyabort', function () {
661 _this3.blacklistCurrentPlaylist({
662 message: 'Aborted early because there isn\'t enough bandwidth to complete the ' + 'request without rebuffering.'
663 }, ABORT_EARLY_BLACKLIST_SECONDS);
664 });
665
666 this.mainSegmentLoader_.on('reseteverything', function () {
667 // If playing an MTS stream, a videojs.MediaSource is listening for
668 // hls-reset to reset caption parsing state in the transmuxer
669 _this3.tech_.trigger('hls-reset');
670 });
671
672 this.mainSegmentLoader_.on('segmenttimemapping', function (event) {
673 // If playing an MTS stream in html, a videojs.MediaSource is listening for
674 // hls-segment-time-mapping update its internal mapping of stream to display time
675 _this3.tech_.trigger({
676 type: 'hls-segment-time-mapping',
677 mapping: event.mapping
678 });
679 });
680
681 this.audioSegmentLoader_.on('ended', function () {
682 _this3.onEndOfStream();
683 });
684 }
685 }, {
686 key: 'mediaSecondsLoaded_',
687 value: function mediaSecondsLoaded_() {
688 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded);
689 }
690
691 /**
692 * Call load on our SegmentLoaders
693 */
694 }, {
695 key: 'load',
696 value: function load() {
697 this.mainSegmentLoader_.load();
698 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
699 this.audioSegmentLoader_.load();
700 }
701 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
702 this.subtitleSegmentLoader_.load();
703 }
704 }
705
706 /**
707 * Re-tune playback quality level for the current player
708 * conditions. This method may perform destructive actions, like
709 * removing already buffered content, to readjust the currently
710 * active playlist quickly.
711 *
712 * @private
713 */
714 }, {
715 key: 'fastQualityChange_',
716 value: function fastQualityChange_() {
717 var media = this.selectPlaylist();
718
719 if (media !== this.masterPlaylistLoader_.media()) {
720 this.masterPlaylistLoader_.media(media);
721
722 this.mainSegmentLoader_.resetLoader();
723 // don't need to reset audio as it is reset when media changes
724 }
725 }
726
727 /**
728 * Begin playback.
729 */
730 }, {
731 key: 'play',
732 value: function play() {
733 if (this.setupFirstPlay()) {
734 return;
735 }
736
737 if (this.tech_.ended()) {
738 this.tech_.setCurrentTime(0);
739 }
740
741 if (this.hasPlayed_()) {
742 this.load();
743 }
744
745 var seekable = this.tech_.seekable();
746
747 // if the viewer has paused and we fell out of the live window,
748 // seek forward to the live point
749 if (this.tech_.duration() === Infinity) {
750 if (this.tech_.currentTime() < seekable.start(0)) {
751 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
752 }
753 }
754 }
755
756 /**
757 * Seek to the latest media position if this is a live video and the
758 * player and video are loaded and initialized.
759 */
760 }, {
761 key: 'setupFirstPlay',
762 value: function setupFirstPlay() {
763 var _this4 = this;
764
765 var media = this.masterPlaylistLoader_.media();
766
767 // Check that everything is ready to begin buffering for the first call to play
768 // If 1) there is no active media
769 // 2) the player is paused
770 // 3) the first play has already been setup
771 // then exit early
772 if (!media || this.tech_.paused() || this.hasPlayed_()) {
773 return false;
774 }
775
776 // when the video is a live stream
777 if (!media.endList) {
778 var _ret3 = (function () {
779 var seekable = _this4.seekable();
780
781 if (!seekable.length) {
782 // without a seekable range, the player cannot seek to begin buffering at the live
783 // point
784 return {
785 v: false
786 };
787 }
788
789 if (_videoJs2['default'].browser.IE_VERSION && _this4.mode_ === 'html5' && _this4.tech_.readyState() === 0) {
790 // IE11 throws an InvalidStateError if you try to set currentTime while the
791 // readyState is 0, so it must be delayed until the tech fires loadedmetadata.
792 _this4.tech_.one('loadedmetadata', function () {
793 _this4.trigger('firstplay');
794 _this4.tech_.setCurrentTime(seekable.end(0));
795 _this4.hasPlayed_ = function () {
796 return true;
797 };
798 });
799
800 return {
801 v: false
802 };
803 }
804
805 // trigger firstplay to inform the source handler to ignore the next seek event
806 _this4.trigger('firstplay');
807 // seek to the live point
808 _this4.tech_.setCurrentTime(seekable.end(0));
809 })();
810
811 if (typeof _ret3 === 'object') return _ret3.v;
812 }
813
814 this.hasPlayed_ = function () {
815 return true;
816 };
817 // we can begin loading now that everything is ready
818 this.load();
819 return true;
820 }
821
822 /**
823 * handle the sourceopen event on the MediaSource
824 *
825 * @private
826 */
827 }, {
828 key: 'handleSourceOpen_',
829 value: function handleSourceOpen_() {
830 // Only attempt to create the source buffer if none already exist.
831 // handleSourceOpen is also called when we are "re-opening" a source buffer
832 // after `endOfStream` has been called (in response to a seek for instance)
833 try {
834 this.setupSourceBuffers_();
835 } catch (e) {
836 _videoJs2['default'].log.warn('Failed to create Source Buffers', e);
837 return this.mediaSource.endOfStream('decode');
838 }
839
840 // if autoplay is enabled, begin playback. This is duplicative of
841 // code in video.js but is required because play() must be invoked
842 // *after* the media source has opened.
843 if (this.tech_.autoplay()) {
844 this.tech_.play();
845 }
846
847 this.trigger('sourceopen');
848 }
849
850 /**
851 * Calls endOfStream on the media source when all active stream types have called
852 * endOfStream
853 *
854 * @param {string} streamType
855 * Stream type of the segment loader that called endOfStream
856 * @private
857 */
858 }, {
859 key: 'onEndOfStream',
860 value: function onEndOfStream() {
861 var isEndOfStream = this.mainSegmentLoader_.ended_;
862
863 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
864 // if the audio playlist loader exists, then alternate audio is active, so we need
865 // to wait for both the main and audio segment loaders to call endOfStream
866 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
867 }
868
869 if (isEndOfStream) {
870 this.mediaSource.endOfStream();
871 }
872 }
873
874 /**
875 * Check if a playlist has stopped being updated
876 * @param {Object} playlist the media playlist object
877 * @return {boolean} whether the playlist has stopped being updated or not
878 */
879 }, {
880 key: 'stuckAtPlaylistEnd_',
881 value: function stuckAtPlaylistEnd_(playlist) {
882 var seekable = this.seekable();
883
884 if (!seekable.length) {
885 // playlist doesn't have enough information to determine whether we are stuck
886 return false;
887 }
888
889 var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration);
890
891 if (expired === null) {
892 return false;
893 }
894
895 // does not use the safe live end to calculate playlist end, since we
896 // don't want to say we are stuck while there is still content
897 var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired);
898 var currentTime = this.tech_.currentTime();
899 var buffered = this.tech_.buffered();
900
901 if (!buffered.length) {
902 // return true if the playhead reached the absolute end of the playlist
903 return absolutePlaylistEnd - currentTime <= _ranges2['default'].SAFE_TIME_DELTA;
904 }
905 var bufferedEnd = buffered.end(buffered.length - 1);
906
907 // return true if there is too little buffer left and buffer has reached absolute
908 // end of playlist
909 return bufferedEnd - currentTime <= _ranges2['default'].SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= _ranges2['default'].SAFE_TIME_DELTA;
910 }
911
912 /**
913 * Blacklists a playlist when an error occurs for a set amount of time
914 * making it unavailable for selection by the rendition selection algorithm
915 * and then forces a new playlist (rendition) selection.
916 *
917 * @param {Object=} error an optional error that may include the playlist
918 * to blacklist
919 * @param {Number=} blacklistDuration an optional number of seconds to blacklist the
920 * playlist
921 */
922 }, {
923 key: 'blacklistCurrentPlaylist',
924 value: function blacklistCurrentPlaylist(error, blacklistDuration) {
925 if (error === undefined) error = {};
926
927 var currentPlaylist = undefined;
928 var nextPlaylist = undefined;
929
930 // If the `error` was generated by the playlist loader, it will contain
931 // the playlist we were trying to load (but failed) and that should be
932 // blacklisted instead of the currently selected playlist which is likely
933 // out-of-date in this scenario
934 currentPlaylist = error.playlist || this.masterPlaylistLoader_.media();
935
936 blacklistDuration = blacklistDuration || error.blacklistDuration || this.blacklistDuration;
937
938 // If there is no current playlist, then an error occurred while we were
939 // trying to load the master OR while we were disposing of the tech
940 if (!currentPlaylist) {
941 this.error = error;
942
943 try {
944 return this.mediaSource.endOfStream('network');
945 } catch (e) {
946 return this.trigger('error');
947 }
948 }
949
950 var isFinalRendition = this.masterPlaylistLoader_.isFinalRendition_();
951
952 if (isFinalRendition) {
953 // Never blacklisting this playlist because it's final rendition
954 _videoJs2['default'].log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the final playlist.');
955
956 this.tech_.trigger('retryplaylist');
957 return this.masterPlaylistLoader_.load(isFinalRendition);
958 }
959 // Blacklist this playlist
960 currentPlaylist.excludeUntil = Date.now() + blacklistDuration * 1000;
961 this.tech_.trigger('blacklistplaylist');
962 this.tech_.trigger({ type: 'usage', name: 'hls-rendition-blacklisted' });
963
964 // Select a new playlist
965 nextPlaylist = this.selectPlaylist();
966 _videoJs2['default'].log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.');
967
968 return this.masterPlaylistLoader_.media(nextPlaylist);
969 }
970
971 /**
972 * Pause all segment loaders
973 */
974 }, {
975 key: 'pauseLoading',
976 value: function pauseLoading() {
977 this.mainSegmentLoader_.pause();
978 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
979 this.audioSegmentLoader_.pause();
980 }
981 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
982 this.subtitleSegmentLoader_.pause();
983 }
984 }
985
986 /**
987 * set the current time on all segment loaders
988 *
989 * @param {TimeRange} currentTime the current time to set
990 * @return {TimeRange} the current time
991 */
992 }, {
993 key: 'setCurrentTime',
994 value: function setCurrentTime(currentTime) {
995 var buffered = _ranges2['default'].findRange(this.tech_.buffered(), currentTime);
996
997 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) {
998 // return immediately if the metadata is not ready yet
999 return 0;
1000 }
1001
1002 // it's clearly an edge-case but don't thrown an error if asked to
1003 // seek within an empty playlist
1004 if (!this.masterPlaylistLoader_.media().segments) {
1005 return 0;
1006 }
1007
1008 // In flash playback, the segment loaders should be reset on every seek, even
1009 // in buffer seeks. If the seek location is already buffered, continue buffering as
1010 // usual
1011 if (buffered && buffered.length && this.mode_ !== 'flash') {
1012 return currentTime;
1013 }
1014
1015 // cancel outstanding requests so we begin buffering at the new
1016 // location
1017 this.mainSegmentLoader_.resetEverything();
1018 this.mainSegmentLoader_.abort();
1019 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
1020 this.audioSegmentLoader_.resetEverything();
1021 this.audioSegmentLoader_.abort();
1022 }
1023 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
1024 this.subtitleSegmentLoader_.resetEverything();
1025 this.subtitleSegmentLoader_.abort();
1026 }
1027
1028 // start segment loader loading in case they are paused
1029 this.load();
1030 }
1031
1032 /**
1033 * get the current duration
1034 *
1035 * @return {TimeRange} the duration
1036 */
1037 }, {
1038 key: 'duration',
1039 value: function duration() {
1040 if (!this.masterPlaylistLoader_) {
1041 return 0;
1042 }
1043
1044 if (this.mediaSource) {
1045 return this.mediaSource.duration;
1046 }
1047
1048 return Hls.Playlist.duration(this.masterPlaylistLoader_.media());
1049 }
1050
1051 /**
1052 * check the seekable range
1053 *
1054 * @return {TimeRange} the seekable range
1055 */
1056 }, {
1057 key: 'seekable',
1058 value: function seekable() {
1059 return this.seekable_;
1060 }
1061 }, {
1062 key: 'onSyncInfoUpdate_',
1063 value: function onSyncInfoUpdate_() {
1064 var mainSeekable = undefined;
1065 var audioSeekable = undefined;
1066
1067 if (!this.masterPlaylistLoader_) {
1068 return;
1069 }
1070
1071 var media = this.masterPlaylistLoader_.media();
1072
1073 if (!media) {
1074 return;
1075 }
1076
1077 var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
1078
1079 if (expired === null) {
1080 // not enough information to update seekable
1081 return;
1082 }
1083
1084 mainSeekable = Hls.Playlist.seekable(media, expired);
1085
1086 if (mainSeekable.length === 0) {
1087 return;
1088 }
1089
1090 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
1091 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
1092 expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration);
1093
1094 if (expired === null) {
1095 return;
1096 }
1097
1098 audioSeekable = Hls.Playlist.seekable(media, expired);
1099
1100 if (audioSeekable.length === 0) {
1101 return;
1102 }
1103 }
1104
1105 if (!audioSeekable) {
1106 // seekable has been calculated based on buffering video data so it
1107 // can be returned directly
1108 this.seekable_ = mainSeekable;
1109 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) {
1110 // seekables are pretty far off, rely on main
1111 this.seekable_ = mainSeekable;
1112 } else {
1113 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)]]);
1114 }
1115
1116 this.tech_.trigger('seekablechanged');
1117 }
1118
1119 /**
1120 * Update the player duration
1121 */
1122 }, {
1123 key: 'updateDuration',
1124 value: function updateDuration() {
1125 var _this5 = this;
1126
1127 var oldDuration = this.mediaSource.duration;
1128 var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media());
1129 var buffered = this.tech_.buffered();
1130 var setDuration = function setDuration() {
1131 _this5.mediaSource.duration = newDuration;
1132 _this5.tech_.trigger('durationchange');
1133
1134 _this5.mediaSource.removeEventListener('sourceopen', setDuration);
1135 };
1136
1137 if (buffered.length > 0) {
1138 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1));
1139 }
1140
1141 // if the duration has changed, invalidate the cached value
1142 if (oldDuration !== newDuration) {
1143 // update the duration
1144 if (this.mediaSource.readyState !== 'open') {
1145 this.mediaSource.addEventListener('sourceopen', setDuration);
1146 } else {
1147 setDuration();
1148 }
1149 }
1150 }
1151
1152 /**
1153 * dispose of the MasterPlaylistController and everything
1154 * that it controls
1155 */
1156 }, {
1157 key: 'dispose',
1158 value: function dispose() {
1159 var _this6 = this;
1160
1161 this.decrypter_.terminate();
1162 this.masterPlaylistLoader_.dispose();
1163 this.mainSegmentLoader_.dispose();
1164
1165 ['AUDIO', 'SUBTITLES'].forEach(function (type) {
1166 var groups = _this6.mediaTypes_[type].groups;
1167
1168 for (var id in groups) {
1169 groups[id].forEach(function (group) {
1170 if (group.playlistLoader) {
1171 group.playlistLoader.dispose();
1172 }
1173 });
1174 }
1175 });
1176
1177 this.audioSegmentLoader_.dispose();
1178 this.subtitleSegmentLoader_.dispose();
1179 }
1180
1181 /**
1182 * return the master playlist object if we have one
1183 *
1184 * @return {Object} the master playlist object that we parsed
1185 */
1186 }, {
1187 key: 'master',
1188 value: function master() {
1189 return this.masterPlaylistLoader_.master;
1190 }
1191
1192 /**
1193 * return the currently selected playlist
1194 *
1195 * @return {Object} the currently selected playlist object that we parsed
1196 */
1197 }, {
1198 key: 'media',
1199 value: function media() {
1200 // playlist loader will not return media if it has not been fully loaded
1201 return this.masterPlaylistLoader_.media() || this.initialMedia_;
1202 }
1203
1204 /**
1205 * setup our internal source buffers on our segment Loaders
1206 *
1207 * @private
1208 */
1209 }, {
1210 key: 'setupSourceBuffers_',
1211 value: function setupSourceBuffers_() {
1212 var media = this.masterPlaylistLoader_.media();
1213 var mimeTypes = undefined;
1214
1215 // wait until a media playlist is available and the Media Source is
1216 // attached
1217 if (!media || this.mediaSource.readyState !== 'open') {
1218 return;
1219 }
1220
1221 mimeTypes = mimeTypesForPlaylist_(this.masterPlaylistLoader_.master, media);
1222 if (mimeTypes.length < 1) {
1223 this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri;
1224 return this.mediaSource.endOfStream('decode');
1225 }
1226 this.mainSegmentLoader_.mimeType(mimeTypes[0]);
1227 if (mimeTypes[1]) {
1228 this.audioSegmentLoader_.mimeType(mimeTypes[1]);
1229 }
1230
1231 // exclude any incompatible variant streams from future playlist
1232 // selection
1233 this.excludeIncompatibleVariants_(media);
1234 }
1235
1236 /**
1237 * Blacklist playlists that are known to be codec or
1238 * stream-incompatible with the SourceBuffer configuration. For
1239 * instance, Media Source Extensions would cause the video element to
1240 * stall waiting for video data if you switched from a variant with
1241 * video and audio to an audio-only one.
1242 *
1243 * @param {Object} media a media playlist compatible with the current
1244 * set of SourceBuffers. Variants in the current master playlist that
1245 * do not appear to have compatible codec or stream configurations
1246 * will be excluded from the default playlist selection algorithm
1247 * indefinitely.
1248 * @private
1249 */
1250 }, {
1251 key: 'excludeIncompatibleVariants_',
1252 value: function excludeIncompatibleVariants_(media) {
1253 var master = this.masterPlaylistLoader_.master;
1254 var codecCount = 2;
1255 var videoCodec = null;
1256 var codecs = undefined;
1257
1258 if (media.attributes.CODECS) {
1259 codecs = (0, _utilCodecsJs.parseCodecs)(media.attributes.CODECS);
1260 videoCodec = codecs.videoCodec;
1261 codecCount = codecs.codecCount;
1262 }
1263 master.playlists.forEach(function (variant) {
1264 var variantCodecs = {
1265 codecCount: 2,
1266 videoCodec: null
1267 };
1268
1269 if (variant.attributes.CODECS) {
1270 var codecString = variant.attributes.CODECS;
1271
1272 variantCodecs = (0, _utilCodecsJs.parseCodecs)(codecString);
1273
1274 if (window.MediaSource && window.MediaSource.isTypeSupported && !window.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs_(codecString) + '"')) {
1275 variant.excludeUntil = Infinity;
1276 }
1277 }
1278
1279 // if the streams differ in the presence or absence of audio or
1280 // video, they are incompatible
1281 if (variantCodecs.codecCount !== codecCount) {
1282 variant.excludeUntil = Infinity;
1283 }
1284
1285 // if h.264 is specified on the current playlist, some flavor of
1286 // it must be specified on all compatible variants
1287 if (variantCodecs.videoCodec !== videoCodec) {
1288 variant.excludeUntil = Infinity;
1289 }
1290 });
1291 }
1292 }, {
1293 key: 'updateAdCues_',
1294 value: function updateAdCues_(media) {
1295 var offset = 0;
1296 var seekable = this.seekable();
1297
1298 if (seekable.length) {
1299 offset = seekable.start(0);
1300 }
1301
1302 _adCueTags2['default'].updateAdCues(media, this.cueTagsTrack_, offset);
1303 }
1304
1305 /**
1306 * Calculates the desired forward buffer length based on current time
1307 *
1308 * @return {Number} Desired forward buffer length in seconds
1309 */
1310 }, {
1311 key: 'goalBufferLength',
1312 value: function goalBufferLength() {
1313 var currentTime = this.tech_.currentTime();
1314 var initial = _config2['default'].GOAL_BUFFER_LENGTH;
1315 var rate = _config2['default'].GOAL_BUFFER_LENGTH_RATE;
1316 var max = Math.max(initial, _config2['default'].MAX_GOAL_BUFFER_LENGTH);
1317
1318 return Math.min(initial + currentTime * rate, max);
1319 }
1320
1321 /**
1322 * Calculates the desired buffer low water line based on current time
1323 *
1324 * @return {Number} Desired buffer low water line in seconds
1325 */
1326 }, {
1327 key: 'bufferLowWaterLine',
1328 value: function bufferLowWaterLine() {
1329 var currentTime = this.tech_.currentTime();
1330 var initial = _config2['default'].BUFFER_LOW_WATER_LINE;
1331 var rate = _config2['default'].BUFFER_LOW_WATER_LINE_RATE;
1332 var max = Math.max(initial, _config2['default'].MAX_BUFFER_LOW_WATER_LINE);
1333
1334 return Math.min(initial + currentTime * rate, max);
1335 }
1336 }]);
1337
1338 return MasterPlaylistController;
1339})(_videoJs2['default'].EventTarget);
1340
1341exports.MasterPlaylistController = MasterPlaylistController;
\No newline at end of file