UNPKG

48.6 kBJavaScriptView Raw
1/**
2 * @file segment-loader.js
3 */
4'use strict';
5
6Object.defineProperty(exports, '__esModule', {
7 value: true
8});
9
10var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
11
12var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
13
14function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
15
16function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
17
18function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
19
20var _playlist = require('./playlist');
21
22var _playlist2 = _interopRequireDefault(_playlist);
23
24var _videoJs = require('video.js');
25
26var _videoJs2 = _interopRequireDefault(_videoJs);
27
28var _sourceUpdater = require('./source-updater');
29
30var _sourceUpdater2 = _interopRequireDefault(_sourceUpdater);
31
32var _config = require('./config');
33
34var _config2 = _interopRequireDefault(_config);
35
36var _globalWindow = require('global/window');
37
38var _globalWindow2 = _interopRequireDefault(_globalWindow);
39
40var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs = require('videojs-contrib-media-sources/es5/remove-cues-from-track.js');
41
42var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2 = _interopRequireDefault(_videojsContribMediaSourcesEs5RemoveCuesFromTrackJs);
43
44var _binUtils = require('./bin-utils');
45
46var _mediaSegmentRequest = require('./media-segment-request');
47
48var _ranges = require('./ranges');
49
50var _playlistSelectors = require('./playlist-selectors');
51
52// in ms
53var CHECK_BUFFER_DELAY = 500;
54
55/**
56 * Determines if we should call endOfStream on the media source based
57 * on the state of the buffer or if appened segment was the final
58 * segment in the playlist.
59 *
60 * @param {Object} playlist a media playlist object
61 * @param {Object} mediaSource the MediaSource object
62 * @param {Number} segmentIndex the index of segment we last appended
63 * @returns {Boolean} do we need to call endOfStream on the MediaSource
64 */
65var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) {
66 if (!playlist || !mediaSource) {
67 return false;
68 }
69
70 var segments = playlist.segments;
71
72 // determine a few boolean values to help make the branch below easier
73 // to read
74 var appendedLastSegment = segmentIndex === segments.length;
75
76 // if we've buffered to the end of the video, we need to call endOfStream
77 // so that MediaSources can trigger the `ended` event when it runs out of
78 // buffered data instead of waiting for me
79 return playlist.endList && mediaSource.readyState === 'open' && appendedLastSegment;
80};
81
82var finite = function finite(num) {
83 return typeof num === 'number' && isFinite(num);
84};
85
86var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) {
87 // Although these checks should most likely cover non 'main' types, for now it narrows
88 // the scope of our checks.
89 if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) {
90 return null;
91 }
92
93 if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) {
94 return 'Neither audio nor video found in segment.';
95 }
96
97 if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) {
98 return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.';
99 }
100
101 if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) {
102 return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.';
103 }
104
105 return null;
106};
107
108exports.illegalMediaSwitch = illegalMediaSwitch;
109/**
110 * Calculates a time value that is safe to remove from the back buffer without interupting
111 * playback.
112 *
113 * @param {TimeRange} seekable
114 * The current seekable range
115 * @param {Number} currentTime
116 * The current time of the player
117 * @param {Number} targetDuration
118 * The target duration of the current playlist
119 * @return {Number}
120 * Time that is safe to remove from the back buffer without interupting playback
121 */
122var safeBackBufferTrimTime = function safeBackBufferTrimTime(seekable, currentTime, targetDuration) {
123 var removeToTime = undefined;
124
125 if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
126 // If we have a seekable range use that as the limit for what can be removed safely
127 removeToTime = seekable.start(0);
128 } else {
129 // otherwise remove anything older than 30 seconds before the current play head
130 removeToTime = currentTime - 30;
131 }
132
133 // Don't allow removing from the buffer within target duration of current time
134 // to avoid the possibility of removing the GOP currently being played which could
135 // cause playback stalls.
136 return Math.min(removeToTime, currentTime - targetDuration);
137};
138
139exports.safeBackBufferTrimTime = safeBackBufferTrimTime;
140/**
141 * An object that manages segment loading and appending.
142 *
143 * @class SegmentLoader
144 * @param {Object} options required and optional options
145 * @extends videojs.EventTarget
146 */
147
148var SegmentLoader = (function (_videojs$EventTarget) {
149 _inherits(SegmentLoader, _videojs$EventTarget);
150
151 function SegmentLoader(settings) {
152 var _this = this;
153
154 var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
155
156 _classCallCheck(this, SegmentLoader);
157
158 _get(Object.getPrototypeOf(SegmentLoader.prototype), 'constructor', this).call(this);
159 // check pre-conditions
160 if (!settings) {
161 throw new TypeError('Initialization settings are required');
162 }
163 if (typeof settings.currentTime !== 'function') {
164 throw new TypeError('No currentTime getter specified');
165 }
166 if (!settings.mediaSource) {
167 throw new TypeError('No MediaSource specified');
168 }
169 // public properties
170 this.state = 'INIT';
171 this.bandwidth = settings.bandwidth;
172 this.throughput = { rate: 0, count: 0 };
173 this.roundTrip = NaN;
174 this.resetStats_();
175 this.mediaIndex = null;
176
177 // private settings
178 this.hasPlayed_ = settings.hasPlayed;
179 this.currentTime_ = settings.currentTime;
180 this.seekable_ = settings.seekable;
181 this.seeking_ = settings.seeking;
182 this.duration_ = settings.duration;
183 this.mediaSource_ = settings.mediaSource;
184 this.hls_ = settings.hls;
185 this.loaderType_ = settings.loaderType;
186 this.startingMedia_ = void 0;
187 this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
188 this.goalBufferLength_ = settings.goalBufferLength;
189
190 // private instance variables
191 this.checkBufferTimeout_ = null;
192 this.error_ = void 0;
193 this.currentTimeline_ = -1;
194 this.pendingSegment_ = null;
195 this.mimeType_ = null;
196 this.sourceUpdater_ = null;
197 this.xhrOptions_ = null;
198
199 // Fragmented mp4 playback
200 this.activeInitSegmentId_ = null;
201 this.initSegments_ = {};
202
203 this.decrypter_ = settings.decrypter;
204
205 // Manages the tracking and generation of sync-points, mappings
206 // between a time in the display time and a segment index within
207 // a playlist
208 this.syncController_ = settings.syncController;
209 this.syncPoint_ = {
210 segmentIndex: 0,
211 time: 0
212 };
213
214 this.syncController_.on('syncinfoupdate', function () {
215 return _this.trigger('syncinfoupdate');
216 });
217
218 this.mediaSource_.addEventListener('sourceopen', function () {
219 return _this.ended_ = false;
220 });
221
222 // ...for determining the fetch location
223 this.fetchAtBuffer_ = false;
224
225 if (options.debug) {
226 this.logger_ = _videoJs2['default'].log.bind(_videoJs2['default'], 'segment-loader', this.loaderType_, '->');
227 }
228 }
229
230 /**
231 * reset all of our media stats
232 *
233 * @private
234 */
235
236 _createClass(SegmentLoader, [{
237 key: 'resetStats_',
238 value: function resetStats_() {
239 this.mediaBytesTransferred = 0;
240 this.mediaRequests = 0;
241 this.mediaRequestsAborted = 0;
242 this.mediaRequestsTimedout = 0;
243 this.mediaRequestsErrored = 0;
244 this.mediaTransferDuration = 0;
245 this.mediaSecondsLoaded = 0;
246 }
247
248 /**
249 * dispose of the SegmentLoader and reset to the default state
250 */
251 }, {
252 key: 'dispose',
253 value: function dispose() {
254 this.state = 'DISPOSED';
255 this.pause();
256 this.abort_();
257 if (this.sourceUpdater_) {
258 this.sourceUpdater_.dispose();
259 }
260 this.resetStats_();
261 }
262
263 /**
264 * abort anything that is currently doing on with the SegmentLoader
265 * and reset to a default state
266 */
267 }, {
268 key: 'abort',
269 value: function abort() {
270 if (this.state !== 'WAITING') {
271 if (this.pendingSegment_) {
272 this.pendingSegment_ = null;
273 }
274 return;
275 }
276
277 this.abort_();
278
279 // We aborted the requests we were waiting on, so reset the loader's state to READY
280 // since we are no longer "waiting" on any requests. XHR callback is not always run
281 // when the request is aborted. This will prevent the loader from being stuck in the
282 // WAITING state indefinitely.
283 this.state = 'READY';
284
285 // don't wait for buffer check timeouts to begin fetching the
286 // next segment
287 if (!this.paused()) {
288 this.monitorBuffer_();
289 }
290 }
291
292 /**
293 * abort all pending xhr requests and null any pending segements
294 *
295 * @private
296 */
297 }, {
298 key: 'abort_',
299 value: function abort_() {
300 if (this.pendingSegment_) {
301 this.pendingSegment_.abortRequests();
302 }
303
304 // clear out the segment being processed
305 this.pendingSegment_ = null;
306 }
307
308 /**
309 * set an error on the segment loader and null out any pending segements
310 *
311 * @param {Error} error the error to set on the SegmentLoader
312 * @return {Error} the error that was set or that is currently set
313 */
314 }, {
315 key: 'error',
316 value: function error(_error) {
317 if (typeof _error !== 'undefined') {
318 this.error_ = _error;
319 }
320
321 this.pendingSegment_ = null;
322 return this.error_;
323 }
324 }, {
325 key: 'endOfStream',
326 value: function endOfStream() {
327 this.ended_ = true;
328 this.pause();
329 this.trigger('ended');
330 }
331
332 /**
333 * Indicates which time ranges are buffered
334 *
335 * @return {TimeRange}
336 * TimeRange object representing the current buffered ranges
337 */
338 }, {
339 key: 'buffered_',
340 value: function buffered_() {
341 if (!this.sourceUpdater_) {
342 return _videoJs2['default'].createTimeRanges();
343 }
344
345 return this.sourceUpdater_.buffered();
346 }
347
348 /**
349 * Gets and sets init segment for the provided map
350 *
351 * @param {Object} map
352 * The map object representing the init segment to get or set
353 * @param {Boolean=} set
354 * If true, the init segment for the provided map should be saved
355 * @return {Object}
356 * map object for desired init segment
357 */
358 }, {
359 key: 'initSegment',
360 value: function initSegment(map) {
361 var set = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];
362
363 if (!map) {
364 return null;
365 }
366
367 var id = (0, _binUtils.initSegmentId)(map);
368 var storedMap = this.initSegments_[id];
369
370 if (set && !storedMap && map.bytes) {
371 this.initSegments_[id] = storedMap = {
372 resolvedUri: map.resolvedUri,
373 byterange: map.byterange,
374 bytes: map.bytes
375 };
376 }
377
378 return storedMap || map;
379 }
380
381 /**
382 * Returns true if all configuration required for loading is present, otherwise false.
383 *
384 * @return {Boolean} True if the all configuration is ready for loading
385 * @private
386 */
387 }, {
388 key: 'couldBeginLoading_',
389 value: function couldBeginLoading_() {
390 return this.playlist_ && (
391 // the source updater is created when init_ is called, so either having a
392 // source updater or being in the INIT state with a mimeType is enough
393 // to say we have all the needed configuration to start loading.
394 this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused();
395 }
396
397 /**
398 * load a playlist and start to fill the buffer
399 */
400 }, {
401 key: 'load',
402 value: function load() {
403 // un-pause
404 this.monitorBuffer_();
405
406 // if we don't have a playlist yet, keep waiting for one to be
407 // specified
408 if (!this.playlist_) {
409 return;
410 }
411
412 // not sure if this is the best place for this
413 this.syncController_.setDateTimeMapping(this.playlist_);
414
415 // if all the configuration is ready, initialize and begin loading
416 if (this.state === 'INIT' && this.couldBeginLoading_()) {
417 return this.init_();
418 }
419
420 // if we're in the middle of processing a segment already, don't
421 // kick off an additional segment request
422 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') {
423 return;
424 }
425
426 this.state = 'READY';
427 }
428
429 /**
430 * Once all the starting parameters have been specified, begin
431 * operation. This method should only be invoked from the INIT
432 * state.
433 *
434 * @private
435 */
436 }, {
437 key: 'init_',
438 value: function init_() {
439 this.state = 'READY';
440 this.sourceUpdater_ = new _sourceUpdater2['default'](this.mediaSource_, this.mimeType_);
441 this.resetEverything();
442 return this.monitorBuffer_();
443 }
444
445 /**
446 * set a playlist on the segment loader
447 *
448 * @param {PlaylistLoader} media the playlist to set on the segment loader
449 */
450 }, {
451 key: 'playlist',
452 value: function playlist(newPlaylist) {
453 var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
454
455 if (!newPlaylist) {
456 return;
457 }
458
459 var oldPlaylist = this.playlist_;
460 var segmentInfo = this.pendingSegment_;
461
462 this.playlist_ = newPlaylist;
463 this.xhrOptions_ = options;
464
465 // when we haven't started playing yet, the start of a live playlist
466 // is always our zero-time so force a sync update each time the playlist
467 // is refreshed from the server
468 if (!this.hasPlayed_()) {
469 newPlaylist.syncInfo = {
470 mediaSequence: newPlaylist.mediaSequence,
471 time: 0
472 };
473 }
474
475 // in VOD, this is always a rendition switch (or we updated our syncInfo above)
476 // in LIVE, we always want to update with new playlists (including refreshes)
477 this.trigger('syncinfoupdate');
478
479 // if we were unpaused but waiting for a playlist, start
480 // buffering now
481 if (this.state === 'INIT' && this.couldBeginLoading_()) {
482 return this.init_();
483 }
484
485 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
486 if (this.mediaIndex !== null) {
487 // we must "resync" the segment loader when we switch renditions and
488 // the segment loader is already synced to the previous rendition
489 this.resyncLoader();
490 }
491
492 // the rest of this function depends on `oldPlaylist` being defined
493 return;
494 }
495
496 // we reloaded the same playlist so we are in a live scenario
497 // and we will likely need to adjust the mediaIndex
498 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
499
500 this.logger_('mediaSequenceDiff', mediaSequenceDiff);
501
502 // update the mediaIndex on the SegmentLoader
503 // this is important because we can abort a request and this value must be
504 // equal to the last appended mediaIndex
505 if (this.mediaIndex !== null) {
506 this.mediaIndex -= mediaSequenceDiff;
507 }
508
509 // update the mediaIndex on the SegmentInfo object
510 // this is important because we will update this.mediaIndex with this value
511 // in `handleUpdateEnd_` after the segment has been successfully appended
512 if (segmentInfo) {
513 segmentInfo.mediaIndex -= mediaSequenceDiff;
514
515 // we need to update the referenced segment so that timing information is
516 // saved for the new playlist's segment, however, if the segment fell off the
517 // playlist, we can leave the old reference and just lose the timing info
518 if (segmentInfo.mediaIndex >= 0) {
519 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
520 }
521 }
522
523 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
524 }
525
526 /**
527 * Prevent the loader from fetching additional segments. If there
528 * is a segment request outstanding, it will finish processing
529 * before the loader halts. A segment loader can be unpaused by
530 * calling load().
531 */
532 }, {
533 key: 'pause',
534 value: function pause() {
535 if (this.checkBufferTimeout_) {
536 _globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
537
538 this.checkBufferTimeout_ = null;
539 }
540 }
541
542 /**
543 * Returns whether the segment loader is fetching additional
544 * segments when given the opportunity. This property can be
545 * modified through calls to pause() and load().
546 */
547 }, {
548 key: 'paused',
549 value: function paused() {
550 return this.checkBufferTimeout_ === null;
551 }
552
553 /**
554 * create/set the following mimetype on the SourceBuffer through a
555 * SourceUpdater
556 *
557 * @param {String} mimeType the mime type string to use
558 */
559 }, {
560 key: 'mimeType',
561 value: function mimeType(_mimeType) {
562 if (this.mimeType_) {
563 return;
564 }
565
566 this.mimeType_ = _mimeType;
567 // if we were unpaused but waiting for a sourceUpdater, start
568 // buffering now
569 if (this.state === 'INIT' && this.couldBeginLoading_()) {
570 this.init_();
571 }
572 }
573
574 /**
575 * Delete all the buffered data and reset the SegmentLoader
576 */
577 }, {
578 key: 'resetEverything',
579 value: function resetEverything() {
580 this.ended_ = false;
581 this.resetLoader();
582 this.remove(0, this.duration_());
583 this.trigger('reseteverything');
584 }
585
586 /**
587 * Force the SegmentLoader to resync and start loading around the currentTime instead
588 * of starting at the end of the buffer
589 *
590 * Useful for fast quality changes
591 */
592 }, {
593 key: 'resetLoader',
594 value: function resetLoader() {
595 this.fetchAtBuffer_ = false;
596 this.resyncLoader();
597 }
598
599 /**
600 * Force the SegmentLoader to restart synchronization and make a conservative guess
601 * before returning to the simple walk-forward method
602 */
603 }, {
604 key: 'resyncLoader',
605 value: function resyncLoader() {
606 this.mediaIndex = null;
607 this.syncPoint_ = null;
608 this.abort();
609 }
610
611 /**
612 * Remove any data in the source buffer between start and end times
613 * @param {Number} start - the start time of the region to remove from the buffer
614 * @param {Number} end - the end time of the region to remove from the buffer
615 */
616 }, {
617 key: 'remove',
618 value: function remove(start, end) {
619 if (this.sourceUpdater_) {
620 this.sourceUpdater_.remove(start, end);
621 }
622 (0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.segmentMetadataTrack_);
623 }
624
625 /**
626 * (re-)schedule monitorBufferTick_ to run as soon as possible
627 *
628 * @private
629 */
630 }, {
631 key: 'monitorBuffer_',
632 value: function monitorBuffer_() {
633 if (this.checkBufferTimeout_) {
634 _globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
635 }
636
637 this.checkBufferTimeout_ = _globalWindow2['default'].setTimeout(this.monitorBufferTick_.bind(this), 1);
638 }
639
640 /**
641 * As long as the SegmentLoader is in the READY state, periodically
642 * invoke fillBuffer_().
643 *
644 * @private
645 */
646 }, {
647 key: 'monitorBufferTick_',
648 value: function monitorBufferTick_() {
649 if (this.state === 'READY') {
650 this.fillBuffer_();
651 }
652
653 if (this.checkBufferTimeout_) {
654 _globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
655 }
656
657 this.checkBufferTimeout_ = _globalWindow2['default'].setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY);
658 }
659
660 /**
661 * fill the buffer with segements unless the sourceBuffers are
662 * currently updating
663 *
664 * Note: this function should only ever be called by monitorBuffer_
665 * and never directly
666 *
667 * @private
668 */
669 }, {
670 key: 'fillBuffer_',
671 value: function fillBuffer_() {
672 if (this.sourceUpdater_.updating()) {
673 return;
674 }
675
676 if (!this.syncPoint_) {
677 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
678 }
679
680 // see if we need to begin loading immediately
681 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);
682
683 if (!segmentInfo) {
684 return;
685 }
686
687 var isEndOfStream = detectEndOfStream(this.playlist_, this.mediaSource_, segmentInfo.mediaIndex);
688
689 if (isEndOfStream) {
690 this.endOfStream();
691 return;
692 }
693
694 if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 && this.mediaSource_.readyState === 'ended' && !this.seeking_()) {
695 return;
696 }
697
698 // We will need to change timestampOffset of the sourceBuffer if either of
699 // the following conditions are true:
700 // - The segment.timeline !== this.currentTimeline
701 // (we are crossing a discontinuity somehow)
702 // - The "timestampOffset" for the start of this segment is less than
703 // the currently set timestampOffset
704 if (segmentInfo.timeline !== this.currentTimeline_ || segmentInfo.startOfSegment !== null && segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset()) {
705 this.syncController_.reset();
706 segmentInfo.timestampOffset = segmentInfo.startOfSegment;
707 }
708
709 this.loadSegment_(segmentInfo);
710 }
711
712 /**
713 * Determines what segment request should be made, given current playback
714 * state.
715 *
716 * @param {TimeRanges} buffered - the state of the buffer
717 * @param {Object} playlist - the playlist object to fetch segments from
718 * @param {Number} mediaIndex - the previous mediaIndex fetched or null
719 * @param {Boolean} hasPlayed - a flag indicating whether we have played or not
720 * @param {Number} currentTime - the playback position in seconds
721 * @param {Object} syncPoint - a segment info object that describes the
722 * @returns {Object} a segment request object that describes the segment to load
723 */
724 }, {
725 key: 'checkBuffer_',
726 value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) {
727 var lastBufferedEnd = 0;
728 var startOfSegment = undefined;
729
730 if (buffered.length) {
731 lastBufferedEnd = buffered.end(buffered.length - 1);
732 }
733
734 var bufferedTime = Math.max(0, lastBufferedEnd - currentTime);
735
736 if (!playlist.segments.length) {
737 return null;
738 }
739
740 // if there is plenty of content buffered, and the video has
741 // been played before relax for awhile
742 if (bufferedTime >= this.goalBufferLength_()) {
743 return null;
744 }
745
746 // if the video has not yet played once, and we already have
747 // one segment downloaded do nothing
748 if (!hasPlayed && bufferedTime >= 1) {
749 return null;
750 }
751
752 this.logger_('checkBuffer_', 'mediaIndex:', mediaIndex, 'hasPlayed:', hasPlayed, 'currentTime:', currentTime, 'syncPoint:', syncPoint, 'fetchAtBuffer:', this.fetchAtBuffer_, 'bufferedTime:', bufferedTime);
753
754 // When the syncPoint is null, there is no way of determining a good
755 // conservative segment index to fetch from
756 // The best thing to do here is to get the kind of sync-point data by
757 // making a request
758 if (syncPoint === null) {
759 mediaIndex = this.getSyncSegmentCandidate_(playlist);
760 this.logger_('getSync', 'mediaIndex:', mediaIndex);
761 return this.generateSegmentInfo_(playlist, mediaIndex, null, true);
762 }
763
764 // Under normal playback conditions fetching is a simple walk forward
765 if (mediaIndex !== null) {
766 this.logger_('walkForward', 'mediaIndex:', mediaIndex + 1);
767 var segment = playlist.segments[mediaIndex];
768
769 if (segment && segment.end) {
770 startOfSegment = segment.end;
771 } else {
772 startOfSegment = lastBufferedEnd;
773 }
774 return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false);
775 }
776
777 // There is a sync-point but the lack of a mediaIndex indicates that
778 // we need to make a good conservative guess about which segment to
779 // fetch
780 if (this.fetchAtBuffer_) {
781 // Find the segment containing the end of the buffer
782 var mediaSourceInfo = _playlist2['default'].getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time);
783
784 mediaIndex = mediaSourceInfo.mediaIndex;
785 startOfSegment = mediaSourceInfo.startTime;
786 } else {
787 // Find the segment containing currentTime
788 var mediaSourceInfo = _playlist2['default'].getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time);
789
790 mediaIndex = mediaSourceInfo.mediaIndex;
791 startOfSegment = mediaSourceInfo.startTime;
792 }
793 this.logger_('getMediaIndexForTime', 'mediaIndex:', mediaIndex, 'startOfSegment:', startOfSegment);
794
795 return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false);
796 }
797
798 /**
799 * The segment loader has no recourse except to fetch a segment in the
800 * current playlist and use the internal timestamps in that segment to
801 * generate a syncPoint. This function returns a good candidate index
802 * for that process.
803 *
804 * @param {Object} playlist - the playlist object to look for a
805 * @returns {Number} An index of a segment from the playlist to load
806 */
807 }, {
808 key: 'getSyncSegmentCandidate_',
809 value: function getSyncSegmentCandidate_(playlist) {
810 var _this2 = this;
811
812 if (this.currentTimeline_ === -1) {
813 return 0;
814 }
815
816 var segmentIndexArray = playlist.segments.map(function (s, i) {
817 return {
818 timeline: s.timeline,
819 segmentIndex: i
820 };
821 }).filter(function (s) {
822 return s.timeline === _this2.currentTimeline_;
823 });
824
825 if (segmentIndexArray.length) {
826 return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex;
827 }
828
829 return Math.max(playlist.segments.length - 1, 0);
830 }
831 }, {
832 key: 'generateSegmentInfo_',
833 value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) {
834 if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) {
835 return null;
836 }
837
838 var segment = playlist.segments[mediaIndex];
839
840 return {
841 requestId: 'segment-loader-' + Math.random(),
842 // resolve the segment URL relative to the playlist
843 uri: segment.resolvedUri,
844 // the segment's mediaIndex at the time it was requested
845 mediaIndex: mediaIndex,
846 // whether or not to update the SegmentLoader's state with this
847 // segment's mediaIndex
848 isSyncRequest: isSyncRequest,
849 startOfSegment: startOfSegment,
850 // the segment's playlist
851 playlist: playlist,
852 // unencrypted bytes of the segment
853 bytes: null,
854 // when a key is defined for this segment, the encrypted bytes
855 encryptedBytes: null,
856 // The target timestampOffset for this segment when we append it
857 // to the source buffer
858 timestampOffset: null,
859 // The timeline that the segment is in
860 timeline: segment.timeline,
861 // The expected duration of the segment in seconds
862 duration: segment.duration,
863 // retain the segment in case the playlist updates while doing an async process
864 segment: segment
865 };
866 }
867
868 /**
869 * Determines if the network has enough bandwidth to complete the current segment
870 * request in a timely manner. If not, the request will be aborted early and bandwidth
871 * updated to trigger a playlist switch.
872 *
873 * @param {Object} stats
874 * Object containing stats about the request timing and size
875 * @return {Boolean} True if the request was aborted, false otherwise
876 * @private
877 */
878 }, {
879 key: 'abortRequestEarly_',
880 value: function abortRequestEarly_(stats) {
881 if (this.hls_.tech_.paused() ||
882 // Don't abort if the current playlist is on the lowestEnabledRendition
883 // TODO: Replace using timeout with a boolean indicating whether this playlist is
884 // the lowestEnabledRendition.
885 !this.xhrOptions_.timeout ||
886 // Don't abort if we have no bandwidth information to estimate segment sizes
887 !this.playlist_.attributes.BANDWIDTH) {
888 return false;
889 }
890
891 // Wait at least 1 second since the first byte of data has been received before
892 // using the calculated bandwidth from the progress event to allow the bitrate
893 // to stabilize
894 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
895 return false;
896 }
897
898 var currentTime = this.currentTime_();
899 var measuredBandwidth = stats.bandwidth;
900 var segmentDuration = this.pendingSegment_.duration;
901
902 var requestTimeRemaining = _playlist2['default'].estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived);
903
904 // Subtract 1 from the timeUntilRebuffer so we still consider an early abort
905 // if we are only left with less than 1 second when the request completes.
906 // A negative timeUntilRebuffering indicates we are already rebuffering
907 var timeUntilRebuffer = (0, _ranges.timeUntilRebuffer)(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1;
908
909 // Only consider aborting early if the estimated time to finish the download
910 // is larger than the estimated time until the player runs out of forward buffer
911 if (requestTimeRemaining <= timeUntilRebuffer) {
912 return false;
913 }
914
915 var switchCandidate = (0, _playlistSelectors.minRebufferMaxBandwidthSelector)({
916 master: this.hls_.playlists.master,
917 currentTime: currentTime,
918 bandwidth: measuredBandwidth,
919 duration: this.duration_(),
920 segmentDuration: segmentDuration,
921 timeUntilRebuffer: timeUntilRebuffer,
922 currentTimeline: this.currentTimeline_,
923 syncController: this.syncController_
924 });
925
926 if (!switchCandidate) {
927 return;
928 }
929
930 var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer;
931
932 var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;
933
934 var minimumTimeSaving = 0.5;
935
936 // If we are already rebuffering, increase the amount of variance we add to the
937 // potential round trip time of the new request so that we are not too aggressive
938 // with switching to a playlist that might save us a fraction of a second.
939 if (timeUntilRebuffer <= _ranges.TIME_FUDGE_FACTOR) {
940 minimumTimeSaving = 1;
941 }
942
943 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) {
944 return false;
945 }
946
947 // set the bandwidth to that of the desired playlist being sure to scale by
948 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
949 // don't trigger a bandwidthupdate as the bandwidth is artifial
950 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * _config2['default'].BANDWIDTH_VARIANCE + 1;
951 this.abort();
952 this.trigger('earlyabort');
953 return true;
954 }
955
956 /**
957 * XHR `progress` event handler
958 *
959 * @param {Event}
960 * The XHR `progress` event
961 * @param {Object} simpleSegment
962 * A simplified segment object copy
963 * @private
964 */
965 }, {
966 key: 'handleProgress_',
967 value: function handleProgress_(event, simpleSegment) {
968 if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) {
969 return;
970 }
971
972 this.trigger('progress');
973 }
974
975 /**
976 * load a specific segment from a request into the buffer
977 *
978 * @private
979 */
980 }, {
981 key: 'loadSegment_',
982 value: function loadSegment_(segmentInfo) {
983 this.state = 'WAITING';
984 this.pendingSegment_ = segmentInfo;
985 this.trimBackBuffer_(segmentInfo);
986
987 segmentInfo.abortRequests = (0, _mediaSegmentRequest.mediaSegmentRequest)(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.createSimplifiedSegmentObj_(segmentInfo),
988 // progress callback
989 this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this));
990 }
991
992 /**
993 * trim the back buffer so that we don't have too much data
994 * in the source buffer
995 *
996 * @private
997 *
998 * @param {Object} segmentInfo - the current segment
999 */
1000 }, {
1001 key: 'trimBackBuffer_',
1002 value: function trimBackBuffer_(segmentInfo) {
1003 var removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10);
1004
1005 // Chrome has a hard limit of 150MB of
1006 // buffer and a very conservative "garbage collector"
1007 // We manually clear out the old buffer to ensure
1008 // we don't trigger the QuotaExceeded error
1009 // on the source buffer during subsequent appends
1010
1011 if (removeToTime > 0) {
1012 this.remove(0, removeToTime);
1013 }
1014 }
1015
1016 /**
1017 * created a simplified copy of the segment object with just the
1018 * information necessary to perform the XHR and decryption
1019 *
1020 * @private
1021 *
1022 * @param {Object} segmentInfo - the current segment
1023 * @returns {Object} a simplified segment object copy
1024 */
1025 }, {
1026 key: 'createSimplifiedSegmentObj_',
1027 value: function createSimplifiedSegmentObj_(segmentInfo) {
1028 var segment = segmentInfo.segment;
1029 var simpleSegment = {
1030 resolvedUri: segment.resolvedUri,
1031 byterange: segment.byterange,
1032 requestId: segmentInfo.requestId
1033 };
1034
1035 if (segment.key) {
1036 // if the media sequence is greater than 2^32, the IV will be incorrect
1037 // assuming 10s segments, that would be about 1300 years
1038 var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]);
1039
1040 simpleSegment.key = {
1041 resolvedUri: segment.key.resolvedUri,
1042 iv: iv
1043 };
1044 }
1045
1046 if (segment.map) {
1047 simpleSegment.map = this.initSegment(segment.map);
1048 }
1049
1050 return simpleSegment;
1051 }
1052
1053 /**
1054 * Handle the callback from the segmentRequest function and set the
1055 * associated SegmentLoader state and errors if necessary
1056 *
1057 * @private
1058 */
1059 }, {
1060 key: 'segmentRequestFinished_',
1061 value: function segmentRequestFinished_(error, simpleSegment) {
1062 // every request counts as a media request even if it has been aborted
1063 // or canceled due to a timeout
1064 this.mediaRequests += 1;
1065
1066 if (simpleSegment.stats) {
1067 this.mediaBytesTransferred += simpleSegment.stats.bytesReceived;
1068 this.mediaTransferDuration += simpleSegment.stats.roundTripTime;
1069 }
1070
1071 // The request was aborted and the SegmentLoader has already been reset
1072 if (!this.pendingSegment_) {
1073 this.mediaRequestsAborted += 1;
1074 return;
1075 }
1076
1077 // the request was aborted and the SegmentLoader has already started
1078 // another request. this can happen when the timeout for an aborted
1079 // request triggers due to a limitation in the XHR library
1080 // do not count this as any sort of request or we risk double-counting
1081 if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
1082 return;
1083 }
1084
1085 // an error occurred from the active pendingSegment_ so reset everything
1086 if (error) {
1087 this.pendingSegment_ = null;
1088 this.state = 'READY';
1089
1090 // the requests were aborted just record the aborted stat and exit
1091 // this is not a true error condition and nothing corrective needs
1092 // to be done
1093 if (error.code === _mediaSegmentRequest.REQUEST_ERRORS.ABORTED) {
1094 this.mediaRequestsAborted += 1;
1095 return;
1096 }
1097
1098 this.pause();
1099
1100 // the error is really just that at least one of the requests timed-out
1101 // set the bandwidth to a very low value and trigger an ABR switch to
1102 // take emergency action
1103 if (error.code === _mediaSegmentRequest.REQUEST_ERRORS.TIMEOUT) {
1104 this.mediaRequestsTimedout += 1;
1105 this.bandwidth = 1;
1106 this.roundTrip = NaN;
1107 this.trigger('bandwidthupdate');
1108 return;
1109 }
1110
1111 // if control-flow has arrived here, then the error is real
1112 // emit an error event to blacklist the current playlist
1113 this.mediaRequestsErrored += 1;
1114 this.error(error);
1115 this.trigger('error');
1116 return;
1117 }
1118
1119 // the response was a success so set any bandwidth stats the request
1120 // generated for ABR purposes
1121 this.bandwidth = simpleSegment.stats.bandwidth;
1122 this.roundTrip = simpleSegment.stats.roundTripTime;
1123
1124 // if this request included an initialization segment, save that data
1125 // to the initSegment cache
1126 if (simpleSegment.map) {
1127 simpleSegment.map = this.initSegment(simpleSegment.map, true);
1128 }
1129
1130 this.processSegmentResponse_(simpleSegment);
1131 }
1132
1133 /**
1134 * Move any important data from the simplified segment object
1135 * back to the real segment object for future phases
1136 *
1137 * @private
1138 */
1139 }, {
1140 key: 'processSegmentResponse_',
1141 value: function processSegmentResponse_(simpleSegment) {
1142 var segmentInfo = this.pendingSegment_;
1143
1144 segmentInfo.bytes = simpleSegment.bytes;
1145 if (simpleSegment.map) {
1146 segmentInfo.segment.map.bytes = simpleSegment.map.bytes;
1147 }
1148
1149 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
1150 this.handleSegment_();
1151 }
1152
1153 /**
1154 * append a decrypted segement to the SourceBuffer through a SourceUpdater
1155 *
1156 * @private
1157 */
1158 }, {
1159 key: 'handleSegment_',
1160 value: function handleSegment_() {
1161 var _this3 = this;
1162
1163 if (!this.pendingSegment_) {
1164 this.state = 'READY';
1165 return;
1166 }
1167
1168 var segmentInfo = this.pendingSegment_;
1169 var segment = segmentInfo.segment;
1170 var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo);
1171
1172 // When we have our first timing info, determine what media types this loader is
1173 // dealing with. Although we're maintaining extra state, it helps to preserve the
1174 // separation of segment loader from the actual source buffers.
1175 if (typeof this.startingMedia_ === 'undefined' && timingInfo && (
1176 // Guard against cases where we're not getting timing info at all until we are
1177 // certain that all streams will provide it.
1178 timingInfo.containsAudio || timingInfo.containsVideo)) {
1179 this.startingMedia_ = {
1180 containsAudio: timingInfo.containsAudio,
1181 containsVideo: timingInfo.containsVideo
1182 };
1183 }
1184
1185 var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo);
1186
1187 if (illegalMediaSwitchError) {
1188 this.error({
1189 message: illegalMediaSwitchError,
1190 blacklistDuration: Infinity
1191 });
1192 this.trigger('error');
1193 return;
1194 }
1195
1196 if (segmentInfo.isSyncRequest) {
1197 this.trigger('syncinfoupdate');
1198 this.pendingSegment_ = null;
1199 this.state = 'READY';
1200 return;
1201 }
1202
1203 if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
1204 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
1205 // fired when a timestamp offset is set in HLS (can also identify discontinuities)
1206 this.trigger('timestampoffset');
1207 }
1208
1209 var timelineMapping = this.syncController_.mappingForTimeline(segmentInfo.timeline);
1210
1211 if (timelineMapping !== null) {
1212 this.trigger({
1213 type: 'segmenttimemapping',
1214 mapping: timelineMapping
1215 });
1216 }
1217
1218 this.state = 'APPENDING';
1219
1220 // if the media initialization segment is changing, append it
1221 // before the content segment
1222 if (segment.map) {
1223 (function () {
1224 var initId = (0, _binUtils.initSegmentId)(segment.map);
1225
1226 if (!_this3.activeInitSegmentId_ || _this3.activeInitSegmentId_ !== initId) {
1227 var initSegment = _this3.initSegment(segment.map);
1228
1229 _this3.sourceUpdater_.appendBuffer(initSegment.bytes, function () {
1230 _this3.activeInitSegmentId_ = initId;
1231 });
1232 }
1233 })();
1234 }
1235
1236 segmentInfo.byteLength = segmentInfo.bytes.byteLength;
1237 if (typeof segment.start === 'number' && typeof segment.end === 'number') {
1238 this.mediaSecondsLoaded += segment.end - segment.start;
1239 } else {
1240 this.mediaSecondsLoaded += segment.duration;
1241 }
1242
1243 this.sourceUpdater_.appendBuffer(segmentInfo.bytes, this.handleUpdateEnd_.bind(this));
1244 }
1245
1246 /**
1247 * callback to run when appendBuffer is finished. detects if we are
1248 * in a good state to do things with the data we got, or if we need
1249 * to wait for more
1250 *
1251 * @private
1252 */
1253 }, {
1254 key: 'handleUpdateEnd_',
1255 value: function handleUpdateEnd_() {
1256 this.logger_('handleUpdateEnd_', 'segmentInfo:', this.pendingSegment_);
1257
1258 if (!this.pendingSegment_) {
1259 this.state = 'READY';
1260 if (!this.paused()) {
1261 this.monitorBuffer_();
1262 }
1263 return;
1264 }
1265
1266 var segmentInfo = this.pendingSegment_;
1267 var segment = segmentInfo.segment;
1268 var isWalkingForward = this.mediaIndex !== null;
1269
1270 this.pendingSegment_ = null;
1271 this.recordThroughput_(segmentInfo);
1272 this.addSegmentMetadataCue_(segmentInfo);
1273
1274 this.state = 'READY';
1275
1276 this.mediaIndex = segmentInfo.mediaIndex;
1277 this.fetchAtBuffer_ = true;
1278 this.currentTimeline_ = segmentInfo.timeline;
1279
1280 // We must update the syncinfo to recalculate the seekable range before
1281 // the following conditional otherwise it may consider this a bad "guess"
1282 // and attempt to resync when the post-update seekable window and live
1283 // point would mean that this was the perfect segment to fetch
1284 this.trigger('syncinfoupdate');
1285
1286 // If we previously appended a segment that ends more than 3 targetDurations before
1287 // the currentTime_ that means that our conservative guess was too conservative.
1288 // In that case, reset the loader state so that we try to use any information gained
1289 // from the previous request to create a new, more accurate, sync-point.
1290 if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) {
1291 this.resetEverything();
1292 return;
1293 }
1294
1295 // Don't do a rendition switch unless we have enough time to get a sync segment
1296 // and conservatively guess
1297 if (isWalkingForward) {
1298 this.trigger('bandwidthupdate');
1299 }
1300 this.trigger('progress');
1301
1302 // any time an update finishes and the last segment is in the
1303 // buffer, end the stream. this ensures the "ended" event will
1304 // fire if playback reaches that point.
1305 var isEndOfStream = detectEndOfStream(segmentInfo.playlist, this.mediaSource_, segmentInfo.mediaIndex + 1);
1306
1307 if (isEndOfStream) {
1308 this.endOfStream();
1309 }
1310
1311 if (!this.paused()) {
1312 this.monitorBuffer_();
1313 }
1314 }
1315
1316 /**
1317 * Records the current throughput of the decrypt, transmux, and append
1318 * portion of the semgment pipeline. `throughput.rate` is a the cumulative
1319 * moving average of the throughput. `throughput.count` is the number of
1320 * data points in the average.
1321 *
1322 * @private
1323 * @param {Object} segmentInfo the object returned by loadSegment
1324 */
1325 }, {
1326 key: 'recordThroughput_',
1327 value: function recordThroughput_(segmentInfo) {
1328 var rate = this.throughput.rate;
1329 // Add one to the time to ensure that we don't accidentally attempt to divide
1330 // by zero in the case where the throughput is ridiculously high
1331 var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1;
1332 // Multiply by 8000 to convert from bytes/millisecond to bits/second
1333 var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000);
1334
1335 // This is just a cumulative moving average calculation:
1336 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
1337 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count;
1338 }
1339
1340 /**
1341 * A debugging logger noop that is set to console.log only if debugging
1342 * is enabled globally
1343 *
1344 * @private
1345 */
1346 }, {
1347 key: 'logger_',
1348 value: function logger_() {}
1349
1350 /**
1351 * Adds a cue to the segment-metadata track with some metadata information about the
1352 * segment
1353 *
1354 * @private
1355 * @param {Object} segmentInfo
1356 * the object returned by loadSegment
1357 * @method addSegmentMetadataCue_
1358 */
1359 }, {
1360 key: 'addSegmentMetadataCue_',
1361 value: function addSegmentMetadataCue_(segmentInfo) {
1362 if (!this.segmentMetadataTrack_) {
1363 return;
1364 }
1365
1366 var segment = segmentInfo.segment;
1367 var start = segment.start;
1368 var end = segment.end;
1369
1370 // Do not try adding the cue if the start and end times are invalid.
1371 if (!finite(start) || !finite(end)) {
1372 return;
1373 }
1374
1375 (0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.segmentMetadataTrack_);
1376
1377 var Cue = _globalWindow2['default'].WebKitDataCue || _globalWindow2['default'].VTTCue;
1378 var value = {
1379 uri: segmentInfo.uri,
1380 timeline: segmentInfo.timeline,
1381 playlist: segmentInfo.playlist.uri,
1382 start: start,
1383 end: end
1384 };
1385 var data = JSON.stringify(value);
1386 var cue = new Cue(start, end, data);
1387
1388 // Attach the metadata to the value property of the cue to keep consistency between
1389 // the differences of WebKitDataCue in safari and VTTCue in other browsers
1390 cue.value = value;
1391
1392 this.segmentMetadataTrack_.addCue(cue);
1393 }
1394 }]);
1395
1396 return SegmentLoader;
1397})(_videoJs2['default'].EventTarget);
1398
1399exports['default'] = SegmentLoader;
\No newline at end of file