UNPKG

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