UNPKG

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