UNPKG

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