UNPKG

68.9 kBJavaScriptView Raw
1/**
2 * @file master-playlist-controller.js
3 */
4import window from 'global/window';
5import PlaylistLoader from './playlist-loader';
6import DashPlaylistLoader from './dash-playlist-loader';
7import { isEnabled, isLowestEnabledRendition } from './playlist.js';
8import SegmentLoader from './segment-loader';
9import SourceUpdater from './source-updater';
10import VTTSegmentLoader from './vtt-segment-loader';
11import * as Ranges from './ranges';
12import videojs from 'video.js';
13import { updateAdCues } from './ad-cue-tags';
14import SyncController from './sync-controller';
15import TimelineChangeController from './timeline-change-controller';
16import Decrypter from 'worker!./decrypter-worker.js';
17import Config from './config';
18import {
19 parseCodecs,
20 browserSupportsCodec,
21 muxerSupportsCodec,
22 DEFAULT_AUDIO_CODEC,
23 DEFAULT_VIDEO_CODEC
24} from '@videojs/vhs-utils/es/codecs.js';
25import { codecsForPlaylist, unwrapCodecList, codecCount } from './util/codecs.js';
26import { createMediaTypes, setupMediaGroups } from './media-groups';
27import logger from './util/logger';
28
29const ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2;
30
31let Vhs;
32
33// SegmentLoader stats that need to have each loader's
34// values summed to calculate the final value
35const loaderStats = [
36 'mediaRequests',
37 'mediaRequestsAborted',
38 'mediaRequestsTimedout',
39 'mediaRequestsErrored',
40 'mediaTransferDuration',
41 'mediaBytesTransferred',
42 'mediaAppends'
43];
44const sumLoaderStat = function(stat) {
45 return this.audioSegmentLoader_[stat] +
46 this.mainSegmentLoader_[stat];
47};
48const shouldSwitchToMedia = function({
49 currentPlaylist,
50 buffered,
51 currentTime,
52 nextPlaylist,
53 bufferLowWaterLine,
54 bufferHighWaterLine,
55 duration,
56 experimentalBufferBasedABR,
57 log
58}) {
59 // we have no other playlist to switch to
60 if (!nextPlaylist) {
61 videojs.log.warn('We received no playlist to switch to. Please check your stream.');
62 return false;
63 }
64
65 const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`;
66
67 if (!currentPlaylist) {
68 log(`${sharedLogLine} as current playlist is not set`);
69 return true;
70 }
71
72 // no need to switch if playlist is the same
73 if (nextPlaylist.id === currentPlaylist.id) {
74 return false;
75 }
76
77 // determine if current time is in a buffered range.
78 const isBuffered = Boolean(Ranges.findRange(buffered, currentTime).length);
79
80 // If the playlist is live, then we want to not take low water line into account.
81 // This is because in LIVE, the player plays 3 segments from the end of the
82 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
83 // in those segments, a viewer will never experience a rendition upswitch.
84 if (!currentPlaylist.endList) {
85 // For LLHLS live streams, don't switch renditions before playback has started, as it almost
86 // doubles the time to first playback.
87 if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') {
88 log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`);
89 return false;
90 }
91 log(`${sharedLogLine} as current playlist is live`);
92 return true;
93 }
94
95 const forwardBuffer = Ranges.timeAheadOf(buffered, currentTime);
96 const maxBufferLowWaterLine = experimentalBufferBasedABR ?
97 Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE;
98
99 // For the same reason as LIVE, we ignore the low water line when the VOD
100 // duration is below the max potential low water line
101 if (duration < maxBufferLowWaterLine) {
102 log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`);
103 return true;
104 }
105
106 const nextBandwidth = nextPlaylist.attributes.BANDWIDTH;
107 const currBandwidth = currentPlaylist.attributes.BANDWIDTH;
108
109 // when switching down, if our buffer is lower than the high water line,
110 // we can switch down
111 if (nextBandwidth < currBandwidth && (!experimentalBufferBasedABR || forwardBuffer < bufferHighWaterLine)) {
112 let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`;
113
114 if (experimentalBufferBasedABR) {
115 logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`;
116 }
117 log(logLine);
118 return true;
119 }
120
121 // and if our buffer is higher than the low water line,
122 // we can switch up
123 if ((!experimentalBufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) {
124 let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`;
125
126 if (experimentalBufferBasedABR) {
127 logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`;
128 }
129 log(logLine);
130 return true;
131 }
132
133 log(`not ${sharedLogLine} as no switching criteria met`);
134
135 return false;
136};
137
138/**
139 * the master playlist controller controller all interactons
140 * between playlists and segmentloaders. At this time this mainly
141 * involves a master playlist and a series of audio playlists
142 * if they are available
143 *
144 * @class MasterPlaylistController
145 * @extends videojs.EventTarget
146 */
147export class MasterPlaylistController extends videojs.EventTarget {
148 constructor(options) {
149 super();
150
151 const {
152 src,
153 handleManifestRedirects,
154 withCredentials,
155 tech,
156 bandwidth,
157 externVhs,
158 useCueTags,
159 blacklistDuration,
160 enableLowInitialPlaylist,
161 sourceType,
162 cacheEncryptionKeys,
163 experimentalBufferBasedABR,
164 experimentalLeastPixelDiffSelector,
165 captionServices
166 } = options;
167
168 if (!src) {
169 throw new Error('A non-empty playlist URL or JSON manifest string is required');
170 }
171
172 let { maxPlaylistRetries } = options;
173
174 if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') {
175 maxPlaylistRetries = Infinity;
176 }
177
178 Vhs = externVhs;
179
180 this.experimentalBufferBasedABR = Boolean(experimentalBufferBasedABR);
181 this.experimentalLeastPixelDiffSelector = Boolean(experimentalLeastPixelDiffSelector);
182 this.withCredentials = withCredentials;
183 this.tech_ = tech;
184 this.vhs_ = tech.vhs;
185 this.sourceType_ = sourceType;
186 this.useCueTags_ = useCueTags;
187 this.blacklistDuration = blacklistDuration;
188 this.maxPlaylistRetries = maxPlaylistRetries;
189 this.enableLowInitialPlaylist = enableLowInitialPlaylist;
190
191 if (this.useCueTags_) {
192 this.cueTagsTrack_ = this.tech_.addTextTrack(
193 'metadata',
194 'ad-cues'
195 );
196 this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
197 }
198
199 this.requestOptions_ = {
200 withCredentials,
201 handleManifestRedirects,
202 maxPlaylistRetries,
203 timeout: null
204 };
205
206 this.on('error', this.pauseLoading);
207
208 this.mediaTypes_ = createMediaTypes();
209
210 this.mediaSource = new window.MediaSource();
211
212 this.handleDurationChange_ = this.handleDurationChange_.bind(this);
213 this.handleSourceOpen_ = this.handleSourceOpen_.bind(this);
214 this.handleSourceEnded_ = this.handleSourceEnded_.bind(this);
215
216 this.mediaSource.addEventListener('durationchange', this.handleDurationChange_);
217
218 // load the media source into the player
219 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_);
220 this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_);
221 // we don't have to handle sourceclose since dispose will handle termination of
222 // everything, and the MediaSource should not be detached without a proper disposal
223
224 this.seekable_ = videojs.createTimeRanges();
225 this.hasPlayed_ = false;
226
227 this.syncController_ = new SyncController(options);
228 this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
229 kind: 'metadata',
230 label: 'segment-metadata'
231 }, false).track;
232
233 this.decrypter_ = new Decrypter();
234 this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
235 this.inbandTextTracks_ = {};
236 this.timelineChangeController_ = new TimelineChangeController();
237
238 const segmentLoaderSettings = {
239 vhs: this.vhs_,
240 parse708captions: options.parse708captions,
241 useDtsForTimestampOffset: options.useDtsForTimestampOffset,
242 captionServices,
243 mediaSource: this.mediaSource,
244 currentTime: this.tech_.currentTime.bind(this.tech_),
245 seekable: () => this.seekable(),
246 seeking: () => this.tech_.seeking(),
247 duration: () => this.duration(),
248 hasPlayed: () => this.hasPlayed_,
249 goalBufferLength: () => this.goalBufferLength(),
250 bandwidth,
251 syncController: this.syncController_,
252 decrypter: this.decrypter_,
253 sourceType: this.sourceType_,
254 inbandTextTracks: this.inbandTextTracks_,
255 cacheEncryptionKeys,
256 sourceUpdater: this.sourceUpdater_,
257 timelineChangeController: this.timelineChangeController_,
258 experimentalExactManifestTimings: options.experimentalExactManifestTimings
259 };
260
261 // The source type check not only determines whether a special DASH playlist loader
262 // should be used, but also covers the case where the provided src is a vhs-json
263 // manifest object (instead of a URL). In the case of vhs-json, the default
264 // PlaylistLoader should be used.
265 this.masterPlaylistLoader_ = this.sourceType_ === 'dash' ?
266 new DashPlaylistLoader(src, this.vhs_, this.requestOptions_) :
267 new PlaylistLoader(src, this.vhs_, this.requestOptions_);
268 this.setupMasterPlaylistLoaderListeners_();
269
270 // setup segment loaders
271 // combined audio/video or just video when alternate audio track is selected
272 this.mainSegmentLoader_ =
273 new SegmentLoader(videojs.mergeOptions(segmentLoaderSettings, {
274 segmentMetadataTrack: this.segmentMetadataTrack_,
275 loaderType: 'main'
276 }), options);
277
278 // alternate audio track
279 this.audioSegmentLoader_ =
280 new SegmentLoader(videojs.mergeOptions(segmentLoaderSettings, {
281 loaderType: 'audio'
282 }), options);
283
284 this.subtitleSegmentLoader_ =
285 new VTTSegmentLoader(videojs.mergeOptions(segmentLoaderSettings, {
286 loaderType: 'vtt',
287 featuresNativeTextTracks: this.tech_.featuresNativeTextTracks,
288 loadVttJs: () => new Promise((resolve, reject) => {
289 function onLoad() {
290 tech.off('vttjserror', onError);
291 resolve();
292 }
293
294 function onError() {
295 tech.off('vttjsloaded', onLoad);
296 reject();
297 }
298
299 tech.one('vttjsloaded', onLoad);
300 tech.one('vttjserror', onError);
301
302 // safe to call multiple times, script will be loaded only once:
303 tech.addWebVttScript_();
304 })
305 }), options);
306
307 this.setupSegmentLoaderListeners_();
308
309 if (this.experimentalBufferBasedABR) {
310 this.masterPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_());
311 this.tech_.on('pause', () => this.stopABRTimer_());
312 this.tech_.on('play', () => this.startABRTimer_());
313 }
314
315 // Create SegmentLoader stat-getters
316 // mediaRequests_
317 // mediaRequestsAborted_
318 // mediaRequestsTimedout_
319 // mediaRequestsErrored_
320 // mediaTransferDuration_
321 // mediaBytesTransferred_
322 // mediaAppends_
323 loaderStats.forEach((stat) => {
324 this[stat + '_'] = sumLoaderStat.bind(this, stat);
325 });
326
327 this.logger_ = logger('MPC');
328
329 this.triggeredFmp4Usage = false;
330 if (this.tech_.preload() === 'none') {
331 this.loadOnPlay_ = () => {
332 this.loadOnPlay_ = null;
333 this.masterPlaylistLoader_.load();
334 };
335
336 this.tech_.one('play', this.loadOnPlay_);
337 } else {
338 this.masterPlaylistLoader_.load();
339 }
340
341 this.timeToLoadedData__ = -1;
342 this.mainAppendsToLoadedData__ = -1;
343 this.audioAppendsToLoadedData__ = -1;
344
345 const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart';
346
347 // start the first frame timer on loadstart or play (for preload none)
348 this.tech_.one(event, () => {
349 const timeToLoadedDataStart = Date.now();
350
351 this.tech_.one('loadeddata', () => {
352 this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart;
353 this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends;
354 this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends;
355 });
356 });
357 }
358
359 mainAppendsToLoadedData_() {
360 return this.mainAppendsToLoadedData__;
361 }
362
363 audioAppendsToLoadedData_() {
364 return this.audioAppendsToLoadedData__;
365 }
366
367 appendsToLoadedData_() {
368 const main = this.mainAppendsToLoadedData_();
369 const audio = this.audioAppendsToLoadedData_();
370
371 if (main === -1 || audio === -1) {
372 return -1;
373 }
374
375 return main + audio;
376 }
377
378 timeToLoadedData_() {
379 return this.timeToLoadedData__;
380 }
381
382 /**
383 * Run selectPlaylist and switch to the new playlist if we should
384 *
385 * @param {string} [reason=abr] a reason for why the ABR check is made
386 * @private
387 */
388 checkABR_(reason = 'abr') {
389 const nextPlaylist = this.selectPlaylist();
390
391 if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) {
392 this.switchMedia_(nextPlaylist, reason);
393 }
394 }
395
396 switchMedia_(playlist, cause, delay) {
397 const oldMedia = this.media();
398 const oldId = oldMedia && (oldMedia.id || oldMedia.uri);
399 const newId = playlist.id || playlist.uri;
400
401 if (oldId && oldId !== newId) {
402 this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`);
403 this.tech_.trigger({type: 'usage', name: `vhs-rendition-change-${cause}`});
404 }
405 this.masterPlaylistLoader_.media(playlist, delay);
406 }
407
408 /**
409 * Start a timer that periodically calls checkABR_
410 *
411 * @private
412 */
413 startABRTimer_() {
414 this.stopABRTimer_();
415 this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250);
416 }
417
418 /**
419 * Stop the timer that periodically calls checkABR_
420 *
421 * @private
422 */
423 stopABRTimer_() {
424 // if we're scrubbing, we don't need to pause.
425 // This getter will be added to Video.js in version 7.11.
426 if (this.tech_.scrubbing && this.tech_.scrubbing()) {
427 return;
428 }
429 window.clearInterval(this.abrTimer_);
430 this.abrTimer_ = null;
431 }
432
433 /**
434 * Get a list of playlists for the currently selected audio playlist
435 *
436 * @return {Array} the array of audio playlists
437 */
438 getAudioTrackPlaylists_() {
439 const master = this.master();
440 const defaultPlaylists = master && master.playlists || [];
441
442 // if we don't have any audio groups then we can only
443 // assume that the audio tracks are contained in masters
444 // playlist array, use that or an empty array.
445 if (!master || !master.mediaGroups || !master.mediaGroups.AUDIO) {
446 return defaultPlaylists;
447 }
448
449 const AUDIO = master.mediaGroups.AUDIO;
450 const groupKeys = Object.keys(AUDIO);
451 let track;
452
453 // get the current active track
454 if (Object.keys(this.mediaTypes_.AUDIO.groups).length) {
455 track = this.mediaTypes_.AUDIO.activeTrack();
456 // or get the default track from master if mediaTypes_ isn't setup yet
457 } else {
458 // default group is `main` or just the first group.
459 const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]];
460
461 for (const label in defaultGroup) {
462 if (defaultGroup[label].default) {
463 track = {label};
464 break;
465 }
466 }
467 }
468
469 // no active track no playlists.
470 if (!track) {
471 return defaultPlaylists;
472 }
473
474 const playlists = [];
475
476 // get all of the playlists that are possible for the
477 // active track.
478 for (const group in AUDIO) {
479 if (AUDIO[group][track.label]) {
480 const properties = AUDIO[group][track.label];
481
482 if (properties.playlists && properties.playlists.length) {
483 playlists.push.apply(playlists, properties.playlists);
484 } else if (properties.uri) {
485 playlists.push(properties);
486 } else if (master.playlists.length) {
487 // if an audio group does not have a uri
488 // see if we have main playlists that use it as a group.
489 // if we do then add those to the playlists list.
490 for (let i = 0; i < master.playlists.length; i++) {
491 const playlist = master.playlists[i];
492
493 if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) {
494 playlists.push(playlist);
495 }
496 }
497 }
498 }
499 }
500
501 if (!playlists.length) {
502 return defaultPlaylists;
503 }
504
505 return playlists;
506 }
507
508 /**
509 * Register event handlers on the master playlist loader. A helper
510 * function for construction time.
511 *
512 * @private
513 */
514 setupMasterPlaylistLoaderListeners_() {
515 this.masterPlaylistLoader_.on('loadedmetadata', () => {
516 const media = this.masterPlaylistLoader_.media();
517 const requestTimeout = (media.targetDuration * 1.5) * 1000;
518
519 // If we don't have any more available playlists, we don't want to
520 // timeout the request.
521 if (isLowestEnabledRendition(this.masterPlaylistLoader_.master, this.masterPlaylistLoader_.media())) {
522 this.requestOptions_.timeout = 0;
523 } else {
524 this.requestOptions_.timeout = requestTimeout;
525 }
526
527 // if this isn't a live video and preload permits, start
528 // downloading segments
529 if (media.endList && this.tech_.preload() !== 'none') {
530 this.mainSegmentLoader_.playlist(media, this.requestOptions_);
531 this.mainSegmentLoader_.load();
532 }
533
534 setupMediaGroups({
535 sourceType: this.sourceType_,
536 segmentLoaders: {
537 AUDIO: this.audioSegmentLoader_,
538 SUBTITLES: this.subtitleSegmentLoader_,
539 main: this.mainSegmentLoader_
540 },
541 tech: this.tech_,
542 requestOptions: this.requestOptions_,
543 masterPlaylistLoader: this.masterPlaylistLoader_,
544 vhs: this.vhs_,
545 master: this.master(),
546 mediaTypes: this.mediaTypes_,
547 blacklistCurrentPlaylist: this.blacklistCurrentPlaylist.bind(this)
548 });
549
550 this.triggerPresenceUsage_(this.master(), media);
551 this.setupFirstPlay();
552
553 if (!this.mediaTypes_.AUDIO.activePlaylistLoader ||
554 this.mediaTypes_.AUDIO.activePlaylistLoader.media()) {
555 this.trigger('selectedinitialmedia');
556 } else {
557 // We must wait for the active audio playlist loader to
558 // finish setting up before triggering this event so the
559 // representations API and EME setup is correct
560 this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => {
561 this.trigger('selectedinitialmedia');
562 });
563 }
564
565 });
566
567 this.masterPlaylistLoader_.on('loadedplaylist', () => {
568 if (this.loadOnPlay_) {
569 this.tech_.off('play', this.loadOnPlay_);
570 }
571 let updatedPlaylist = this.masterPlaylistLoader_.media();
572
573 if (!updatedPlaylist) {
574 // exclude any variants that are not supported by the browser before selecting
575 // an initial media as the playlist selectors do not consider browser support
576 this.excludeUnsupportedVariants_();
577
578 let selectedMedia;
579
580 if (this.enableLowInitialPlaylist) {
581 selectedMedia = this.selectInitialPlaylist();
582 }
583
584 if (!selectedMedia) {
585 selectedMedia = this.selectPlaylist();
586 }
587
588 if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) {
589 return;
590 }
591
592 this.initialMedia_ = selectedMedia;
593
594 this.switchMedia_(this.initialMedia_, 'initial');
595
596 // Under the standard case where a source URL is provided, loadedplaylist will
597 // fire again since the playlist will be requested. In the case of vhs-json
598 // (where the manifest object is provided as the source), when the media
599 // playlist's `segments` list is already available, a media playlist won't be
600 // requested, and loadedplaylist won't fire again, so the playlist handler must be
601 // called on its own here.
602 const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments;
603
604 if (!haveJsonSource) {
605 return;
606 }
607 updatedPlaylist = this.initialMedia_;
608 }
609
610 this.handleUpdatedMediaPlaylist(updatedPlaylist);
611 });
612
613 this.masterPlaylistLoader_.on('error', () => {
614 this.blacklistCurrentPlaylist(this.masterPlaylistLoader_.error);
615 });
616
617 this.masterPlaylistLoader_.on('mediachanging', () => {
618 this.mainSegmentLoader_.abort();
619 this.mainSegmentLoader_.pause();
620 });
621
622 this.masterPlaylistLoader_.on('mediachange', () => {
623 const media = this.masterPlaylistLoader_.media();
624 const requestTimeout = (media.targetDuration * 1.5) * 1000;
625
626 // If we don't have any more available playlists, we don't want to
627 // timeout the request.
628 if (isLowestEnabledRendition(this.masterPlaylistLoader_.master, this.masterPlaylistLoader_.media())) {
629 this.requestOptions_.timeout = 0;
630 } else {
631 this.requestOptions_.timeout = requestTimeout;
632 }
633
634 this.masterPlaylistLoader_.load();
635
636 // TODO: Create a new event on the PlaylistLoader that signals
637 // that the segments have changed in some way and use that to
638 // update the SegmentLoader instead of doing it twice here and
639 // on `loadedplaylist`
640 this.mainSegmentLoader_.playlist(media, this.requestOptions_);
641
642 this.mainSegmentLoader_.load();
643
644 this.tech_.trigger({
645 type: 'mediachange',
646 bubbles: true
647 });
648 });
649
650 this.masterPlaylistLoader_.on('playlistunchanged', () => {
651 const updatedPlaylist = this.masterPlaylistLoader_.media();
652
653 // ignore unchanged playlists that have already been
654 // excluded for not-changing. We likely just have a really slowly updating
655 // playlist.
656 if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') {
657 return;
658 }
659
660 const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist);
661
662 if (playlistOutdated) {
663 // Playlist has stopped updating and we're stuck at its end. Try to
664 // blacklist it and switch to another playlist in the hope that that
665 // one is updating (and give the player a chance to re-adjust to the
666 // safe live point).
667 this.blacklistCurrentPlaylist({
668 message: 'Playlist no longer updating.',
669 reason: 'playlist-unchanged'
670 });
671 // useful for monitoring QoS
672 this.tech_.trigger('playliststuck');
673 }
674 });
675
676 this.masterPlaylistLoader_.on('renditiondisabled', () => {
677 this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled'});
678 this.tech_.trigger({type: 'usage', name: 'hls-rendition-disabled'});
679 });
680 this.masterPlaylistLoader_.on('renditionenabled', () => {
681 this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled'});
682 this.tech_.trigger({type: 'usage', name: 'hls-rendition-enabled'});
683 });
684 }
685
686 /**
687 * Given an updated media playlist (whether it was loaded for the first time, or
688 * refreshed for live playlists), update any relevant properties and state to reflect
689 * changes in the media that should be accounted for (e.g., cues and duration).
690 *
691 * @param {Object} updatedPlaylist the updated media playlist object
692 *
693 * @private
694 */
695 handleUpdatedMediaPlaylist(updatedPlaylist) {
696 if (this.useCueTags_) {
697 this.updateAdCues_(updatedPlaylist);
698 }
699
700 // TODO: Create a new event on the PlaylistLoader that signals
701 // that the segments have changed in some way and use that to
702 // update the SegmentLoader instead of doing it twice here and
703 // on `mediachange`
704 this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
705 this.updateDuration(!updatedPlaylist.endList);
706
707 // If the player isn't paused, ensure that the segment loader is running,
708 // as it is possible that it was temporarily stopped while waiting for
709 // a playlist (e.g., in case the playlist errored and we re-requested it).
710 if (!this.tech_.paused()) {
711 this.mainSegmentLoader_.load();
712 if (this.audioSegmentLoader_) {
713 this.audioSegmentLoader_.load();
714 }
715 }
716 }
717
718 /**
719 * A helper function for triggerring presence usage events once per source
720 *
721 * @private
722 */
723 triggerPresenceUsage_(master, media) {
724 const mediaGroups = master.mediaGroups || {};
725 let defaultDemuxed = true;
726 const audioGroupKeys = Object.keys(mediaGroups.AUDIO);
727
728 for (const mediaGroup in mediaGroups.AUDIO) {
729 for (const label in mediaGroups.AUDIO[mediaGroup]) {
730 const properties = mediaGroups.AUDIO[mediaGroup][label];
731
732 if (!properties.uri) {
733 defaultDemuxed = false;
734 }
735 }
736 }
737
738 if (defaultDemuxed) {
739 this.tech_.trigger({type: 'usage', name: 'vhs-demuxed'});
740 this.tech_.trigger({type: 'usage', name: 'hls-demuxed'});
741 }
742
743 if (Object.keys(mediaGroups.SUBTITLES).length) {
744 this.tech_.trigger({type: 'usage', name: 'vhs-webvtt'});
745 this.tech_.trigger({type: 'usage', name: 'hls-webvtt'});
746 }
747
748 if (Vhs.Playlist.isAes(media)) {
749 this.tech_.trigger({type: 'usage', name: 'vhs-aes'});
750 this.tech_.trigger({type: 'usage', name: 'hls-aes'});
751 }
752
753 if (audioGroupKeys.length &&
754 Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
755 this.tech_.trigger({type: 'usage', name: 'vhs-alternate-audio'});
756 this.tech_.trigger({type: 'usage', name: 'hls-alternate-audio'});
757 }
758
759 if (this.useCueTags_) {
760 this.tech_.trigger({type: 'usage', name: 'vhs-playlist-cue-tags'});
761 this.tech_.trigger({type: 'usage', name: 'hls-playlist-cue-tags'});
762 }
763 }
764
765 shouldSwitchToMedia_(nextPlaylist) {
766 const currentPlaylist = this.masterPlaylistLoader_.media() ||
767 this.masterPlaylistLoader_.pendingMedia_;
768 const currentTime = this.tech_.currentTime();
769 const bufferLowWaterLine = this.bufferLowWaterLine();
770 const bufferHighWaterLine = this.bufferHighWaterLine();
771 const buffered = this.tech_.buffered();
772
773 return shouldSwitchToMedia({
774 buffered,
775 currentTime,
776 currentPlaylist,
777 nextPlaylist,
778 bufferLowWaterLine,
779 bufferHighWaterLine,
780 duration: this.duration(),
781 experimentalBufferBasedABR: this.experimentalBufferBasedABR,
782 log: this.logger_
783 });
784 }
785 /**
786 * Register event handlers on the segment loaders. A helper function
787 * for construction time.
788 *
789 * @private
790 */
791 setupSegmentLoaderListeners_() {
792 this.mainSegmentLoader_.on('bandwidthupdate', () => {
793 // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's
794 // useful to check to see if a rendition switch should be made.
795 this.checkABR_('bandwidthupdate');
796 this.tech_.trigger('bandwidthupdate');
797 });
798
799 this.mainSegmentLoader_.on('timeout', () => {
800 if (this.experimentalBufferBasedABR) {
801 // If a rendition change is needed, then it would've be done on `bandwidthupdate`.
802 // Here the only consideration is that for buffer based ABR there's no guarantee
803 // of an immediate switch (since the bandwidth is averaged with a timeout
804 // bandwidth value of 1), so force a load on the segment loader to keep it going.
805 this.mainSegmentLoader_.load();
806 }
807 });
808
809 // `progress` events are not reliable enough of a bandwidth measure to trigger buffer
810 // based ABR.
811 if (!this.experimentalBufferBasedABR) {
812 this.mainSegmentLoader_.on('progress', () => {
813 this.trigger('progress');
814 });
815 }
816
817 this.mainSegmentLoader_.on('error', () => {
818 this.blacklistCurrentPlaylist(this.mainSegmentLoader_.error());
819 });
820
821 this.mainSegmentLoader_.on('appenderror', () => {
822 this.error = this.mainSegmentLoader_.error_;
823 this.trigger('error');
824 });
825
826 this.mainSegmentLoader_.on('syncinfoupdate', () => {
827 this.onSyncInfoUpdate_();
828 });
829
830 this.mainSegmentLoader_.on('timestampoffset', () => {
831 this.tech_.trigger({type: 'usage', name: 'vhs-timestamp-offset'});
832 this.tech_.trigger({type: 'usage', name: 'hls-timestamp-offset'});
833 });
834 this.audioSegmentLoader_.on('syncinfoupdate', () => {
835 this.onSyncInfoUpdate_();
836 });
837
838 this.audioSegmentLoader_.on('appenderror', () => {
839 this.error = this.audioSegmentLoader_.error_;
840 this.trigger('error');
841 });
842
843 this.mainSegmentLoader_.on('ended', () => {
844 this.logger_('main segment loader ended');
845 this.onEndOfStream();
846 });
847
848 this.mainSegmentLoader_.on('earlyabort', (event) => {
849 // never try to early abort with the new ABR algorithm
850 if (this.experimentalBufferBasedABR) {
851 return;
852 }
853
854 this.delegateLoaders_('all', ['abort']);
855
856 this.blacklistCurrentPlaylist({
857 message: 'Aborted early because there isn\'t enough bandwidth to complete the ' +
858 'request without rebuffering.'
859 }, ABORT_EARLY_BLACKLIST_SECONDS);
860 });
861
862 const updateCodecs = () => {
863 if (!this.sourceUpdater_.hasCreatedSourceBuffers()) {
864 return this.tryToCreateSourceBuffers_();
865 }
866
867 const codecs = this.getCodecsOrExclude_();
868
869 // no codecs means that the playlist was excluded
870 if (!codecs) {
871 return;
872 }
873
874 this.sourceUpdater_.addOrChangeSourceBuffers(codecs);
875 };
876
877 this.mainSegmentLoader_.on('trackinfo', updateCodecs);
878 this.audioSegmentLoader_.on('trackinfo', updateCodecs);
879
880 this.mainSegmentLoader_.on('fmp4', () => {
881 if (!this.triggeredFmp4Usage) {
882 this.tech_.trigger({type: 'usage', name: 'vhs-fmp4'});
883 this.tech_.trigger({type: 'usage', name: 'hls-fmp4'});
884 this.triggeredFmp4Usage = true;
885 }
886 });
887
888 this.audioSegmentLoader_.on('fmp4', () => {
889 if (!this.triggeredFmp4Usage) {
890 this.tech_.trigger({type: 'usage', name: 'vhs-fmp4'});
891 this.tech_.trigger({type: 'usage', name: 'hls-fmp4'});
892 this.triggeredFmp4Usage = true;
893 }
894 });
895
896 this.audioSegmentLoader_.on('ended', () => {
897 this.logger_('audioSegmentLoader ended');
898 this.onEndOfStream();
899 });
900 }
901
902 mediaSecondsLoaded_() {
903 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded +
904 this.mainSegmentLoader_.mediaSecondsLoaded);
905 }
906
907 /**
908 * Call load on our SegmentLoaders
909 */
910 load() {
911 this.mainSegmentLoader_.load();
912 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
913 this.audioSegmentLoader_.load();
914 }
915 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
916 this.subtitleSegmentLoader_.load();
917 }
918 }
919
920 /**
921 * Re-tune playback quality level for the current player
922 * conditions without performing destructive actions, like
923 * removing already buffered content
924 *
925 * @private
926 * @deprecated
927 */
928 smoothQualityChange_(media = this.selectPlaylist()) {
929 this.fastQualityChange_(media);
930 }
931
932 /**
933 * Re-tune playback quality level for the current player
934 * conditions. This method will perform destructive actions like removing
935 * already buffered content in order to readjust the currently active
936 * playlist quickly. This is good for manual quality changes
937 *
938 * @private
939 */
940 fastQualityChange_(media = this.selectPlaylist()) {
941 if (media === this.masterPlaylistLoader_.media()) {
942 this.logger_('skipping fastQualityChange because new media is same as old');
943 return;
944 }
945
946 this.switchMedia_(media, 'fast-quality');
947
948 // Delete all buffered data to allow an immediate quality switch, then seek to give
949 // the browser a kick to remove any cached frames from the previous rendtion (.04 seconds
950 // ahead is roughly the minimum that will accomplish this across a variety of content
951 // in IE and Edge, but seeking in place is sufficient on all other browsers)
952 // Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/
953 // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904
954 this.mainSegmentLoader_.resetEverything(() => {
955 // Since this is not a typical seek, we avoid the seekTo method which can cause segments
956 // from the previously enabled rendition to load before the new playlist has finished loading
957 if (videojs.browser.IE_VERSION || videojs.browser.IS_EDGE) {
958 this.tech_.setCurrentTime(this.tech_.currentTime() + 0.04);
959 } else {
960 this.tech_.setCurrentTime(this.tech_.currentTime());
961 }
962 });
963
964 // don't need to reset audio as it is reset when media changes
965 }
966
967 /**
968 * Begin playback.
969 */
970 play() {
971 if (this.setupFirstPlay()) {
972 return;
973 }
974
975 if (this.tech_.ended()) {
976 this.tech_.setCurrentTime(0);
977 }
978
979 if (this.hasPlayed_) {
980 this.load();
981 }
982
983 const seekable = this.tech_.seekable();
984
985 // if the viewer has paused and we fell out of the live window,
986 // seek forward to the live point
987 if (this.tech_.duration() === Infinity) {
988 if (this.tech_.currentTime() < seekable.start(0)) {
989 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
990 }
991 }
992 }
993
994 /**
995 * Seek to the latest media position if this is a live video and the
996 * player and video are loaded and initialized.
997 */
998 setupFirstPlay() {
999 const media = this.masterPlaylistLoader_.media();
1000
1001 // Check that everything is ready to begin buffering for the first call to play
1002 // If 1) there is no active media
1003 // 2) the player is paused
1004 // 3) the first play has already been setup
1005 // then exit early
1006 if (!media || this.tech_.paused() || this.hasPlayed_) {
1007 return false;
1008 }
1009
1010 // when the video is a live stream
1011 if (!media.endList) {
1012 const seekable = this.seekable();
1013
1014 if (!seekable.length) {
1015 // without a seekable range, the player cannot seek to begin buffering at the live
1016 // point
1017 return false;
1018 }
1019
1020 if (videojs.browser.IE_VERSION &&
1021 this.tech_.readyState() === 0) {
1022 // IE11 throws an InvalidStateError if you try to set currentTime while the
1023 // readyState is 0, so it must be delayed until the tech fires loadedmetadata.
1024 this.tech_.one('loadedmetadata', () => {
1025 this.trigger('firstplay');
1026 this.tech_.setCurrentTime(seekable.end(0));
1027 this.hasPlayed_ = true;
1028 });
1029
1030 return false;
1031 }
1032
1033 // trigger firstplay to inform the source handler to ignore the next seek event
1034 this.trigger('firstplay');
1035 // seek to the live point
1036 this.tech_.setCurrentTime(seekable.end(0));
1037 }
1038
1039 this.hasPlayed_ = true;
1040 // we can begin loading now that everything is ready
1041 this.load();
1042 return true;
1043 }
1044
1045 /**
1046 * handle the sourceopen event on the MediaSource
1047 *
1048 * @private
1049 */
1050 handleSourceOpen_() {
1051 // Only attempt to create the source buffer if none already exist.
1052 // handleSourceOpen is also called when we are "re-opening" a source buffer
1053 // after `endOfStream` has been called (in response to a seek for instance)
1054 this.tryToCreateSourceBuffers_();
1055
1056 // if autoplay is enabled, begin playback. This is duplicative of
1057 // code in video.js but is required because play() must be invoked
1058 // *after* the media source has opened.
1059 if (this.tech_.autoplay()) {
1060 const playPromise = this.tech_.play();
1061
1062 // Catch/silence error when a pause interrupts a play request
1063 // on browsers which return a promise
1064 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') {
1065 playPromise.then(null, (e) => {});
1066 }
1067 }
1068
1069 this.trigger('sourceopen');
1070 }
1071
1072 /**
1073 * handle the sourceended event on the MediaSource
1074 *
1075 * @private
1076 */
1077 handleSourceEnded_() {
1078 if (!this.inbandTextTracks_.metadataTrack_) {
1079 return;
1080 }
1081
1082 const cues = this.inbandTextTracks_.metadataTrack_.cues;
1083
1084 if (!cues || !cues.length) {
1085 return;
1086 }
1087
1088 const duration = this.duration();
1089
1090 cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ?
1091 Number.MAX_VALUE : duration;
1092 }
1093
1094 /**
1095 * handle the durationchange event on the MediaSource
1096 *
1097 * @private
1098 */
1099 handleDurationChange_() {
1100 this.tech_.trigger('durationchange');
1101 }
1102
1103 /**
1104 * Calls endOfStream on the media source when all active stream types have called
1105 * endOfStream
1106 *
1107 * @param {string} streamType
1108 * Stream type of the segment loader that called endOfStream
1109 * @private
1110 */
1111 onEndOfStream() {
1112 let isEndOfStream = this.mainSegmentLoader_.ended_;
1113
1114 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
1115 const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_();
1116
1117 // if the audio playlist loader exists, then alternate audio is active
1118 if (!mainMediaInfo || mainMediaInfo.hasVideo) {
1119 // if we do not know if the main segment loader contains video yet or if we
1120 // definitively know the main segment loader contains video, then we need to wait
1121 // for both main and audio segment loaders to call endOfStream
1122 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
1123 } else {
1124 // otherwise just rely on the audio loader
1125 isEndOfStream = this.audioSegmentLoader_.ended_;
1126 }
1127 }
1128
1129 if (!isEndOfStream) {
1130 return;
1131 }
1132
1133 this.stopABRTimer_();
1134 this.sourceUpdater_.endOfStream();
1135 }
1136
1137 /**
1138 * Check if a playlist has stopped being updated
1139 *
1140 * @param {Object} playlist the media playlist object
1141 * @return {boolean} whether the playlist has stopped being updated or not
1142 */
1143 stuckAtPlaylistEnd_(playlist) {
1144 const seekable = this.seekable();
1145
1146 if (!seekable.length) {
1147 // playlist doesn't have enough information to determine whether we are stuck
1148 return false;
1149 }
1150
1151 const expired =
1152 this.syncController_.getExpiredTime(playlist, this.duration());
1153
1154 if (expired === null) {
1155 return false;
1156 }
1157
1158 // does not use the safe live end to calculate playlist end, since we
1159 // don't want to say we are stuck while there is still content
1160 const absolutePlaylistEnd = Vhs.Playlist.playlistEnd(playlist, expired);
1161 const currentTime = this.tech_.currentTime();
1162 const buffered = this.tech_.buffered();
1163
1164 if (!buffered.length) {
1165 // return true if the playhead reached the absolute end of the playlist
1166 return absolutePlaylistEnd - currentTime <= Ranges.SAFE_TIME_DELTA;
1167 }
1168 const bufferedEnd = buffered.end(buffered.length - 1);
1169
1170 // return true if there is too little buffer left and buffer has reached absolute
1171 // end of playlist
1172 return bufferedEnd - currentTime <= Ranges.SAFE_TIME_DELTA &&
1173 absolutePlaylistEnd - bufferedEnd <= Ranges.SAFE_TIME_DELTA;
1174 }
1175
1176 /**
1177 * Blacklists a playlist when an error occurs for a set amount of time
1178 * making it unavailable for selection by the rendition selection algorithm
1179 * and then forces a new playlist (rendition) selection.
1180 *
1181 * @param {Object=} error an optional error that may include the playlist
1182 * to blacklist
1183 * @param {number=} blacklistDuration an optional number of seconds to blacklist the
1184 * playlist
1185 */
1186 blacklistCurrentPlaylist(error = {}, blacklistDuration) {
1187 // If the `error` was generated by the playlist loader, it will contain
1188 // the playlist we were trying to load (but failed) and that should be
1189 // blacklisted instead of the currently selected playlist which is likely
1190 // out-of-date in this scenario
1191 const currentPlaylist = error.playlist || this.masterPlaylistLoader_.media();
1192
1193 blacklistDuration = blacklistDuration ||
1194 error.blacklistDuration ||
1195 this.blacklistDuration;
1196
1197 // If there is no current playlist, then an error occurred while we were
1198 // trying to load the master OR while we were disposing of the tech
1199 if (!currentPlaylist) {
1200 this.error = error;
1201
1202 if (this.mediaSource.readyState !== 'open') {
1203 this.trigger('error');
1204 } else {
1205 this.sourceUpdater_.endOfStream('network');
1206 }
1207
1208 return;
1209 }
1210
1211 currentPlaylist.playlistErrors_++;
1212
1213 const playlists = this.masterPlaylistLoader_.master.playlists;
1214 const enabledPlaylists = playlists.filter(isEnabled);
1215 const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === currentPlaylist;
1216
1217 // Don't blacklist the only playlist unless it was blacklisted
1218 // forever
1219 if (playlists.length === 1 && blacklistDuration !== Infinity) {
1220 videojs.log.warn(`Problem encountered with playlist ${currentPlaylist.id}. ` +
1221 'Trying again since it is the only playlist.');
1222
1223 this.tech_.trigger('retryplaylist');
1224 // if this is a final rendition, we should delay
1225 return this.masterPlaylistLoader_.load(isFinalRendition);
1226 }
1227
1228 if (isFinalRendition) {
1229 // Since we're on the final non-blacklisted playlist, and we're about to blacklist
1230 // it, instead of erring the player or retrying this playlist, clear out the current
1231 // blacklist. This allows other playlists to be attempted in case any have been
1232 // fixed.
1233 let reincluded = false;
1234
1235 playlists.forEach((playlist) => {
1236 // skip current playlist which is about to be blacklisted
1237 if (playlist === currentPlaylist) {
1238 return;
1239 }
1240 const excludeUntil = playlist.excludeUntil;
1241
1242 // a playlist cannot be reincluded if it wasn't excluded to begin with.
1243 if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) {
1244 reincluded = true;
1245 delete playlist.excludeUntil;
1246 }
1247 });
1248
1249 if (reincluded) {
1250 videojs.log.warn('Removing other playlists from the exclusion list because the last ' +
1251 'rendition is about to be excluded.');
1252 // Technically we are retrying a playlist, in that we are simply retrying a previous
1253 // playlist. This is needed for users relying on the retryplaylist event to catch a
1254 // case where the player might be stuck and looping through "dead" playlists.
1255 this.tech_.trigger('retryplaylist');
1256 }
1257 }
1258
1259 // Blacklist this playlist
1260 let excludeUntil;
1261
1262 if (currentPlaylist.playlistErrors_ > this.maxPlaylistRetries) {
1263 excludeUntil = Infinity;
1264 } else {
1265 excludeUntil = Date.now() + (blacklistDuration * 1000);
1266 }
1267
1268 currentPlaylist.excludeUntil = excludeUntil;
1269
1270 if (error.reason) {
1271 currentPlaylist.lastExcludeReason_ = error.reason;
1272 }
1273 this.tech_.trigger('blacklistplaylist');
1274 this.tech_.trigger({type: 'usage', name: 'vhs-rendition-blacklisted'});
1275 this.tech_.trigger({type: 'usage', name: 'hls-rendition-blacklisted'});
1276
1277 // TODO: should we select a new playlist if this blacklist wasn't for the currentPlaylist?
1278 // Would be something like media().id !=== currentPlaylist.id and we would need something
1279 // like `pendingMedia` in playlist loaders to check against that too. This will prevent us
1280 // from loading a new playlist on any blacklist.
1281 // Select a new playlist
1282 const nextPlaylist = this.selectPlaylist();
1283
1284 if (!nextPlaylist) {
1285 this.error = 'Playback cannot continue. No available working or supported playlists.';
1286 this.trigger('error');
1287 return;
1288 }
1289
1290 const logFn = error.internal ? this.logger_ : videojs.log.warn;
1291 const errorMessage = error.message ? (' ' + error.message) : '';
1292
1293 logFn(`${(error.internal ? 'Internal problem' : 'Problem')} encountered with playlist ${currentPlaylist.id}.` +
1294 `${errorMessage} Switching to playlist ${nextPlaylist.id}.`);
1295
1296 // if audio group changed reset audio loaders
1297 if (nextPlaylist.attributes.AUDIO !== currentPlaylist.attributes.AUDIO) {
1298 this.delegateLoaders_('audio', ['abort', 'pause']);
1299 }
1300
1301 // if subtitle group changed reset subtitle loaders
1302 if (nextPlaylist.attributes.SUBTITLES !== currentPlaylist.attributes.SUBTITLES) {
1303 this.delegateLoaders_('subtitle', ['abort', 'pause']);
1304 }
1305
1306 this.delegateLoaders_('main', ['abort', 'pause']);
1307
1308 const delayDuration = (nextPlaylist.targetDuration / 2) * 1000 || 5 * 1000;
1309 const shouldDelay = typeof nextPlaylist.lastRequest === 'number' &&
1310 (Date.now() - nextPlaylist.lastRequest) <= delayDuration;
1311
1312 // delay if it's a final rendition or if the last refresh is sooner than half targetDuration
1313 return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay);
1314 }
1315
1316 /**
1317 * Pause all segment/playlist loaders
1318 */
1319 pauseLoading() {
1320 this.delegateLoaders_('all', ['abort', 'pause']);
1321 this.stopABRTimer_();
1322 }
1323
1324 /**
1325 * Call a set of functions in order on playlist loaders, segment loaders,
1326 * or both types of loaders.
1327 *
1328 * @param {string} filter
1329 * Filter loaders that should call fnNames using a string. Can be:
1330 * * all - run on all loaders
1331 * * audio - run on all audio loaders
1332 * * subtitle - run on all subtitle loaders
1333 * * main - run on the main/master loaders
1334 *
1335 * @param {Array|string} fnNames
1336 * A string or array of function names to call.
1337 */
1338 delegateLoaders_(filter, fnNames) {
1339 const loaders = [];
1340
1341 const dontFilterPlaylist = filter === 'all';
1342
1343 if (dontFilterPlaylist || filter === 'main') {
1344 loaders.push(this.masterPlaylistLoader_);
1345 }
1346
1347 const mediaTypes = [];
1348
1349 if (dontFilterPlaylist || filter === 'audio') {
1350 mediaTypes.push('AUDIO');
1351 }
1352
1353 if (dontFilterPlaylist || filter === 'subtitle') {
1354 mediaTypes.push('CLOSED-CAPTIONS');
1355 mediaTypes.push('SUBTITLES');
1356 }
1357
1358 mediaTypes.forEach((mediaType) => {
1359 const loader = this.mediaTypes_[mediaType] &&
1360 this.mediaTypes_[mediaType].activePlaylistLoader;
1361
1362 if (loader) {
1363 loaders.push(loader);
1364 }
1365 });
1366
1367 ['main', 'audio', 'subtitle'].forEach((name) => {
1368 const loader = this[`${name}SegmentLoader_`];
1369
1370 if (loader && (filter === name || filter === 'all')) {
1371 loaders.push(loader);
1372 }
1373 });
1374
1375 loaders.forEach((loader) => fnNames.forEach((fnName) => {
1376 if (typeof loader[fnName] === 'function') {
1377 loader[fnName]();
1378 }
1379 }));
1380 }
1381
1382 /**
1383 * set the current time on all segment loaders
1384 *
1385 * @param {TimeRange} currentTime the current time to set
1386 * @return {TimeRange} the current time
1387 */
1388 setCurrentTime(currentTime) {
1389 const buffered = Ranges.findRange(this.tech_.buffered(), currentTime);
1390
1391 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) {
1392 // return immediately if the metadata is not ready yet
1393 return 0;
1394 }
1395
1396 // it's clearly an edge-case but don't thrown an error if asked to
1397 // seek within an empty playlist
1398 if (!this.masterPlaylistLoader_.media().segments) {
1399 return 0;
1400 }
1401
1402 // if the seek location is already buffered, continue buffering as usual
1403 if (buffered && buffered.length) {
1404 return currentTime;
1405 }
1406
1407 // cancel outstanding requests so we begin buffering at the new
1408 // location
1409 this.mainSegmentLoader_.resetEverything();
1410 this.mainSegmentLoader_.abort();
1411 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
1412 this.audioSegmentLoader_.resetEverything();
1413 this.audioSegmentLoader_.abort();
1414 }
1415 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
1416 this.subtitleSegmentLoader_.resetEverything();
1417 this.subtitleSegmentLoader_.abort();
1418 }
1419
1420 // start segment loader loading in case they are paused
1421 this.load();
1422 }
1423
1424 /**
1425 * get the current duration
1426 *
1427 * @return {TimeRange} the duration
1428 */
1429 duration() {
1430 if (!this.masterPlaylistLoader_) {
1431 return 0;
1432 }
1433
1434 const media = this.masterPlaylistLoader_.media();
1435
1436 if (!media) {
1437 // no playlists loaded yet, so can't determine a duration
1438 return 0;
1439 }
1440
1441 // Don't rely on the media source for duration in the case of a live playlist since
1442 // setting the native MediaSource's duration to infinity ends up with consequences to
1443 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
1444 //
1445 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
1446 // however, few browsers have support for setLiveSeekableRange()
1447 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
1448 //
1449 // Until a time when the duration of the media source can be set to infinity, and a
1450 // seekable range specified across browsers, just return Infinity.
1451 if (!media.endList) {
1452 return Infinity;
1453 }
1454
1455 // Since this is a VOD video, it is safe to rely on the media source's duration (if
1456 // available). If it's not available, fall back to a playlist-calculated estimate.
1457
1458 if (this.mediaSource) {
1459 return this.mediaSource.duration;
1460 }
1461
1462 return Vhs.Playlist.duration(media);
1463 }
1464
1465 /**
1466 * check the seekable range
1467 *
1468 * @return {TimeRange} the seekable range
1469 */
1470 seekable() {
1471 return this.seekable_;
1472 }
1473
1474 onSyncInfoUpdate_() {
1475 let audioSeekable;
1476
1477 // TODO check for creation of both source buffers before updating seekable
1478 //
1479 // A fix was made to this function where a check for
1480 // this.sourceUpdater_.hasCreatedSourceBuffers
1481 // was added to ensure that both source buffers were created before seekable was
1482 // updated. However, it originally had a bug where it was checking for a true and
1483 // returning early instead of checking for false. Setting it to check for false to
1484 // return early though created other issues. A call to play() would check for seekable
1485 // end without verifying that a seekable range was present. In addition, even checking
1486 // for that didn't solve some issues, as handleFirstPlay is sometimes worked around
1487 // due to a media update calling load on the segment loaders, skipping a seek to live,
1488 // thereby starting live streams at the beginning of the stream rather than at the end.
1489 //
1490 // This conditional should be fixed to wait for the creation of two source buffers at
1491 // the same time as the other sections of code are fixed to properly seek to live and
1492 // not throw an error due to checking for a seekable end when no seekable range exists.
1493 //
1494 // For now, fall back to the older behavior, with the understanding that the seekable
1495 // range may not be completely correct, leading to a suboptimal initial live point.
1496 if (!this.masterPlaylistLoader_) {
1497 return;
1498 }
1499
1500 let media = this.masterPlaylistLoader_.media();
1501
1502 if (!media) {
1503 return;
1504 }
1505
1506 let expired = this.syncController_.getExpiredTime(media, this.duration());
1507
1508 if (expired === null) {
1509 // not enough information to update seekable
1510 return;
1511 }
1512
1513 const master = this.masterPlaylistLoader_.master;
1514 const mainSeekable = Vhs.Playlist.seekable(
1515 media,
1516 expired,
1517 Vhs.Playlist.liveEdgeDelay(master, media)
1518 );
1519
1520 if (mainSeekable.length === 0) {
1521 return;
1522 }
1523
1524 if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
1525 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
1526 expired = this.syncController_.getExpiredTime(media, this.duration());
1527
1528 if (expired === null) {
1529 return;
1530 }
1531
1532 audioSeekable = Vhs.Playlist.seekable(
1533 media,
1534 expired,
1535 Vhs.Playlist.liveEdgeDelay(master, media)
1536 );
1537
1538 if (audioSeekable.length === 0) {
1539 return;
1540 }
1541 }
1542
1543 let oldEnd;
1544 let oldStart;
1545
1546 if (this.seekable_ && this.seekable_.length) {
1547 oldEnd = this.seekable_.end(0);
1548 oldStart = this.seekable_.start(0);
1549 }
1550
1551 if (!audioSeekable) {
1552 // seekable has been calculated based on buffering video data so it
1553 // can be returned directly
1554 this.seekable_ = mainSeekable;
1555 } else if (audioSeekable.start(0) > mainSeekable.end(0) ||
1556 mainSeekable.start(0) > audioSeekable.end(0)) {
1557 // seekables are pretty far off, rely on main
1558 this.seekable_ = mainSeekable;
1559 } else {
1560 this.seekable_ = videojs.createTimeRanges([[
1561 (audioSeekable.start(0) > mainSeekable.start(0)) ? audioSeekable.start(0) :
1562 mainSeekable.start(0),
1563 (audioSeekable.end(0) < mainSeekable.end(0)) ? audioSeekable.end(0) :
1564 mainSeekable.end(0)
1565 ]]);
1566 }
1567
1568 // seekable is the same as last time
1569 if (this.seekable_ && this.seekable_.length) {
1570 if (this.seekable_.end(0) === oldEnd && this.seekable_.start(0) === oldStart) {
1571 return;
1572 }
1573 }
1574
1575 this.logger_(`seekable updated [${Ranges.printableRange(this.seekable_)}]`);
1576
1577 this.tech_.trigger('seekablechanged');
1578 }
1579
1580 /**
1581 * Update the player duration
1582 */
1583 updateDuration(isLive) {
1584 if (this.updateDuration_) {
1585 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
1586 this.updateDuration_ = null;
1587 }
1588 if (this.mediaSource.readyState !== 'open') {
1589 this.updateDuration_ = this.updateDuration.bind(this, isLive);
1590 this.mediaSource.addEventListener('sourceopen', this.updateDuration_);
1591 return;
1592 }
1593
1594 if (isLive) {
1595 const seekable = this.seekable();
1596
1597 if (!seekable.length) {
1598 return;
1599 }
1600
1601 // Even in the case of a live playlist, the native MediaSource's duration should not
1602 // be set to Infinity (even though this would be expected for a live playlist), since
1603 // setting the native MediaSource's duration to infinity ends up with consequences to
1604 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
1605 //
1606 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
1607 // however, few browsers have support for setLiveSeekableRange()
1608 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
1609 //
1610 // Until a time when the duration of the media source can be set to infinity, and a
1611 // seekable range specified across browsers, the duration should be greater than or
1612 // equal to the last possible seekable value.
1613
1614 // MediaSource duration starts as NaN
1615 // It is possible (and probable) that this case will never be reached for many
1616 // sources, since the MediaSource reports duration as the highest value without
1617 // accounting for timestamp offset. For example, if the timestamp offset is -100 and
1618 // we buffered times 0 to 100 with real times of 100 to 200, even though current
1619 // time will be between 0 and 100, the native media source may report the duration
1620 // as 200. However, since we report duration separate from the media source (as
1621 // Infinity), and as long as the native media source duration value is greater than
1622 // our reported seekable range, seeks will work as expected. The large number as
1623 // duration for live is actually a strategy used by some players to work around the
1624 // issue of live seekable ranges cited above.
1625 if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) {
1626 this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1));
1627 }
1628 return;
1629 }
1630
1631 const buffered = this.tech_.buffered();
1632 let duration = Vhs.Playlist.duration(this.masterPlaylistLoader_.media());
1633
1634 if (buffered.length > 0) {
1635 duration = Math.max(duration, buffered.end(buffered.length - 1));
1636 }
1637
1638 if (this.mediaSource.duration !== duration) {
1639 this.sourceUpdater_.setDuration(duration);
1640 }
1641 }
1642
1643 /**
1644 * dispose of the MasterPlaylistController and everything
1645 * that it controls
1646 */
1647 dispose() {
1648 this.trigger('dispose');
1649 this.decrypter_.terminate();
1650 this.masterPlaylistLoader_.dispose();
1651 this.mainSegmentLoader_.dispose();
1652
1653 if (this.loadOnPlay_) {
1654 this.tech_.off('play', this.loadOnPlay_);
1655 }
1656
1657 ['AUDIO', 'SUBTITLES'].forEach((type) => {
1658 const groups = this.mediaTypes_[type].groups;
1659
1660 for (const id in groups) {
1661 groups[id].forEach((group) => {
1662 if (group.playlistLoader) {
1663 group.playlistLoader.dispose();
1664 }
1665 });
1666 }
1667 });
1668
1669 this.audioSegmentLoader_.dispose();
1670 this.subtitleSegmentLoader_.dispose();
1671 this.sourceUpdater_.dispose();
1672 this.timelineChangeController_.dispose();
1673
1674 this.stopABRTimer_();
1675
1676 if (this.updateDuration_) {
1677 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
1678 }
1679
1680 this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_);
1681
1682 // load the media source into the player
1683 this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_);
1684 this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_);
1685 this.off();
1686 }
1687
1688 /**
1689 * return the master playlist object if we have one
1690 *
1691 * @return {Object} the master playlist object that we parsed
1692 */
1693 master() {
1694 return this.masterPlaylistLoader_.master;
1695 }
1696
1697 /**
1698 * return the currently selected playlist
1699 *
1700 * @return {Object} the currently selected playlist object that we parsed
1701 */
1702 media() {
1703 // playlist loader will not return media if it has not been fully loaded
1704 return this.masterPlaylistLoader_.media() || this.initialMedia_;
1705 }
1706
1707 areMediaTypesKnown_() {
1708 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
1709 const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_();
1710 // if we are not using an audio loader, then we have audio media info
1711 // otherwise check on the segment loader.
1712 const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_();
1713
1714 // one or both loaders has not loaded sufficently to get codecs
1715 if (!hasMainMediaInfo || !hasAudioMediaInfo) {
1716 return false;
1717 }
1718
1719 return true;
1720 }
1721
1722 getCodecsOrExclude_() {
1723 const media = {
1724 main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {},
1725 audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {}
1726 };
1727
1728 // set "main" media equal to video
1729 media.video = media.main;
1730 const playlistCodecs = codecsForPlaylist(this.master(), this.media());
1731 const codecs = {};
1732 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
1733
1734 if (media.main.hasVideo) {
1735 codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC;
1736 }
1737
1738 if (media.main.isMuxed) {
1739 codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`;
1740 }
1741
1742 if ((media.main.hasAudio && !media.main.isMuxed) || media.audio.hasAudio || usingAudioLoader) {
1743 codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC;
1744 // set audio isFmp4 so we use the correct "supports" function below
1745 media.audio.isFmp4 = (media.main.hasAudio && !media.main.isMuxed) ? media.main.isFmp4 : media.audio.isFmp4;
1746 }
1747
1748 // no codecs, no playback.
1749 if (!codecs.audio && !codecs.video) {
1750 this.blacklistCurrentPlaylist({
1751 playlist: this.media(),
1752 message: 'Could not determine codecs for playlist.',
1753 blacklistDuration: Infinity
1754 });
1755 return;
1756 }
1757
1758 // fmp4 relies on browser support, while ts relies on muxer support
1759 const supportFunction = (isFmp4, codec) => (isFmp4 ? browserSupportsCodec(codec) : muxerSupportsCodec(codec));
1760 const unsupportedCodecs = {};
1761 let unsupportedAudio;
1762
1763 ['video', 'audio'].forEach(function(type) {
1764 if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) {
1765 const supporter = media[type].isFmp4 ? 'browser' : 'muxer';
1766
1767 unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || [];
1768 unsupportedCodecs[supporter].push(codecs[type]);
1769
1770 if (type === 'audio') {
1771 unsupportedAudio = supporter;
1772 }
1773 }
1774 });
1775
1776 if (usingAudioLoader && unsupportedAudio && this.media().attributes.AUDIO) {
1777 const audioGroup = this.media().attributes.AUDIO;
1778
1779 this.master().playlists.forEach(variant => {
1780 const variantAudioGroup = variant.attributes && variant.attributes.AUDIO;
1781
1782 if (variantAudioGroup === audioGroup && variant !== this.media()) {
1783 variant.excludeUntil = Infinity;
1784 }
1785 });
1786 this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`);
1787 }
1788
1789 // if we have any unsupported codecs blacklist this playlist.
1790 if (Object.keys(unsupportedCodecs).length) {
1791 const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => {
1792
1793 if (acc) {
1794 acc += ', ';
1795 }
1796
1797 acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`;
1798
1799 return acc;
1800 }, '') + '.';
1801
1802 this.blacklistCurrentPlaylist({
1803 playlist: this.media(),
1804 internal: true,
1805 message,
1806 blacklistDuration: Infinity
1807 });
1808 return;
1809 }
1810 // check if codec switching is happening
1811 if (
1812 this.sourceUpdater_.hasCreatedSourceBuffers() &&
1813 !this.sourceUpdater_.canChangeType()
1814 ) {
1815 const switchMessages = [];
1816
1817 ['video', 'audio'].forEach((type) => {
1818 const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type;
1819 const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type;
1820
1821 if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) {
1822 switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`);
1823 }
1824 });
1825
1826 if (switchMessages.length) {
1827 this.blacklistCurrentPlaylist({
1828 playlist: this.media(),
1829 message: `Codec switching not supported: ${switchMessages.join(', ')}.`,
1830 blacklistDuration: Infinity,
1831 internal: true
1832 });
1833 return;
1834 }
1835 }
1836
1837 // TODO: when using the muxer shouldn't we just return
1838 // the codecs that the muxer outputs?
1839 return codecs;
1840 }
1841
1842 /**
1843 * Create source buffers and exlude any incompatible renditions.
1844 *
1845 * @private
1846 */
1847 tryToCreateSourceBuffers_() {
1848 // media source is not ready yet or sourceBuffers are already
1849 // created.
1850 if (
1851 this.mediaSource.readyState !== 'open' ||
1852 this.sourceUpdater_.hasCreatedSourceBuffers()
1853 ) {
1854 return;
1855 }
1856
1857 if (!this.areMediaTypesKnown_()) {
1858 return;
1859 }
1860
1861 const codecs = this.getCodecsOrExclude_();
1862
1863 // no codecs means that the playlist was excluded
1864 if (!codecs) {
1865 return;
1866 }
1867
1868 this.sourceUpdater_.createSourceBuffers(codecs);
1869
1870 const codecString = [codecs.video, codecs.audio].filter(Boolean).join(',');
1871
1872 this.excludeIncompatibleVariants_(codecString);
1873 }
1874
1875 /**
1876 * Excludes playlists with codecs that are unsupported by the muxer and browser.
1877 */
1878 excludeUnsupportedVariants_() {
1879 const playlists = this.master().playlists;
1880 const ids = [];
1881
1882 // TODO: why don't we have a property to loop through all
1883 // playlist? Why did we ever mix indexes and keys?
1884 Object.keys(playlists).forEach(key => {
1885 const variant = playlists[key];
1886
1887 // check if we already processed this playlist.
1888 if (ids.indexOf(variant.id) !== -1) {
1889 return;
1890 }
1891
1892 ids.push(variant.id);
1893
1894 const codecs = codecsForPlaylist(this.master, variant);
1895 const unsupported = [];
1896
1897 if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio)) {
1898 unsupported.push(`audio codec ${codecs.audio}`);
1899 }
1900
1901 if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video)) {
1902 unsupported.push(`video codec ${codecs.video}`);
1903 }
1904
1905 if (codecs.text && codecs.text === 'stpp.ttml.im1t') {
1906 unsupported.push(`text codec ${codecs.text}`);
1907 }
1908
1909 if (unsupported.length) {
1910 variant.excludeUntil = Infinity;
1911 this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`);
1912 }
1913 });
1914 }
1915
1916 /**
1917 * Blacklist playlists that are known to be codec or
1918 * stream-incompatible with the SourceBuffer configuration. For
1919 * instance, Media Source Extensions would cause the video element to
1920 * stall waiting for video data if you switched from a variant with
1921 * video and audio to an audio-only one.
1922 *
1923 * @param {Object} media a media playlist compatible with the current
1924 * set of SourceBuffers. Variants in the current master playlist that
1925 * do not appear to have compatible codec or stream configurations
1926 * will be excluded from the default playlist selection algorithm
1927 * indefinitely.
1928 * @private
1929 */
1930 excludeIncompatibleVariants_(codecString) {
1931 const ids = [];
1932 const playlists = this.master().playlists;
1933 const codecs = unwrapCodecList(parseCodecs(codecString));
1934 const codecCount_ = codecCount(codecs);
1935 const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null;
1936 const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null;
1937
1938 Object.keys(playlists).forEach((key) => {
1939 const variant = playlists[key];
1940
1941 // check if we already processed this playlist.
1942 // or it if it is already excluded forever.
1943 if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) {
1944 return;
1945 }
1946
1947 ids.push(variant.id);
1948 const blacklistReasons = [];
1949
1950 // get codecs from the playlist for this variant
1951 const variantCodecs = codecsForPlaylist(this.masterPlaylistLoader_.master, variant);
1952 const variantCodecCount = codecCount(variantCodecs);
1953
1954 // if no codecs are listed, we cannot determine that this
1955 // variant is incompatible. Wait for mux.js to probe
1956 if (!variantCodecs.audio && !variantCodecs.video) {
1957 return;
1958 }
1959
1960 // TODO: we can support this by removing the
1961 // old media source and creating a new one, but it will take some work.
1962 // The number of streams cannot change
1963 if (variantCodecCount !== codecCount_) {
1964 blacklistReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`);
1965 }
1966
1967 // only exclude playlists by codec change, if codecs cannot switch
1968 // during playback.
1969 if (!this.sourceUpdater_.canChangeType()) {
1970 const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null;
1971 const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null;
1972
1973 // the video codec cannot change
1974 if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) {
1975 blacklistReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`);
1976 }
1977
1978 // the audio codec cannot change
1979 if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) {
1980 blacklistReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`);
1981 }
1982 }
1983
1984 if (blacklistReasons.length) {
1985 variant.excludeUntil = Infinity;
1986 this.logger_(`blacklisting ${variant.id}: ${blacklistReasons.join(' && ')}`);
1987 }
1988 });
1989 }
1990
1991 updateAdCues_(media) {
1992 let offset = 0;
1993 const seekable = this.seekable();
1994
1995 if (seekable.length) {
1996 offset = seekable.start(0);
1997 }
1998
1999 updateAdCues(media, this.cueTagsTrack_, offset);
2000 }
2001
2002 /**
2003 * Calculates the desired forward buffer length based on current time
2004 *
2005 * @return {number} Desired forward buffer length in seconds
2006 */
2007 goalBufferLength() {
2008 const currentTime = this.tech_.currentTime();
2009 const initial = Config.GOAL_BUFFER_LENGTH;
2010 const rate = Config.GOAL_BUFFER_LENGTH_RATE;
2011 const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH);
2012
2013 return Math.min(initial + currentTime * rate, max);
2014 }
2015
2016 /**
2017 * Calculates the desired buffer low water line based on current time
2018 *
2019 * @return {number} Desired buffer low water line in seconds
2020 */
2021 bufferLowWaterLine() {
2022 const currentTime = this.tech_.currentTime();
2023 const initial = Config.BUFFER_LOW_WATER_LINE;
2024 const rate = Config.BUFFER_LOW_WATER_LINE_RATE;
2025 const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE);
2026 const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE);
2027
2028 return Math.min(initial + currentTime * rate, this.experimentalBufferBasedABR ? newMax : max);
2029 }
2030
2031 bufferHighWaterLine() {
2032 return Config.BUFFER_HIGH_WATER_LINE;
2033 }
2034
2035}