UNPKG

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