UNPKG

115 kBJavaScriptView Raw
1/**
2 * @file segment-loader.js
3 */
4import Playlist from './playlist';
5import videojs from 'video.js';
6import Config from './config';
7import window from 'global/window';
8import { initSegmentId, segmentKeyId } from './bin-utils';
9import { mediaSegmentRequest, REQUEST_ERRORS } from './media-segment-request';
10import segmentTransmuxer from './segment-transmuxer';
11import { TIME_FUDGE_FACTOR, timeUntilRebuffer as timeUntilRebuffer_ } from './ranges';
12import { minRebufferMaxBandwidthSelector } from './playlist-selectors';
13import logger from './util/logger';
14import { concatSegments } from './util/segment';
15import {
16 createCaptionsTrackIfNotExists,
17 createMetadataTrackIfNotExists,
18 addMetadata,
19 addCaptionData,
20 removeCuesFromTrack
21} from './util/text-tracks';
22import { gopsSafeToAlignWith, removeGopBuffer, updateGopBuffer } from './util/gops';
23import shallowEqual from './util/shallow-equal.js';
24import { QUOTA_EXCEEDED_ERR } from './error-codes';
25import {timeRangesToArray, lastBufferedEnd, timeAheadOf} from './ranges.js';
26import {getKnownPartCount} from './playlist.js';
27
28/**
29 * The segment loader has no recourse except to fetch a segment in the
30 * current playlist and use the internal timestamps in that segment to
31 * generate a syncPoint. This function returns a good candidate index
32 * for that process.
33 *
34 * @param {Array} segments - the segments array from a playlist.
35 * @return {number} An index of a segment from the playlist to load
36 */
37export const getSyncSegmentCandidate = function(currentTimeline, segments, targetTime) {
38 segments = segments || [];
39 const timelineSegments = [];
40 let time = 0;
41
42 for (let i = 0; i < segments.length; i++) {
43 const segment = segments[i];
44
45 if (currentTimeline === segment.timeline) {
46 timelineSegments.push(i);
47 time += segment.duration;
48
49 if (time > targetTime) {
50 return i;
51 }
52 }
53 }
54
55 if (timelineSegments.length === 0) {
56 return 0;
57 }
58
59 // default to the last timeline segment
60 return timelineSegments[timelineSegments.length - 1];
61};
62
63// In the event of a quota exceeded error, keep at least one second of back buffer. This
64// number was arbitrarily chosen and may be updated in the future, but seemed reasonable
65// as a start to prevent any potential issues with removing content too close to the
66// playhead.
67const MIN_BACK_BUFFER = 1;
68
69// in ms
70const CHECK_BUFFER_DELAY = 500;
71const finite = (num) => typeof num === 'number' && isFinite(num);
72// With most content hovering around 30fps, if a segment has a duration less than a half
73// frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will
74// not accurately reflect the rest of the content.
75const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60;
76
77export const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => {
78 // Although these checks should most likely cover non 'main' types, for now it narrows
79 // the scope of our checks.
80 if (loaderType !== 'main' || !startingMedia || !trackInfo) {
81 return null;
82 }
83
84 if (!trackInfo.hasAudio && !trackInfo.hasVideo) {
85 return 'Neither audio nor video found in segment.';
86 }
87
88 if (startingMedia.hasVideo && !trackInfo.hasVideo) {
89 return 'Only audio found in segment when we expected video.' +
90 ' We can\'t switch to audio only from a stream that had video.' +
91 ' To get rid of this message, please add codec information to the manifest.';
92 }
93
94 if (!startingMedia.hasVideo && trackInfo.hasVideo) {
95 return 'Video found in segment when we expected only audio.' +
96 ' We can\'t switch to a stream with video from an audio only stream.' +
97 ' To get rid of this message, please add codec information to the manifest.';
98 }
99
100 return null;
101};
102
103/**
104 * Calculates a time value that is safe to remove from the back buffer without interrupting
105 * playback.
106 *
107 * @param {TimeRange} seekable
108 * The current seekable range
109 * @param {number} currentTime
110 * The current time of the player
111 * @param {number} targetDuration
112 * The target duration of the current playlist
113 * @return {number}
114 * Time that is safe to remove from the back buffer without interrupting playback
115 */
116export const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => {
117 // 30 seconds before the playhead provides a safe default for trimming.
118 //
119 // Choosing a reasonable default is particularly important for high bitrate content and
120 // VOD videos/live streams with large windows, as the buffer may end up overfilled and
121 // throw an APPEND_BUFFER_ERR.
122 let trimTime = currentTime - Config.BACK_BUFFER_LENGTH;
123
124 if (seekable.length) {
125 // Some live playlists may have a shorter window of content than the full allowed back
126 // buffer. For these playlists, don't save content that's no longer within the window.
127 trimTime = Math.max(trimTime, seekable.start(0));
128 }
129
130 // Don't remove within target duration of the current time to avoid the possibility of
131 // removing the GOP currently being played, as removing it can cause playback stalls.
132 const maxTrimTime = currentTime - targetDuration;
133
134 return Math.min(maxTrimTime, trimTime);
135};
136
137export const segmentInfoString = (segmentInfo) => {
138 const {
139 startOfSegment,
140 duration,
141 segment,
142 part,
143 playlist: {
144 mediaSequence: seq,
145 id,
146 segments = []
147 },
148 mediaIndex: index,
149 partIndex,
150 timeline
151 } = segmentInfo;
152
153 const segmentLen = segments.length - 1;
154 let selection = 'mediaIndex/partIndex increment';
155
156 if (segmentInfo.getMediaInfoForTime) {
157 selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`;
158 } else if (segmentInfo.isSyncRequest) {
159 selection = 'getSyncSegmentCandidate (isSyncRequest)';
160 }
161
162 if (segmentInfo.independent) {
163 selection += ` with independent ${segmentInfo.independent}`;
164 }
165
166 const hasPartIndex = typeof partIndex === 'number';
167 const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment';
168 const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({preloadSegment: segment}) - 1 : 0;
169
170 return `${name} [${seq + index}/${seq + segmentLen}]` +
171 (hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') +
172 ` segment start/end [${segment.start} => ${segment.end}]` +
173 (hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') +
174 ` startOfSegment [${startOfSegment}]` +
175 ` duration [${duration}]` +
176 ` timeline [${timeline}]` +
177 ` selected by [${selection}]` +
178 ` playlist [${id}]`;
179};
180
181const timingInfoPropertyForMedia = (mediaType) => `${mediaType}TimingInfo`;
182
183/**
184 * Returns the timestamp offset to use for the segment.
185 *
186 * @param {number} segmentTimeline
187 * The timeline of the segment
188 * @param {number} currentTimeline
189 * The timeline currently being followed by the loader
190 * @param {number} startOfSegment
191 * The estimated segment start
192 * @param {TimeRange[]} buffered
193 * The loader's buffer
194 * @param {boolean} overrideCheck
195 * If true, no checks are made to see if the timestamp offset value should be set,
196 * but sets it directly to a value.
197 *
198 * @return {number|null}
199 * Either a number representing a new timestamp offset, or null if the segment is
200 * part of the same timeline
201 */
202export const timestampOffsetForSegment = ({
203 segmentTimeline,
204 currentTimeline,
205 startOfSegment,
206 buffered,
207 overrideCheck
208}) => {
209 // Check to see if we are crossing a discontinuity to see if we need to set the
210 // timestamp offset on the transmuxer and source buffer.
211 //
212 // Previously, we changed the timestampOffset if the start of this segment was less than
213 // the currently set timestampOffset, but this isn't desirable as it can produce bad
214 // behavior, especially around long running live streams.
215 if (!overrideCheck && segmentTimeline === currentTimeline) {
216 return null;
217 }
218
219 // When changing renditions, it's possible to request a segment on an older timeline. For
220 // instance, given two renditions with the following:
221 //
222 // #EXTINF:10
223 // segment1
224 // #EXT-X-DISCONTINUITY
225 // #EXTINF:10
226 // segment2
227 // #EXTINF:10
228 // segment3
229 //
230 // And the current player state:
231 //
232 // current time: 8
233 // buffer: 0 => 20
234 //
235 // The next segment on the current rendition would be segment3, filling the buffer from
236 // 20s onwards. However, if a rendition switch happens after segment2 was requested,
237 // then the next segment to be requested will be segment1 from the new rendition in
238 // order to fill time 8 and onwards. Using the buffered end would result in repeated
239 // content (since it would position segment1 of the new rendition starting at 20s). This
240 // case can be identified when the new segment's timeline is a prior value. Instead of
241 // using the buffered end, the startOfSegment can be used, which, hopefully, will be
242 // more accurate to the actual start time of the segment.
243 if (segmentTimeline < currentTimeline) {
244 return startOfSegment;
245 }
246
247 // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that
248 // value uses the end of the last segment if it is available. While this value
249 // should often be correct, it's better to rely on the buffered end, as the new
250 // content post discontinuity should line up with the buffered end as if it were
251 // time 0 for the new content.
252 return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment;
253};
254
255/**
256 * Returns whether or not the loader should wait for a timeline change from the timeline
257 * change controller before processing the segment.
258 *
259 * Primary timing in VHS goes by video. This is different from most media players, as
260 * audio is more often used as the primary timing source. For the foreseeable future, VHS
261 * will continue to use video as the primary timing source, due to the current logic and
262 * expectations built around it.
263
264 * Since the timing follows video, in order to maintain sync, the video loader is
265 * responsible for setting both audio and video source buffer timestamp offsets.
266 *
267 * Setting different values for audio and video source buffers could lead to
268 * desyncing. The following examples demonstrate some of the situations where this
269 * distinction is important. Note that all of these cases involve demuxed content. When
270 * content is muxed, the audio and video are packaged together, therefore syncing
271 * separate media playlists is not an issue.
272 *
273 * CASE 1: Audio prepares to load a new timeline before video:
274 *
275 * Timeline: 0 1
276 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
277 * Audio Loader: ^
278 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
279 * Video Loader ^
280 *
281 * In the above example, the audio loader is preparing to load the 6th segment, the first
282 * after a discontinuity, while the video loader is still loading the 5th segment, before
283 * the discontinuity.
284 *
285 * If the audio loader goes ahead and loads and appends the 6th segment before the video
286 * loader crosses the discontinuity, then when appended, the 6th audio segment will use
287 * the timestamp offset from timeline 0. This will likely lead to desyncing. In addition,
288 * the audio loader must provide the audioAppendStart value to trim the content in the
289 * transmuxer, and that value relies on the audio timestamp offset. Since the audio
290 * timestamp offset is set by the video (main) loader, the audio loader shouldn't load the
291 * segment until that value is provided.
292 *
293 * CASE 2: Video prepares to load a new timeline before audio:
294 *
295 * Timeline: 0 1
296 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
297 * Audio Loader: ^
298 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
299 * Video Loader ^
300 *
301 * In the above example, the video loader is preparing to load the 6th segment, the first
302 * after a discontinuity, while the audio loader is still loading the 5th segment, before
303 * the discontinuity.
304 *
305 * If the video loader goes ahead and loads and appends the 6th segment, then once the
306 * segment is loaded and processed, both the video and audio timestamp offsets will be
307 * set, since video is used as the primary timing source. This is to ensure content lines
308 * up appropriately, as any modifications to the video timing are reflected by audio when
309 * the video loader sets the audio and video timestamp offsets to the same value. However,
310 * setting the timestamp offset for audio before audio has had a chance to change
311 * timelines will likely lead to desyncing, as the audio loader will append segment 5 with
312 * a timestamp intended to apply to segments from timeline 1 rather than timeline 0.
313 *
314 * CASE 3: When seeking, audio prepares to load a new timeline before video
315 *
316 * Timeline: 0 1
317 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
318 * Audio Loader: ^
319 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
320 * Video Loader ^
321 *
322 * In the above example, both audio and video loaders are loading segments from timeline
323 * 0, but imagine that the seek originated from timeline 1.
324 *
325 * When seeking to a new timeline, the timestamp offset will be set based on the expected
326 * segment start of the loaded video segment. In order to maintain sync, the audio loader
327 * must wait for the video loader to load its segment and update both the audio and video
328 * timestamp offsets before it may load and append its own segment. This is the case
329 * whether the seek results in a mismatched segment request (e.g., the audio loader
330 * chooses to load segment 3 and the video loader chooses to load segment 4) or the
331 * loaders choose to load the same segment index from each playlist, as the segments may
332 * not be aligned perfectly, even for matching segment indexes.
333 *
334 * @param {Object} timelinechangeController
335 * @param {number} currentTimeline
336 * The timeline currently being followed by the loader
337 * @param {number} segmentTimeline
338 * The timeline of the segment being loaded
339 * @param {('main'|'audio')} loaderType
340 * The loader type
341 * @param {boolean} audioDisabled
342 * Whether the audio is disabled for the loader. This should only be true when the
343 * loader may have muxed audio in its segment, but should not append it, e.g., for
344 * the main loader when an alternate audio playlist is active.
345 *
346 * @return {boolean}
347 * Whether the loader should wait for a timeline change from the timeline change
348 * controller before processing the segment
349 */
350export const shouldWaitForTimelineChange = ({
351 timelineChangeController,
352 currentTimeline,
353 segmentTimeline,
354 loaderType,
355 audioDisabled
356}) => {
357 if (currentTimeline === segmentTimeline) {
358 return false;
359 }
360
361 if (loaderType === 'audio') {
362 const lastMainTimelineChange = timelineChangeController.lastTimelineChange({
363 type: 'main'
364 });
365
366 // Audio loader should wait if:
367 //
368 // * main hasn't had a timeline change yet (thus has not loaded its first segment)
369 // * main hasn't yet changed to the timeline audio is looking to load
370 return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline;
371 }
372
373 // The main loader only needs to wait for timeline changes if there's demuxed audio.
374 // Otherwise, there's nothing to wait for, since audio would be muxed into the main
375 // loader's segments (or the content is audio/video only and handled by the main
376 // loader).
377 if (loaderType === 'main' && audioDisabled) {
378 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({
379 type: 'audio'
380 });
381
382 // Main loader should wait for the audio loader if audio is not pending a timeline
383 // change to the current timeline.
384 //
385 // Since the main loader is responsible for setting the timestamp offset for both
386 // audio and video, the main loader must wait for audio to be about to change to its
387 // timeline before setting the offset, otherwise, if audio is behind in loading,
388 // segments from the previous timeline would be adjusted by the new timestamp offset.
389 //
390 // This requirement means that video will not cross a timeline until the audio is
391 // about to cross to it, so that way audio and video will always cross the timeline
392 // together.
393 //
394 // In addition to normal timeline changes, these rules also apply to the start of a
395 // stream (going from a non-existent timeline, -1, to timeline 0). It's important
396 // that these rules apply to the first timeline change because if they did not, it's
397 // possible that the main loader will cross two timelines before the audio loader has
398 // crossed one. Logic may be implemented to handle the startup as a special case, but
399 // it's easier to simply treat all timeline changes the same.
400 if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) {
401 return false;
402 }
403
404 return true;
405 }
406
407 return false;
408};
409
410export const mediaDuration = (timingInfos) => {
411 let maxDuration = 0;
412
413 ['video', 'audio'].forEach(function(type) {
414 const typeTimingInfo = timingInfos[`${type}TimingInfo`];
415
416 if (!typeTimingInfo) {
417 return;
418 }
419 const {start, end} = typeTimingInfo;
420 let duration;
421
422 if (typeof start === 'bigint' || typeof end === 'bigint') {
423 duration = window.BigInt(end) - window.BigInt(start);
424 } else if (typeof start === 'number' && typeof end === 'number') {
425 duration = end - start;
426 }
427
428 if (typeof duration !== 'undefined' && duration > maxDuration) {
429 maxDuration = duration;
430 }
431 });
432
433 // convert back to a number if it is lower than MAX_SAFE_INTEGER
434 // as we only need BigInt when we are above that.
435 if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) {
436 maxDuration = Number(maxDuration);
437 }
438
439 return maxDuration;
440};
441
442export const segmentTooLong = ({ segmentDuration, maxDuration }) => {
443 // 0 duration segments are most likely due to metadata only segments or a lack of
444 // information.
445 if (!segmentDuration) {
446 return false;
447 }
448
449 // For HLS:
450 //
451 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1
452 // The EXTINF duration of each Media Segment in the Playlist
453 // file, when rounded to the nearest integer, MUST be less than or equal
454 // to the target duration; longer segments can trigger playback stalls
455 // or other errors.
456 //
457 // For DASH, the mpd-parser uses the largest reported segment duration as the target
458 // duration. Although that reported duration is occasionally approximate (i.e., not
459 // exact), a strict check may report that a segment is too long more often in DASH.
460 return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR;
461};
462
463export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => {
464 // Right now we aren't following DASH's timing model exactly, so only perform
465 // this check for HLS content.
466 if (sourceType !== 'hls') {
467 return null;
468 }
469
470 const segmentDuration = mediaDuration({
471 audioTimingInfo: segmentInfo.audioTimingInfo,
472 videoTimingInfo: segmentInfo.videoTimingInfo
473 });
474
475 // Don't report if we lack information.
476 //
477 // If the segment has a duration of 0 it is either a lack of information or a
478 // metadata only segment and shouldn't be reported here.
479 if (!segmentDuration) {
480 return null;
481 }
482
483 const targetDuration = segmentInfo.playlist.targetDuration;
484
485 const isSegmentWayTooLong = segmentTooLong({
486 segmentDuration,
487 maxDuration: targetDuration * 2
488 });
489 const isSegmentSlightlyTooLong = segmentTooLong({
490 segmentDuration,
491 maxDuration: targetDuration
492 });
493
494 const segmentTooLongMessage = `Segment with index ${segmentInfo.mediaIndex} ` +
495 `from playlist ${segmentInfo.playlist.id} ` +
496 `has a duration of ${segmentDuration} ` +
497 `when the reported duration is ${segmentInfo.duration} ` +
498 `and the target duration is ${targetDuration}. ` +
499 'For HLS content, a duration in excess of the target duration may result in ' +
500 'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' +
501 'more details: ' +
502 'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1';
503
504 if (isSegmentWayTooLong || isSegmentSlightlyTooLong) {
505 return {
506 severity: isSegmentWayTooLong ? 'warn' : 'info',
507 message: segmentTooLongMessage
508 };
509 }
510
511 return null;
512};
513
514/**
515 * An object that manages segment loading and appending.
516 *
517 * @class SegmentLoader
518 * @param {Object} options required and optional options
519 * @extends videojs.EventTarget
520 */
521export default class SegmentLoader extends videojs.EventTarget {
522 constructor(settings, options = {}) {
523 super();
524 // check pre-conditions
525 if (!settings) {
526 throw new TypeError('Initialization settings are required');
527 }
528 if (typeof settings.currentTime !== 'function') {
529 throw new TypeError('No currentTime getter specified');
530 }
531 if (!settings.mediaSource) {
532 throw new TypeError('No MediaSource specified');
533 }
534 // public properties
535 this.bandwidth = settings.bandwidth;
536 this.throughput = {rate: 0, count: 0};
537 this.roundTrip = NaN;
538 this.resetStats_();
539 this.mediaIndex = null;
540 this.partIndex = null;
541
542 // private settings
543 this.hasPlayed_ = settings.hasPlayed;
544 this.currentTime_ = settings.currentTime;
545 this.seekable_ = settings.seekable;
546 this.seeking_ = settings.seeking;
547 this.duration_ = settings.duration;
548 this.mediaSource_ = settings.mediaSource;
549 this.vhs_ = settings.vhs;
550 this.loaderType_ = settings.loaderType;
551 this.currentMediaInfo_ = void 0;
552 this.startingMediaInfo_ = void 0;
553 this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
554 this.goalBufferLength_ = settings.goalBufferLength;
555 this.sourceType_ = settings.sourceType;
556 this.sourceUpdater_ = settings.sourceUpdater;
557 this.inbandTextTracks_ = settings.inbandTextTracks;
558 this.state_ = 'INIT';
559 this.timelineChangeController_ = settings.timelineChangeController;
560 this.shouldSaveSegmentTimingInfo_ = true;
561 this.parse708captions_ = settings.parse708captions;
562 this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset;
563 this.captionServices_ = settings.captionServices;
564 this.experimentalExactManifestTimings = settings.experimentalExactManifestTimings;
565
566 // private instance variables
567 this.checkBufferTimeout_ = null;
568 this.error_ = void 0;
569 this.currentTimeline_ = -1;
570 this.pendingSegment_ = null;
571 this.xhrOptions_ = null;
572 this.pendingSegments_ = [];
573 this.audioDisabled_ = false;
574 this.isPendingTimestampOffset_ = false;
575 // TODO possibly move gopBuffer and timeMapping info to a separate controller
576 this.gopBuffer_ = [];
577 this.timeMapping_ = 0;
578 this.safeAppend_ = videojs.browser.IE_VERSION >= 11;
579 this.appendInitSegment_ = {
580 audio: true,
581 video: true
582 };
583 this.playlistOfLastInitSegment_ = {
584 audio: null,
585 video: null
586 };
587 this.callQueue_ = [];
588 // If the segment loader prepares to load a segment, but does not have enough
589 // information yet to start the loading process (e.g., if the audio loader wants to
590 // load a segment from the next timeline but the main loader hasn't yet crossed that
591 // timeline), then the load call will be added to the queue until it is ready to be
592 // processed.
593 this.loadQueue_ = [];
594 this.metadataQueue_ = {
595 id3: [],
596 caption: []
597 };
598 this.waitingOnRemove_ = false;
599 this.quotaExceededErrorRetryTimeout_ = null;
600
601 // Fragmented mp4 playback
602 this.activeInitSegmentId_ = null;
603 this.initSegments_ = {};
604
605 // HLSe playback
606 this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys;
607 this.keyCache_ = {};
608
609 this.decrypter_ = settings.decrypter;
610
611 // Manages the tracking and generation of sync-points, mappings
612 // between a time in the display time and a segment index within
613 // a playlist
614 this.syncController_ = settings.syncController;
615 this.syncPoint_ = {
616 segmentIndex: 0,
617 time: 0
618 };
619
620 this.transmuxer_ = this.createTransmuxer_();
621 this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate');
622 this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_);
623
624 this.mediaSource_.addEventListener('sourceopen', () => {
625 if (!this.isEndOfStream_()) {
626 this.ended_ = false;
627 }
628 });
629
630 // ...for determining the fetch location
631 this.fetchAtBuffer_ = false;
632
633 this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`);
634
635 Object.defineProperty(this, 'state', {
636 get() {
637 return this.state_;
638 },
639 set(newState) {
640 if (newState !== this.state_) {
641 this.logger_(`${this.state_} -> ${newState}`);
642 this.state_ = newState;
643 this.trigger('statechange');
644 }
645 }
646 });
647
648 this.sourceUpdater_.on('ready', () => {
649 if (this.hasEnoughInfoToAppend_()) {
650 this.processCallQueue_();
651 }
652 });
653
654 // Only the main loader needs to listen for pending timeline changes, as the main
655 // loader should wait for audio to be ready to change its timeline so that both main
656 // and audio timelines change together. For more details, see the
657 // shouldWaitForTimelineChange function.
658 if (this.loaderType_ === 'main') {
659 this.timelineChangeController_.on('pendingtimelinechange', () => {
660 if (this.hasEnoughInfoToAppend_()) {
661 this.processCallQueue_();
662 }
663 });
664 }
665 // The main loader only listens on pending timeline changes, but the audio loader,
666 // since its loads follow main, needs to listen on timeline changes. For more details,
667 // see the shouldWaitForTimelineChange function.
668 if (this.loaderType_ === 'audio') {
669 this.timelineChangeController_.on('timelinechange', () => {
670 if (this.hasEnoughInfoToLoad_()) {
671 this.processLoadQueue_();
672 }
673 if (this.hasEnoughInfoToAppend_()) {
674 this.processCallQueue_();
675 }
676 });
677 }
678 }
679
680 createTransmuxer_() {
681 return segmentTransmuxer.createTransmuxer({
682 remux: false,
683 alignGopsAtEnd: this.safeAppend_,
684 keepOriginalTimestamps: true,
685 parse708captions: this.parse708captions_,
686 captionServices: this.captionServices_
687 });
688 }
689
690 /**
691 * reset all of our media stats
692 *
693 * @private
694 */
695 resetStats_() {
696 this.mediaBytesTransferred = 0;
697 this.mediaRequests = 0;
698 this.mediaRequestsAborted = 0;
699 this.mediaRequestsTimedout = 0;
700 this.mediaRequestsErrored = 0;
701 this.mediaTransferDuration = 0;
702 this.mediaSecondsLoaded = 0;
703 this.mediaAppends = 0;
704 }
705
706 /**
707 * dispose of the SegmentLoader and reset to the default state
708 */
709 dispose() {
710 this.trigger('dispose');
711 this.state = 'DISPOSED';
712 this.pause();
713 this.abort_();
714 if (this.transmuxer_) {
715 this.transmuxer_.terminate();
716 }
717 this.resetStats_();
718
719 if (this.checkBufferTimeout_) {
720 window.clearTimeout(this.checkBufferTimeout_);
721 }
722
723 if (this.syncController_ && this.triggerSyncInfoUpdate_) {
724 this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_);
725 }
726
727 this.off();
728 }
729
730 setAudio(enable) {
731 this.audioDisabled_ = !enable;
732 if (enable) {
733 this.appendInitSegment_.audio = true;
734 } else {
735 // remove current track audio if it gets disabled
736 this.sourceUpdater_.removeAudio(0, this.duration_());
737 }
738 }
739
740 /**
741 * abort anything that is currently doing on with the SegmentLoader
742 * and reset to a default state
743 */
744 abort() {
745 if (this.state !== 'WAITING') {
746 if (this.pendingSegment_) {
747 this.pendingSegment_ = null;
748 }
749 return;
750 }
751
752 this.abort_();
753
754 // We aborted the requests we were waiting on, so reset the loader's state to READY
755 // since we are no longer "waiting" on any requests. XHR callback is not always run
756 // when the request is aborted. This will prevent the loader from being stuck in the
757 // WAITING state indefinitely.
758 this.state = 'READY';
759
760 // don't wait for buffer check timeouts to begin fetching the
761 // next segment
762 if (!this.paused()) {
763 this.monitorBuffer_();
764 }
765 }
766
767 /**
768 * abort all pending xhr requests and null any pending segements
769 *
770 * @private
771 */
772 abort_() {
773 if (this.pendingSegment_ && this.pendingSegment_.abortRequests) {
774 this.pendingSegment_.abortRequests();
775 }
776
777 // clear out the segment being processed
778 this.pendingSegment_ = null;
779 this.callQueue_ = [];
780 this.loadQueue_ = [];
781 this.metadataQueue_.id3 = [];
782 this.metadataQueue_.caption = [];
783 this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_);
784 this.waitingOnRemove_ = false;
785 window.clearTimeout(this.quotaExceededErrorRetryTimeout_);
786 this.quotaExceededErrorRetryTimeout_ = null;
787 }
788
789 checkForAbort_(requestId) {
790 // If the state is APPENDING, then aborts will not modify the state, meaning the first
791 // callback that happens should reset the state to READY so that loading can continue.
792 if (this.state === 'APPENDING' && !this.pendingSegment_) {
793 this.state = 'READY';
794 return true;
795 }
796
797 if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) {
798 return true;
799 }
800
801 return false;
802 }
803
804 /**
805 * set an error on the segment loader and null out any pending segements
806 *
807 * @param {Error} error the error to set on the SegmentLoader
808 * @return {Error} the error that was set or that is currently set
809 */
810 error(error) {
811 if (typeof error !== 'undefined') {
812 this.logger_('error occurred:', error);
813 this.error_ = error;
814 }
815
816 this.pendingSegment_ = null;
817 return this.error_;
818 }
819
820 endOfStream() {
821 this.ended_ = true;
822 if (this.transmuxer_) {
823 // need to clear out any cached data to prepare for the new segment
824 segmentTransmuxer.reset(this.transmuxer_);
825 }
826 this.gopBuffer_.length = 0;
827 this.pause();
828 this.trigger('ended');
829 }
830
831 /**
832 * Indicates which time ranges are buffered
833 *
834 * @return {TimeRange}
835 * TimeRange object representing the current buffered ranges
836 */
837 buffered_() {
838 const trackInfo = this.getMediaInfo_();
839
840 if (!this.sourceUpdater_ || !trackInfo) {
841 return videojs.createTimeRanges();
842 }
843
844 if (this.loaderType_ === 'main') {
845 const { hasAudio, hasVideo, isMuxed } = trackInfo;
846
847 if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) {
848 return this.sourceUpdater_.buffered();
849 }
850
851 if (hasVideo) {
852 return this.sourceUpdater_.videoBuffered();
853 }
854 }
855
856 // One case that can be ignored for now is audio only with alt audio,
857 // as we don't yet have proper support for that.
858 return this.sourceUpdater_.audioBuffered();
859 }
860
861 /**
862 * Gets and sets init segment for the provided map
863 *
864 * @param {Object} map
865 * The map object representing the init segment to get or set
866 * @param {boolean=} set
867 * If true, the init segment for the provided map should be saved
868 * @return {Object}
869 * map object for desired init segment
870 */
871 initSegmentForMap(map, set = false) {
872 if (!map) {
873 return null;
874 }
875
876 const id = initSegmentId(map);
877 let storedMap = this.initSegments_[id];
878
879 if (set && !storedMap && map.bytes) {
880 this.initSegments_[id] = storedMap = {
881 resolvedUri: map.resolvedUri,
882 byterange: map.byterange,
883 bytes: map.bytes,
884 tracks: map.tracks,
885 timescales: map.timescales
886 };
887 }
888
889 return storedMap || map;
890 }
891
892 /**
893 * Gets and sets key for the provided key
894 *
895 * @param {Object} key
896 * The key object representing the key to get or set
897 * @param {boolean=} set
898 * If true, the key for the provided key should be saved
899 * @return {Object}
900 * Key object for desired key
901 */
902 segmentKey(key, set = false) {
903 if (!key) {
904 return null;
905 }
906
907 const id = segmentKeyId(key);
908 let storedKey = this.keyCache_[id];
909
910 // TODO: We should use the HTTP Expires header to invalidate our cache per
911 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3
912 if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) {
913 this.keyCache_[id] = storedKey = {
914 resolvedUri: key.resolvedUri,
915 bytes: key.bytes
916 };
917 }
918
919 const result = {
920 resolvedUri: (storedKey || key).resolvedUri
921 };
922
923 if (storedKey) {
924 result.bytes = storedKey.bytes;
925 }
926
927 return result;
928 }
929
930 /**
931 * Returns true if all configuration required for loading is present, otherwise false.
932 *
933 * @return {boolean} True if the all configuration is ready for loading
934 * @private
935 */
936 couldBeginLoading_() {
937 return this.playlist_ && !this.paused();
938 }
939
940 /**
941 * load a playlist and start to fill the buffer
942 */
943 load() {
944 // un-pause
945 this.monitorBuffer_();
946
947 // if we don't have a playlist yet, keep waiting for one to be
948 // specified
949 if (!this.playlist_) {
950 return;
951 }
952
953 // if all the configuration is ready, initialize and begin loading
954 if (this.state === 'INIT' && this.couldBeginLoading_()) {
955 return this.init_();
956 }
957
958 // if we're in the middle of processing a segment already, don't
959 // kick off an additional segment request
960 if (!this.couldBeginLoading_() ||
961 (this.state !== 'READY' &&
962 this.state !== 'INIT')) {
963 return;
964 }
965
966 this.state = 'READY';
967 }
968
969 /**
970 * Once all the starting parameters have been specified, begin
971 * operation. This method should only be invoked from the INIT
972 * state.
973 *
974 * @private
975 */
976 init_() {
977 this.state = 'READY';
978 // if this is the audio segment loader, and it hasn't been inited before, then any old
979 // audio data from the muxed content should be removed
980 this.resetEverything();
981 return this.monitorBuffer_();
982 }
983
984 /**
985 * set a playlist on the segment loader
986 *
987 * @param {PlaylistLoader} media the playlist to set on the segment loader
988 */
989 playlist(newPlaylist, options = {}) {
990 if (!newPlaylist) {
991 return;
992 }
993 const oldPlaylist = this.playlist_;
994 const segmentInfo = this.pendingSegment_;
995
996 this.playlist_ = newPlaylist;
997 this.xhrOptions_ = options;
998
999 // when we haven't started playing yet, the start of a live playlist
1000 // is always our zero-time so force a sync update each time the playlist
1001 // is refreshed from the server
1002 //
1003 // Use the INIT state to determine if playback has started, as the playlist sync info
1004 // should be fixed once requests begin (as sync points are generated based on sync
1005 // info), but not before then.
1006 if (this.state === 'INIT') {
1007 newPlaylist.syncInfo = {
1008 mediaSequence: newPlaylist.mediaSequence,
1009 time: 0
1010 };
1011 // Setting the date time mapping means mapping the program date time (if available)
1012 // to time 0 on the player's timeline. The playlist's syncInfo serves a similar
1013 // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can
1014 // be updated as the playlist is refreshed before the loader starts loading, the
1015 // program date time mapping needs to be updated as well.
1016 //
1017 // This mapping is only done for the main loader because a program date time should
1018 // map equivalently between playlists.
1019 if (this.loaderType_ === 'main') {
1020 this.syncController_.setDateTimeMappingForStart(newPlaylist);
1021 }
1022 }
1023
1024 let oldId = null;
1025
1026 if (oldPlaylist) {
1027 if (oldPlaylist.id) {
1028 oldId = oldPlaylist.id;
1029 } else if (oldPlaylist.uri) {
1030 oldId = oldPlaylist.uri;
1031 }
1032 }
1033
1034 this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`);
1035
1036 // in VOD, this is always a rendition switch (or we updated our syncInfo above)
1037 // in LIVE, we always want to update with new playlists (including refreshes)
1038 this.trigger('syncinfoupdate');
1039
1040 // if we were unpaused but waiting for a playlist, start
1041 // buffering now
1042 if (this.state === 'INIT' && this.couldBeginLoading_()) {
1043 return this.init_();
1044 }
1045
1046 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
1047 if (this.mediaIndex !== null) {
1048 // we must reset/resync the segment loader when we switch renditions and
1049 // the segment loader is already synced to the previous rendition
1050
1051 // on playlist changes we want it to be possible to fetch
1052 // at the buffer for vod but not for live. So we use resetLoader
1053 // for live and resyncLoader for vod. We want this because
1054 // if a playlist uses independent and non-independent segments/parts the
1055 // buffer may not accurately reflect the next segment that we should try
1056 // downloading.
1057 if (!newPlaylist.endList) {
1058 this.resetLoader();
1059 } else {
1060 this.resyncLoader();
1061 }
1062 }
1063 this.currentMediaInfo_ = void 0;
1064 this.trigger('playlistupdate');
1065
1066 // the rest of this function depends on `oldPlaylist` being defined
1067 return;
1068 }
1069
1070 // we reloaded the same playlist so we are in a live scenario
1071 // and we will likely need to adjust the mediaIndex
1072 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
1073
1074 this.logger_(`live window shift [${mediaSequenceDiff}]`);
1075
1076 // update the mediaIndex on the SegmentLoader
1077 // this is important because we can abort a request and this value must be
1078 // equal to the last appended mediaIndex
1079 if (this.mediaIndex !== null) {
1080 this.mediaIndex -= mediaSequenceDiff;
1081
1082 // this can happen if we are going to load the first segment, but get a playlist
1083 // update during that. mediaIndex would go from 0 to -1 if mediaSequence in the
1084 // new playlist was incremented by 1.
1085 if (this.mediaIndex < 0) {
1086 this.mediaIndex = null;
1087 this.partIndex = null;
1088 } else {
1089 const segment = this.playlist_.segments[this.mediaIndex];
1090
1091 // partIndex should remain the same for the same segment
1092 // unless parts fell off of the playlist for this segment.
1093 // In that case we need to reset partIndex and resync
1094 if (this.partIndex && (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex])) {
1095 const mediaIndex = this.mediaIndex;
1096
1097 this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`);
1098 this.resetLoader();
1099
1100 // We want to throw away the partIndex and the data associated with it,
1101 // as the part was dropped from our current playlists segment.
1102 // The mediaIndex will still be valid so keep that around.
1103 this.mediaIndex = mediaIndex;
1104 }
1105 }
1106 }
1107
1108 // update the mediaIndex on the SegmentInfo object
1109 // this is important because we will update this.mediaIndex with this value
1110 // in `handleAppendsDone_` after the segment has been successfully appended
1111 if (segmentInfo) {
1112 segmentInfo.mediaIndex -= mediaSequenceDiff;
1113
1114 if (segmentInfo.mediaIndex < 0) {
1115 segmentInfo.mediaIndex = null;
1116 segmentInfo.partIndex = null;
1117 } else {
1118 // we need to update the referenced segment so that timing information is
1119 // saved for the new playlist's segment, however, if the segment fell off the
1120 // playlist, we can leave the old reference and just lose the timing info
1121 if (segmentInfo.mediaIndex >= 0) {
1122 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
1123 }
1124
1125 if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) {
1126 segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex];
1127 }
1128 }
1129 }
1130
1131 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
1132 }
1133
1134 /**
1135 * Prevent the loader from fetching additional segments. If there
1136 * is a segment request outstanding, it will finish processing
1137 * before the loader halts. A segment loader can be unpaused by
1138 * calling load().
1139 */
1140 pause() {
1141 if (this.checkBufferTimeout_) {
1142 window.clearTimeout(this.checkBufferTimeout_);
1143
1144 this.checkBufferTimeout_ = null;
1145 }
1146 }
1147
1148 /**
1149 * Returns whether the segment loader is fetching additional
1150 * segments when given the opportunity. This property can be
1151 * modified through calls to pause() and load().
1152 */
1153 paused() {
1154 return this.checkBufferTimeout_ === null;
1155 }
1156
1157 /**
1158 * Delete all the buffered data and reset the SegmentLoader
1159 *
1160 * @param {Function} [done] an optional callback to be executed when the remove
1161 * operation is complete
1162 */
1163 resetEverything(done) {
1164 this.ended_ = false;
1165 this.appendInitSegment_ = {
1166 audio: true,
1167 video: true
1168 };
1169 this.resetLoader();
1170
1171 // remove from 0, the earliest point, to Infinity, to signify removal of everything.
1172 // VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader,
1173 // we then clamp the value to duration if necessary.
1174 this.remove(0, Infinity, done);
1175
1176 // clears fmp4 captions
1177 if (this.transmuxer_) {
1178 this.transmuxer_.postMessage({
1179 action: 'clearAllMp4Captions'
1180 });
1181
1182 // reset the cache in the transmuxer
1183 this.transmuxer_.postMessage({
1184 action: 'reset'
1185 });
1186 }
1187 }
1188
1189 /**
1190 * Force the SegmentLoader to resync and start loading around the currentTime instead
1191 * of starting at the end of the buffer
1192 *
1193 * Useful for fast quality changes
1194 */
1195 resetLoader() {
1196 this.fetchAtBuffer_ = false;
1197 this.resyncLoader();
1198 }
1199
1200 /**
1201 * Force the SegmentLoader to restart synchronization and make a conservative guess
1202 * before returning to the simple walk-forward method
1203 */
1204 resyncLoader() {
1205 if (this.transmuxer_) {
1206 // need to clear out any cached data to prepare for the new segment
1207 segmentTransmuxer.reset(this.transmuxer_);
1208 }
1209 this.mediaIndex = null;
1210 this.partIndex = null;
1211 this.syncPoint_ = null;
1212 this.isPendingTimestampOffset_ = false;
1213 this.callQueue_ = [];
1214 this.loadQueue_ = [];
1215 this.metadataQueue_.id3 = [];
1216 this.metadataQueue_.caption = [];
1217 this.abort();
1218
1219 if (this.transmuxer_) {
1220 this.transmuxer_.postMessage({
1221 action: 'clearParsedMp4Captions'
1222 });
1223 }
1224 }
1225
1226 /**
1227 * Remove any data in the source buffer between start and end times
1228 *
1229 * @param {number} start - the start time of the region to remove from the buffer
1230 * @param {number} end - the end time of the region to remove from the buffer
1231 * @param {Function} [done] - an optional callback to be executed when the remove
1232 * @param {boolean} force - force all remove operations to happen
1233 * operation is complete
1234 */
1235 remove(start, end, done = () => {}, force = false) {
1236 // clamp end to duration if we need to remove everything.
1237 // This is due to a browser bug that causes issues if we remove to Infinity.
1238 // videojs/videojs-contrib-hls#1225
1239 if (end === Infinity) {
1240 end = this.duration_();
1241 }
1242
1243 // skip removes that would throw an error
1244 // commonly happens during a rendition switch at the start of a video
1245 // from start 0 to end 0
1246 if (end <= start) {
1247 this.logger_('skipping remove because end ${end} is <= start ${start}');
1248 return;
1249 }
1250
1251 if (!this.sourceUpdater_ || !this.getMediaInfo_()) {
1252 this.logger_('skipping remove because no source updater or starting media info');
1253 // nothing to remove if we haven't processed any media
1254 return;
1255 }
1256
1257 // set it to one to complete this function's removes
1258 let removesRemaining = 1;
1259 const removeFinished = () => {
1260 removesRemaining--;
1261 if (removesRemaining === 0) {
1262 done();
1263 }
1264 };
1265
1266 if (force || !this.audioDisabled_) {
1267 removesRemaining++;
1268 this.sourceUpdater_.removeAudio(start, end, removeFinished);
1269 }
1270
1271 // While it would be better to only remove video if the main loader has video, this
1272 // should be safe with audio only as removeVideo will call back even if there's no
1273 // video buffer.
1274 //
1275 // In theory we can check to see if there's video before calling the remove, but in
1276 // the event that we're switching between renditions and from video to audio only
1277 // (when we add support for that), we may need to clear the video contents despite
1278 // what the new media will contain.
1279 if (force || this.loaderType_ === 'main') {
1280 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_);
1281 removesRemaining++;
1282 this.sourceUpdater_.removeVideo(start, end, removeFinished);
1283 }
1284
1285 // remove any captions and ID3 tags
1286 for (const track in this.inbandTextTracks_) {
1287 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]);
1288 }
1289
1290 removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
1291
1292 // finished this function's removes
1293 removeFinished();
1294 }
1295
1296 /**
1297 * (re-)schedule monitorBufferTick_ to run as soon as possible
1298 *
1299 * @private
1300 */
1301 monitorBuffer_() {
1302 if (this.checkBufferTimeout_) {
1303 window.clearTimeout(this.checkBufferTimeout_);
1304 }
1305
1306 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1);
1307 }
1308
1309 /**
1310 * As long as the SegmentLoader is in the READY state, periodically
1311 * invoke fillBuffer_().
1312 *
1313 * @private
1314 */
1315 monitorBufferTick_() {
1316 if (this.state === 'READY') {
1317 this.fillBuffer_();
1318 }
1319
1320 if (this.checkBufferTimeout_) {
1321 window.clearTimeout(this.checkBufferTimeout_);
1322 }
1323
1324 this.checkBufferTimeout_ = window.setTimeout(
1325 this.monitorBufferTick_.bind(this),
1326 CHECK_BUFFER_DELAY
1327 );
1328 }
1329
1330 /**
1331 * fill the buffer with segements unless the sourceBuffers are
1332 * currently updating
1333 *
1334 * Note: this function should only ever be called by monitorBuffer_
1335 * and never directly
1336 *
1337 * @private
1338 */
1339 fillBuffer_() {
1340 // TODO since the source buffer maintains a queue, and we shouldn't call this function
1341 // except when we're ready for the next segment, this check can most likely be removed
1342 if (this.sourceUpdater_.updating()) {
1343 return;
1344 }
1345
1346 // see if we need to begin loading immediately
1347 const segmentInfo = this.chooseNextRequest_();
1348
1349 if (!segmentInfo) {
1350 return;
1351 }
1352
1353 if (typeof segmentInfo.timestampOffset === 'number') {
1354 this.isPendingTimestampOffset_ = false;
1355 this.timelineChangeController_.pendingTimelineChange({
1356 type: this.loaderType_,
1357 from: this.currentTimeline_,
1358 to: segmentInfo.timeline
1359 });
1360 }
1361
1362 this.loadSegment_(segmentInfo);
1363 }
1364
1365 /**
1366 * Determines if we should call endOfStream on the media source based
1367 * on the state of the buffer or if appened segment was the final
1368 * segment in the playlist.
1369 *
1370 * @param {number} [mediaIndex] the media index of segment we last appended
1371 * @param {Object} [playlist] a media playlist object
1372 * @return {boolean} do we need to call endOfStream on the MediaSource
1373 */
1374 isEndOfStream_(mediaIndex = this.mediaIndex, playlist = this.playlist_, partIndex = this.partIndex) {
1375 if (!playlist || !this.mediaSource_) {
1376 return false;
1377 }
1378
1379 const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex];
1380
1381 // mediaIndex is zero based but length is 1 based
1382 const appendedLastSegment = (mediaIndex + 1) === playlist.segments.length;
1383 // true if there are no parts, or this is the last part.
1384 const appendedLastPart = !segment || !segment.parts || (partIndex + 1) === segment.parts.length;
1385
1386 // if we've buffered to the end of the video, we need to call endOfStream
1387 // so that MediaSources can trigger the `ended` event when it runs out of
1388 // buffered data instead of waiting for me
1389 return playlist.endList &&
1390 this.mediaSource_.readyState === 'open' &&
1391 appendedLastSegment &&
1392 appendedLastPart;
1393 }
1394
1395 /**
1396 * Determines what request should be made given current segment loader state.
1397 *
1398 * @return {Object} a request object that describes the segment/part to load
1399 */
1400 chooseNextRequest_() {
1401 const buffered = this.buffered_();
1402 const bufferedEnd = lastBufferedEnd(buffered) || 0;
1403 const bufferedTime = timeAheadOf(buffered, this.currentTime_());
1404 const preloaded = !this.hasPlayed_() && bufferedTime >= 1;
1405 const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_();
1406 const segments = this.playlist_.segments;
1407
1408 // return no segment if:
1409 // 1. we don't have segments
1410 // 2. The video has not yet played and we already downloaded a segment
1411 // 3. we already have enough buffered time
1412 if (!segments.length || preloaded || haveEnoughBuffer) {
1413 return null;
1414 }
1415
1416 this.syncPoint_ = this.syncPoint_ || this.syncController_.getSyncPoint(
1417 this.playlist_,
1418 this.duration_(),
1419 this.currentTimeline_,
1420 this.currentTime_()
1421 );
1422
1423 const next = {
1424 partIndex: null,
1425 mediaIndex: null,
1426 startOfSegment: null,
1427 playlist: this.playlist_,
1428 isSyncRequest: Boolean(!this.syncPoint_)
1429 };
1430
1431 if (next.isSyncRequest) {
1432 next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd);
1433 } else if (this.mediaIndex !== null) {
1434 const segment = segments[this.mediaIndex];
1435 const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1;
1436
1437 next.startOfSegment = segment.end ? segment.end : bufferedEnd;
1438
1439 if (segment.parts && segment.parts[partIndex + 1]) {
1440 next.mediaIndex = this.mediaIndex;
1441 next.partIndex = partIndex + 1;
1442 } else {
1443 next.mediaIndex = this.mediaIndex + 1;
1444 }
1445 } else {
1446 // Find the segment containing the end of the buffer or current time.
1447 const {segmentIndex, startTime, partIndex} = Playlist.getMediaInfoForTime({
1448 experimentalExactManifestTimings: this.experimentalExactManifestTimings,
1449 playlist: this.playlist_,
1450 currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(),
1451 startingPartIndex: this.syncPoint_.partIndex,
1452 startingSegmentIndex: this.syncPoint_.segmentIndex,
1453 startTime: this.syncPoint_.time
1454 });
1455
1456 next.getMediaInfoForTime = this.fetchAtBuffer_ ?
1457 `bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`;
1458 next.mediaIndex = segmentIndex;
1459 next.startOfSegment = startTime;
1460 next.partIndex = partIndex;
1461 }
1462
1463 const nextSegment = segments[next.mediaIndex];
1464 let nextPart = nextSegment &&
1465 typeof next.partIndex === 'number' &&
1466 nextSegment.parts &&
1467 nextSegment.parts[next.partIndex];
1468
1469 // if the next segment index is invalid or
1470 // the next partIndex is invalid do not choose a next segment.
1471 if (!nextSegment || (typeof next.partIndex === 'number' && !nextPart)) {
1472 return null;
1473 }
1474
1475 // if the next segment has parts, and we don't have a partIndex.
1476 // Set partIndex to 0
1477 if (typeof next.partIndex !== 'number' && nextSegment.parts) {
1478 next.partIndex = 0;
1479 nextPart = nextSegment.parts[0];
1480 }
1481
1482 // if we have no buffered data then we need to make sure
1483 // that the next part we append is "independent" if possible.
1484 // So we check if the previous part is independent, and request
1485 // it if it is.
1486 if (!bufferedTime && nextPart && !nextPart.independent) {
1487
1488 if (next.partIndex === 0) {
1489 const lastSegment = segments[next.mediaIndex - 1];
1490 const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1];
1491
1492 if (lastSegmentLastPart && lastSegmentLastPart.independent) {
1493 next.mediaIndex -= 1;
1494 next.partIndex = lastSegment.parts.length - 1;
1495 next.independent = 'previous segment';
1496 }
1497 } else if (nextSegment.parts[next.partIndex - 1].independent) {
1498 next.partIndex -= 1;
1499 next.independent = 'previous part';
1500 }
1501 }
1502
1503 const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended';
1504
1505 // do not choose a next segment if all of the following:
1506 // 1. this is the last segment in the playlist
1507 // 2. end of stream has been called on the media source already
1508 // 3. the player is not seeking
1509 if (next.mediaIndex >= (segments.length - 1) && ended && !this.seeking_()) {
1510 return null;
1511 }
1512
1513 return this.generateSegmentInfo_(next);
1514 }
1515
1516 generateSegmentInfo_(options) {
1517 const {
1518 independent,
1519 playlist,
1520 mediaIndex,
1521 startOfSegment,
1522 isSyncRequest,
1523 partIndex,
1524 forceTimestampOffset,
1525 getMediaInfoForTime
1526 } = options;
1527 const segment = playlist.segments[mediaIndex];
1528 const part = typeof partIndex === 'number' && segment.parts[partIndex];
1529 const segmentInfo = {
1530 requestId: 'segment-loader-' + Math.random(),
1531 // resolve the segment URL relative to the playlist
1532 uri: part && part.resolvedUri || segment.resolvedUri,
1533 // the segment's mediaIndex at the time it was requested
1534 mediaIndex,
1535 partIndex: part ? partIndex : null,
1536 // whether or not to update the SegmentLoader's state with this
1537 // segment's mediaIndex
1538 isSyncRequest,
1539 startOfSegment,
1540 // the segment's playlist
1541 playlist,
1542 // unencrypted bytes of the segment
1543 bytes: null,
1544 // when a key is defined for this segment, the encrypted bytes
1545 encryptedBytes: null,
1546 // The target timestampOffset for this segment when we append it
1547 // to the source buffer
1548 timestampOffset: null,
1549 // The timeline that the segment is in
1550 timeline: segment.timeline,
1551 // The expected duration of the segment in seconds
1552 duration: part && part.duration || segment.duration,
1553 // retain the segment in case the playlist updates while doing an async process
1554 segment,
1555 part,
1556 byteLength: 0,
1557 transmuxer: this.transmuxer_,
1558 // type of getMediaInfoForTime that was used to get this segment
1559 getMediaInfoForTime,
1560 independent
1561 };
1562
1563 const overrideCheck =
1564 typeof forceTimestampOffset !== 'undefined' ? forceTimestampOffset : this.isPendingTimestampOffset_;
1565
1566 segmentInfo.timestampOffset = this.timestampOffsetForSegment_({
1567 segmentTimeline: segment.timeline,
1568 currentTimeline: this.currentTimeline_,
1569 startOfSegment,
1570 buffered: this.buffered_(),
1571 overrideCheck
1572 });
1573
1574 const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered());
1575
1576 if (typeof audioBufferedEnd === 'number') {
1577 // since the transmuxer is using the actual timing values, but the buffer is
1578 // adjusted by the timestamp offset, we must adjust the value here
1579 segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset();
1580 }
1581
1582 if (this.sourceUpdater_.videoBuffered().length) {
1583 segmentInfo.gopsToAlignWith = gopsSafeToAlignWith(
1584 this.gopBuffer_,
1585 // since the transmuxer is using the actual timing values, but the time is
1586 // adjusted by the timestmap offset, we must adjust the value here
1587 this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(),
1588 this.timeMapping_
1589 );
1590 }
1591
1592 return segmentInfo;
1593 }
1594
1595 // get the timestampoffset for a segment,
1596 // added so that vtt segment loader can override and prevent
1597 // adding timestamp offsets.
1598 timestampOffsetForSegment_(options) {
1599 return timestampOffsetForSegment(options);
1600 }
1601 /**
1602 * Determines if the network has enough bandwidth to complete the current segment
1603 * request in a timely manner. If not, the request will be aborted early and bandwidth
1604 * updated to trigger a playlist switch.
1605 *
1606 * @param {Object} stats
1607 * Object containing stats about the request timing and size
1608 * @private
1609 */
1610 earlyAbortWhenNeeded_(stats) {
1611 if (this.vhs_.tech_.paused() ||
1612 // Don't abort if the current playlist is on the lowestEnabledRendition
1613 // TODO: Replace using timeout with a boolean indicating whether this playlist is
1614 // the lowestEnabledRendition.
1615 !this.xhrOptions_.timeout ||
1616 // Don't abort if we have no bandwidth information to estimate segment sizes
1617 !(this.playlist_.attributes.BANDWIDTH)) {
1618 return;
1619 }
1620
1621 // Wait at least 1 second since the first byte of data has been received before
1622 // using the calculated bandwidth from the progress event to allow the bitrate
1623 // to stabilize
1624 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
1625 return;
1626 }
1627
1628 const currentTime = this.currentTime_();
1629 const measuredBandwidth = stats.bandwidth;
1630 const segmentDuration = this.pendingSegment_.duration;
1631
1632 const requestTimeRemaining =
1633 Playlist.estimateSegmentRequestTime(
1634 segmentDuration,
1635 measuredBandwidth,
1636 this.playlist_,
1637 stats.bytesReceived
1638 );
1639
1640 // Subtract 1 from the timeUntilRebuffer so we still consider an early abort
1641 // if we are only left with less than 1 second when the request completes.
1642 // A negative timeUntilRebuffering indicates we are already rebuffering
1643 const timeUntilRebuffer = timeUntilRebuffer_(
1644 this.buffered_(),
1645 currentTime,
1646 this.vhs_.tech_.playbackRate()
1647 ) - 1;
1648
1649 // Only consider aborting early if the estimated time to finish the download
1650 // is larger than the estimated time until the player runs out of forward buffer
1651 if (requestTimeRemaining <= timeUntilRebuffer) {
1652 return;
1653 }
1654
1655 const switchCandidate = minRebufferMaxBandwidthSelector({
1656 master: this.vhs_.playlists.master,
1657 currentTime,
1658 bandwidth: measuredBandwidth,
1659 duration: this.duration_(),
1660 segmentDuration,
1661 timeUntilRebuffer,
1662 currentTimeline: this.currentTimeline_,
1663 syncController: this.syncController_
1664 });
1665
1666 if (!switchCandidate) {
1667 return;
1668 }
1669
1670 const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer;
1671
1672 const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;
1673
1674 let minimumTimeSaving = 0.5;
1675
1676 // If we are already rebuffering, increase the amount of variance we add to the
1677 // potential round trip time of the new request so that we are not too aggressive
1678 // with switching to a playlist that might save us a fraction of a second.
1679 if (timeUntilRebuffer <= TIME_FUDGE_FACTOR) {
1680 minimumTimeSaving = 1;
1681 }
1682
1683 if (!switchCandidate.playlist ||
1684 switchCandidate.playlist.uri === this.playlist_.uri ||
1685 timeSavedBySwitching < minimumTimeSaving) {
1686 return;
1687 }
1688
1689 // set the bandwidth to that of the desired playlist being sure to scale by
1690 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
1691 // don't trigger a bandwidthupdate as the bandwidth is artifial
1692 this.bandwidth =
1693 switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1;
1694 this.trigger('earlyabort');
1695 }
1696
1697 handleAbort_(segmentInfo) {
1698 this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`);
1699 this.mediaRequestsAborted += 1;
1700 }
1701
1702 /**
1703 * XHR `progress` event handler
1704 *
1705 * @param {Event}
1706 * The XHR `progress` event
1707 * @param {Object} simpleSegment
1708 * A simplified segment object copy
1709 * @private
1710 */
1711 handleProgress_(event, simpleSegment) {
1712 this.earlyAbortWhenNeeded_(simpleSegment.stats);
1713
1714 if (this.checkForAbort_(simpleSegment.requestId)) {
1715 return;
1716 }
1717
1718 this.trigger('progress');
1719 }
1720
1721 handleTrackInfo_(simpleSegment, trackInfo) {
1722 this.earlyAbortWhenNeeded_(simpleSegment.stats);
1723
1724 if (this.checkForAbort_(simpleSegment.requestId)) {
1725 return;
1726 }
1727
1728 if (this.checkForIllegalMediaSwitch(trackInfo)) {
1729 return;
1730 }
1731
1732 trackInfo = trackInfo || {};
1733
1734 // When we have track info, determine what media types this loader is dealing with.
1735 // Guard against cases where we're not getting track info at all until we are
1736 // certain that all streams will provide it.
1737 if (!shallowEqual(this.currentMediaInfo_, trackInfo)) {
1738 this.appendInitSegment_ = {
1739 audio: true,
1740 video: true
1741 };
1742
1743 this.startingMediaInfo_ = trackInfo;
1744 this.currentMediaInfo_ = trackInfo;
1745 this.logger_('trackinfo update', trackInfo);
1746 this.trigger('trackinfo');
1747 }
1748
1749 // trackinfo may cause an abort if the trackinfo
1750 // causes a codec change to an unsupported codec.
1751 if (this.checkForAbort_(simpleSegment.requestId)) {
1752 return;
1753 }
1754
1755 // set trackinfo on the pending segment so that
1756 // it can append.
1757 this.pendingSegment_.trackInfo = trackInfo;
1758
1759 // check if any calls were waiting on the track info
1760 if (this.hasEnoughInfoToAppend_()) {
1761 this.processCallQueue_();
1762 }
1763 }
1764
1765 handleTimingInfo_(simpleSegment, mediaType, timeType, time) {
1766 this.earlyAbortWhenNeeded_(simpleSegment.stats);
1767 if (this.checkForAbort_(simpleSegment.requestId)) {
1768 return;
1769 }
1770
1771 const segmentInfo = this.pendingSegment_;
1772 const timingInfoProperty = timingInfoPropertyForMedia(mediaType);
1773
1774 segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {};
1775 segmentInfo[timingInfoProperty][timeType] = time;
1776
1777 this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`);
1778
1779 // check if any calls were waiting on the timing info
1780 if (this.hasEnoughInfoToAppend_()) {
1781 this.processCallQueue_();
1782 }
1783 }
1784
1785 handleCaptions_(simpleSegment, captionData) {
1786 this.earlyAbortWhenNeeded_(simpleSegment.stats);
1787
1788 if (this.checkForAbort_(simpleSegment.requestId)) {
1789 return;
1790 }
1791
1792 // This could only happen with fmp4 segments, but
1793 // should still not happen in general
1794 if (captionData.length === 0) {
1795 this.logger_('SegmentLoader received no captions from a caption event');
1796 return;
1797 }
1798
1799 const segmentInfo = this.pendingSegment_;
1800
1801 // Wait until we have some video data so that caption timing
1802 // can be adjusted by the timestamp offset
1803 if (!segmentInfo.hasAppendedData_) {
1804 this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData));
1805 return;
1806 }
1807
1808 const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ?
1809 this.sourceUpdater_.audioTimestampOffset() :
1810 this.sourceUpdater_.videoTimestampOffset();
1811
1812 const captionTracks = {};
1813
1814 // get total start/end and captions for each track/stream
1815 captionData.forEach((caption) => {
1816 // caption.stream is actually a track name...
1817 // set to the existing values in tracks or default values
1818 captionTracks[caption.stream] = captionTracks[caption.stream] || {
1819 // Infinity, as any other value will be less than this
1820 startTime: Infinity,
1821 captions: [],
1822 // 0 as an other value will be more than this
1823 endTime: 0
1824 };
1825
1826 const captionTrack = captionTracks[caption.stream];
1827
1828 captionTrack.startTime = Math.min(captionTrack.startTime, (caption.startTime + timestampOffset));
1829 captionTrack.endTime = Math.max(captionTrack.endTime, (caption.endTime + timestampOffset));
1830 captionTrack.captions.push(caption);
1831 });
1832
1833 Object.keys(captionTracks).forEach((trackName) => {
1834 const {startTime, endTime, captions} = captionTracks[trackName];
1835 const inbandTextTracks = this.inbandTextTracks_;
1836
1837 this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`);
1838
1839 createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName);
1840 // clear out any cues that start and end at the same time period for the same track.
1841 // We do this because a rendition change that also changes the timescale for captions
1842 // will result in captions being re-parsed for certain segments. If we add them again
1843 // without clearing we will have two of the same captions visible.
1844 removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]);
1845
1846 addCaptionData({captionArray: captions, inbandTextTracks, timestampOffset});
1847 });
1848
1849 // Reset stored captions since we added parsed
1850 // captions to a text track at this point
1851
1852 if (this.transmuxer_) {
1853 this.transmuxer_.postMessage({
1854 action: 'clearParsedMp4Captions'
1855 });
1856 }
1857 }
1858
1859 handleId3_(simpleSegment, id3Frames, dispatchType) {
1860 this.earlyAbortWhenNeeded_(simpleSegment.stats);
1861
1862 if (this.checkForAbort_(simpleSegment.requestId)) {
1863 return;
1864 }
1865
1866 const segmentInfo = this.pendingSegment_;
1867
1868 // we need to have appended data in order for the timestamp offset to be set
1869 if (!segmentInfo.hasAppendedData_) {
1870 this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType));
1871 return;
1872 }
1873
1874 const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ?
1875 this.sourceUpdater_.audioTimestampOffset() :
1876 this.sourceUpdater_.videoTimestampOffset();
1877
1878 // There's potentially an issue where we could double add metadata if there's a muxed
1879 // audio/video source with a metadata track, and an alt audio with a metadata track.
1880 // However, this probably won't happen, and if it does it can be handled then.
1881 createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.vhs_.tech_);
1882 addMetadata({
1883 inbandTextTracks: this.inbandTextTracks_,
1884 metadataArray: id3Frames,
1885 timestampOffset,
1886 videoDuration: this.duration_()
1887 });
1888 }
1889
1890 processMetadataQueue_() {
1891 this.metadataQueue_.id3.forEach((fn) => fn());
1892 this.metadataQueue_.caption.forEach((fn) => fn());
1893
1894 this.metadataQueue_.id3 = [];
1895 this.metadataQueue_.caption = [];
1896 }
1897
1898 processCallQueue_() {
1899 const callQueue = this.callQueue_;
1900
1901 // Clear out the queue before the queued functions are run, since some of the
1902 // functions may check the length of the load queue and default to pushing themselves
1903 // back onto the queue.
1904 this.callQueue_ = [];
1905 callQueue.forEach((fun) => fun());
1906 }
1907
1908 processLoadQueue_() {
1909 const loadQueue = this.loadQueue_;
1910
1911 // Clear out the queue before the queued functions are run, since some of the
1912 // functions may check the length of the load queue and default to pushing themselves
1913 // back onto the queue.
1914 this.loadQueue_ = [];
1915 loadQueue.forEach((fun) => fun());
1916 }
1917
1918 /**
1919 * Determines whether the loader has enough info to load the next segment.
1920 *
1921 * @return {boolean}
1922 * Whether or not the loader has enough info to load the next segment
1923 */
1924 hasEnoughInfoToLoad_() {
1925 // Since primary timing goes by video, only the audio loader potentially needs to wait
1926 // to load.
1927 if (this.loaderType_ !== 'audio') {
1928 return true;
1929 }
1930
1931 const segmentInfo = this.pendingSegment_;
1932
1933 // A fill buffer must have already run to establish a pending segment before there's
1934 // enough info to load.
1935 if (!segmentInfo) {
1936 return false;
1937 }
1938
1939 // The first segment can and should be loaded immediately so that source buffers are
1940 // created together (before appending). Source buffer creation uses the presence of
1941 // audio and video data to determine whether to create audio/video source buffers, and
1942 // uses processed (transmuxed or parsed) media to determine the types required.
1943 if (!this.getCurrentMediaInfo_()) {
1944 return true;
1945 }
1946
1947 if (
1948 // Technically, instead of waiting to load a segment on timeline changes, a segment
1949 // can be requested and downloaded and only wait before it is transmuxed or parsed.
1950 // But in practice, there are a few reasons why it is better to wait until a loader
1951 // is ready to append that segment before requesting and downloading:
1952 //
1953 // 1. Because audio and main loaders cross discontinuities together, if this loader
1954 // is waiting for the other to catch up, then instead of requesting another
1955 // segment and using up more bandwidth, by not yet loading, more bandwidth is
1956 // allotted to the loader currently behind.
1957 // 2. media-segment-request doesn't have to have logic to consider whether a segment
1958 // is ready to be processed or not, isolating the queueing behavior to the loader.
1959 // 3. The audio loader bases some of its segment properties on timing information
1960 // provided by the main loader, meaning that, if the logic for waiting on
1961 // processing was in media-segment-request, then it would also need to know how
1962 // to re-generate the segment information after the main loader caught up.
1963 shouldWaitForTimelineChange({
1964 timelineChangeController: this.timelineChangeController_,
1965 currentTimeline: this.currentTimeline_,
1966 segmentTimeline: segmentInfo.timeline,
1967 loaderType: this.loaderType_,
1968 audioDisabled: this.audioDisabled_
1969 })
1970 ) {
1971 return false;
1972 }
1973
1974 return true;
1975 }
1976
1977 getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) {
1978 return segmentInfo && segmentInfo.trackInfo || this.currentMediaInfo_;
1979 }
1980
1981 getMediaInfo_(segmentInfo = this.pendingSegment_) {
1982 return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_;
1983 }
1984
1985 hasEnoughInfoToAppend_() {
1986 if (!this.sourceUpdater_.ready()) {
1987 return false;
1988 }
1989
1990 // If content needs to be removed or the loader is waiting on an append reattempt,
1991 // then no additional content should be appended until the prior append is resolved.
1992 if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) {
1993 return false;
1994 }
1995
1996 const segmentInfo = this.pendingSegment_;
1997 const trackInfo = this.getCurrentMediaInfo_();
1998
1999 // no segment to append any data for or
2000 // we do not have information on this specific
2001 // segment yet
2002 if (!segmentInfo || !trackInfo) {
2003 return false;
2004 }
2005
2006 const {hasAudio, hasVideo, isMuxed} = trackInfo;
2007
2008 if (hasVideo && !segmentInfo.videoTimingInfo) {
2009 return false;
2010 }
2011
2012 // muxed content only relies on video timing information for now.
2013 if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) {
2014 return false;
2015 }
2016
2017 if (
2018 shouldWaitForTimelineChange({
2019 timelineChangeController: this.timelineChangeController_,
2020 currentTimeline: this.currentTimeline_,
2021 segmentTimeline: segmentInfo.timeline,
2022 loaderType: this.loaderType_,
2023 audioDisabled: this.audioDisabled_
2024 })
2025 ) {
2026 return false;
2027 }
2028
2029 return true;
2030 }
2031
2032 handleData_(simpleSegment, result) {
2033 this.earlyAbortWhenNeeded_(simpleSegment.stats);
2034
2035 if (this.checkForAbort_(simpleSegment.requestId)) {
2036 return;
2037 }
2038
2039 // If there's anything in the call queue, then this data came later and should be
2040 // executed after the calls currently queued.
2041 if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) {
2042 this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result));
2043 return;
2044 }
2045
2046 const segmentInfo = this.pendingSegment_;
2047
2048 // update the time mapping so we can translate from display time to media time
2049 this.setTimeMapping_(segmentInfo.timeline);
2050
2051 // for tracking overall stats
2052 this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment);
2053
2054 // Note that the state isn't changed from loading to appending. This is because abort
2055 // logic may change behavior depending on the state, and changing state too early may
2056 // inflate our estimates of bandwidth. In the future this should be re-examined to
2057 // note more granular states.
2058
2059 // don't process and append data if the mediaSource is closed
2060 if (this.mediaSource_.readyState === 'closed') {
2061 return;
2062 }
2063
2064 // if this request included an initialization segment, save that data
2065 // to the initSegment cache
2066 if (simpleSegment.map) {
2067 simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true);
2068 // move over init segment properties to media request
2069 segmentInfo.segment.map = simpleSegment.map;
2070 }
2071
2072 // if this request included a segment key, save that data in the cache
2073 if (simpleSegment.key) {
2074 this.segmentKey(simpleSegment.key, true);
2075 }
2076
2077 segmentInfo.isFmp4 = simpleSegment.isFmp4;
2078 segmentInfo.timingInfo = segmentInfo.timingInfo || {};
2079
2080 if (segmentInfo.isFmp4) {
2081 this.trigger('fmp4');
2082
2083 segmentInfo.timingInfo.start =
2084 segmentInfo[timingInfoPropertyForMedia(result.type)].start;
2085 } else {
2086 const trackInfo = this.getCurrentMediaInfo_();
2087 const useVideoTimingInfo =
2088 this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo;
2089 let firstVideoFrameTimeForData;
2090
2091 if (useVideoTimingInfo) {
2092 firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start;
2093 }
2094
2095 // Segment loader knows more about segment timing than the transmuxer (in certain
2096 // aspects), so make any changes required for a more accurate start time.
2097 // Don't set the end time yet, as the segment may not be finished processing.
2098 segmentInfo.timingInfo.start = this.trueSegmentStart_({
2099 currentStart: segmentInfo.timingInfo.start,
2100 playlist: segmentInfo.playlist,
2101 mediaIndex: segmentInfo.mediaIndex,
2102 currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(),
2103 useVideoTimingInfo,
2104 firstVideoFrameTimeForData,
2105 videoTimingInfo: segmentInfo.videoTimingInfo,
2106 audioTimingInfo: segmentInfo.audioTimingInfo
2107 });
2108 }
2109
2110 // Init segments for audio and video only need to be appended in certain cases. Now
2111 // that data is about to be appended, we can check the final cases to determine
2112 // whether we should append an init segment.
2113 this.updateAppendInitSegmentStatus(segmentInfo, result.type);
2114 // Timestamp offset should be updated once we get new data and have its timing info,
2115 // as we use the start of the segment to offset the best guess (playlist provided)
2116 // timestamp offset.
2117 this.updateSourceBufferTimestampOffset_(segmentInfo);
2118
2119 // if this is a sync request we need to determine whether it should
2120 // be appended or not.
2121 if (segmentInfo.isSyncRequest) {
2122 // first save/update our timing info for this segment.
2123 // this is what allows us to choose an accurate segment
2124 // and the main reason we make a sync request.
2125 this.updateTimingInfoEnd_(segmentInfo);
2126 this.syncController_.saveSegmentTimingInfo({
2127 segmentInfo,
2128 shouldSaveTimelineMapping: this.loaderType_ === 'main'
2129 });
2130
2131 const next = this.chooseNextRequest_();
2132
2133 // If the sync request isn't the segment that would be requested next
2134 // after taking into account its timing info, do not append it.
2135 if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) {
2136 this.logger_('sync segment was incorrect, not appending');
2137 return;
2138 }
2139 // otherwise append it like any other segment as our guess was correct.
2140 this.logger_('sync segment was correct, appending');
2141 }
2142
2143 // Save some state so that in the future anything waiting on first append (and/or
2144 // timestamp offset(s)) can process immediately. While the extra state isn't optimal,
2145 // we need some notion of whether the timestamp offset or other relevant information
2146 // has had a chance to be set.
2147 segmentInfo.hasAppendedData_ = true;
2148 // Now that the timestamp offset should be set, we can append any waiting ID3 tags.
2149 this.processMetadataQueue_();
2150
2151 this.appendData_(segmentInfo, result);
2152 }
2153
2154 updateAppendInitSegmentStatus(segmentInfo, type) {
2155 // alt audio doesn't manage timestamp offset
2156 if (this.loaderType_ === 'main' &&
2157 typeof segmentInfo.timestampOffset === 'number' &&
2158 // in the case that we're handling partial data, we don't want to append an init
2159 // segment for each chunk
2160 !segmentInfo.changedTimestampOffset) {
2161 // if the timestamp offset changed, the timeline may have changed, so we have to re-
2162 // append init segments
2163 this.appendInitSegment_ = {
2164 audio: true,
2165 video: true
2166 };
2167 }
2168
2169 if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) {
2170 // make sure we append init segment on playlist changes, in case the media config
2171 // changed
2172 this.appendInitSegment_[type] = true;
2173 }
2174 }
2175
2176 getInitSegmentAndUpdateState_({ type, initSegment, map, playlist }) {
2177 // "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section
2178 // (Section 3) required to parse the applicable Media Segments. It applies to every
2179 // Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag
2180 // or until the end of the playlist."
2181 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5
2182 if (map) {
2183 const id = initSegmentId(map);
2184
2185 if (this.activeInitSegmentId_ === id) {
2186 // don't need to re-append the init segment if the ID matches
2187 return null;
2188 }
2189
2190 // a map-specified init segment takes priority over any transmuxed (or otherwise
2191 // obtained) init segment
2192 //
2193 // this also caches the init segment for later use
2194 initSegment = this.initSegmentForMap(map, true).bytes;
2195 this.activeInitSegmentId_ = id;
2196 }
2197
2198 // We used to always prepend init segments for video, however, that shouldn't be
2199 // necessary. Instead, we should only append on changes, similar to what we've always
2200 // done for audio. This is more important (though may not be that important) for
2201 // frame-by-frame appending for LHLS, simply because of the increased quantity of
2202 // appends.
2203 if (initSegment && this.appendInitSegment_[type]) {
2204 // Make sure we track the playlist that we last used for the init segment, so that
2205 // we can re-append the init segment in the event that we get data from a new
2206 // playlist. Discontinuities and track changes are handled in other sections.
2207 this.playlistOfLastInitSegment_[type] = playlist;
2208 // Disable future init segment appends for this type. Until a change is necessary.
2209 this.appendInitSegment_[type] = false;
2210
2211 // we need to clear out the fmp4 active init segment id, since
2212 // we are appending the muxer init segment
2213 this.activeInitSegmentId_ = null;
2214
2215 return initSegment;
2216 }
2217
2218 return null;
2219 }
2220
2221 handleQuotaExceededError_({segmentInfo, type, bytes}, error) {
2222 const audioBuffered = this.sourceUpdater_.audioBuffered();
2223 const videoBuffered = this.sourceUpdater_.videoBuffered();
2224
2225 // For now we're ignoring any notion of gaps in the buffer, but they, in theory,
2226 // should be cleared out during the buffer removals. However, log in case it helps
2227 // debug.
2228 if (audioBuffered.length > 1) {
2229 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' +
2230 timeRangesToArray(audioBuffered).join(', '));
2231 }
2232 if (videoBuffered.length > 1) {
2233 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' +
2234 timeRangesToArray(videoBuffered).join(', '));
2235 }
2236
2237 const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0;
2238 const audioBufferEnd = audioBuffered.length ?
2239 audioBuffered.end(audioBuffered.length - 1) : 0;
2240 const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0;
2241 const videoBufferEnd = videoBuffered.length ?
2242 videoBuffered.end(videoBuffered.length - 1) : 0;
2243
2244 if (
2245 (audioBufferEnd - audioBufferStart) <= MIN_BACK_BUFFER &&
2246 (videoBufferEnd - videoBufferStart) <= MIN_BACK_BUFFER
2247 ) {
2248 // Can't remove enough buffer to make room for new segment (or the browser doesn't
2249 // allow for appends of segments this size). In the future, it may be possible to
2250 // split up the segment and append in pieces, but for now, error out this playlist
2251 // in an attempt to switch to a more manageable rendition.
2252 this.logger_('On QUOTA_EXCEEDED_ERR, single segment too large to append to ' +
2253 'buffer, triggering an error. ' +
2254 `Appended byte length: ${bytes.byteLength}, ` +
2255 `audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` +
2256 `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `);
2257 this.error({
2258 message: 'Quota exceeded error with append of a single segment of content',
2259 excludeUntil: Infinity
2260 });
2261 this.trigger('error');
2262 return;
2263 }
2264
2265 // To try to resolve the quota exceeded error, clear back buffer and retry. This means
2266 // that the segment-loader should block on future events until this one is handled, so
2267 // that it doesn't keep moving onto further segments. Adding the call to the call
2268 // queue will prevent further appends until waitingOnRemove_ and
2269 // quotaExceededErrorRetryTimeout_ are cleared.
2270 //
2271 // Note that this will only block the current loader. In the case of demuxed content,
2272 // the other load may keep filling as fast as possible. In practice, this should be
2273 // OK, as it is a rare case when either audio has a high enough bitrate to fill up a
2274 // source buffer, or video fills without enough room for audio to append (and without
2275 // the availability of clearing out seconds of back buffer to make room for audio).
2276 // But it might still be good to handle this case in the future as a TODO.
2277 this.waitingOnRemove_ = true;
2278 this.callQueue_.push(this.appendToSourceBuffer_.bind(this, {segmentInfo, type, bytes}));
2279
2280 const currentTime = this.currentTime_();
2281 // Try to remove as much audio and video as possible to make room for new content
2282 // before retrying.
2283 const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER;
2284
2285 this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`);
2286 this.remove(0, timeToRemoveUntil, () => {
2287
2288 this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`);
2289 this.waitingOnRemove_ = false;
2290 // wait the length of time alotted in the back buffer to prevent wasted
2291 // attempts (since we can't clear less than the minimum)
2292 this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => {
2293 this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue');
2294 this.quotaExceededErrorRetryTimeout_ = null;
2295 this.processCallQueue_();
2296 }, MIN_BACK_BUFFER * 1000);
2297 }, true);
2298 }
2299
2300 handleAppendError_({segmentInfo, type, bytes}, error) {
2301 // if there's no error, nothing to do
2302 if (!error) {
2303 return;
2304 }
2305
2306 if (error.code === QUOTA_EXCEEDED_ERR) {
2307 this.handleQuotaExceededError_({segmentInfo, type, bytes});
2308 // A quota exceeded error should be recoverable with a future re-append, so no need
2309 // to trigger an append error.
2310 return;
2311 }
2312
2313 this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error);
2314 this.error(`${type} append of ${bytes.length}b failed for segment ` +
2315 `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`);
2316
2317 // If an append errors, we often can't recover.
2318 // (see https://w3c.github.io/media-source/#sourcebuffer-append-error).
2319 //
2320 // Trigger a special error so that it can be handled separately from normal,
2321 // recoverable errors.
2322 this.trigger('appenderror');
2323 }
2324
2325 appendToSourceBuffer_({ segmentInfo, type, initSegment, data, bytes }) {
2326 // If this is a re-append, bytes were already created and don't need to be recreated
2327 if (!bytes) {
2328 const segments = [data];
2329 let byteLength = data.byteLength;
2330
2331 if (initSegment) {
2332 // if the media initialization segment is changing, append it before the content
2333 // segment
2334 segments.unshift(initSegment);
2335 byteLength += initSegment.byteLength;
2336 }
2337
2338 // Technically we should be OK appending the init segment separately, however, we
2339 // haven't yet tested that, and prepending is how we have always done things.
2340 bytes = concatSegments({
2341 bytes: byteLength,
2342 segments
2343 });
2344 }
2345
2346 this.sourceUpdater_.appendBuffer(
2347 {segmentInfo, type, bytes},
2348 this.handleAppendError_.bind(this, {segmentInfo, type, bytes})
2349 );
2350 }
2351
2352 handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) {
2353 if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) {
2354 return;
2355 }
2356
2357 const segment = this.pendingSegment_.segment;
2358 const timingInfoProperty = `${type}TimingInfo`;
2359
2360 if (!segment[timingInfoProperty]) {
2361 segment[timingInfoProperty] = {};
2362 }
2363
2364 segment[timingInfoProperty].transmuxerPrependedSeconds =
2365 segmentTimingInfo.prependedContentDuration || 0;
2366 segment[timingInfoProperty].transmuxedPresentationStart =
2367 segmentTimingInfo.start.presentation;
2368 segment[timingInfoProperty].transmuxedDecodeStart =
2369 segmentTimingInfo.start.decode;
2370 segment[timingInfoProperty].transmuxedPresentationEnd =
2371 segmentTimingInfo.end.presentation;
2372 segment[timingInfoProperty].transmuxedDecodeEnd =
2373 segmentTimingInfo.end.decode;
2374 // mainly used as a reference for debugging
2375 segment[timingInfoProperty].baseMediaDecodeTime =
2376 segmentTimingInfo.baseMediaDecodeTime;
2377 }
2378
2379 appendData_(segmentInfo, result) {
2380 const {
2381 type,
2382 data
2383 } = result;
2384
2385 if (!data || !data.byteLength) {
2386 return;
2387 }
2388
2389 if (type === 'audio' && this.audioDisabled_) {
2390 return;
2391 }
2392
2393 const initSegment = this.getInitSegmentAndUpdateState_({
2394 type,
2395 initSegment: result.initSegment,
2396 playlist: segmentInfo.playlist,
2397 map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null
2398 });
2399
2400 this.appendToSourceBuffer_({ segmentInfo, type, initSegment, data });
2401 }
2402
2403 /**
2404 * load a specific segment from a request into the buffer
2405 *
2406 * @private
2407 */
2408 loadSegment_(segmentInfo) {
2409 this.state = 'WAITING';
2410 this.pendingSegment_ = segmentInfo;
2411 this.trimBackBuffer_(segmentInfo);
2412
2413 if (typeof segmentInfo.timestampOffset === 'number') {
2414 if (this.transmuxer_) {
2415 this.transmuxer_.postMessage({
2416 action: 'clearAllMp4Captions'
2417 });
2418 }
2419 }
2420
2421 if (!this.hasEnoughInfoToLoad_()) {
2422 this.loadQueue_.push(() => {
2423 // regenerate the audioAppendStart, timestampOffset, etc as they
2424 // may have changed since this function was added to the queue.
2425 const options = Object.assign(
2426 {},
2427 segmentInfo,
2428 {forceTimestampOffset: true}
2429 );
2430
2431 Object.assign(segmentInfo, this.generateSegmentInfo_(options));
2432 this.isPendingTimestampOffset_ = false;
2433 this.updateTransmuxerAndRequestSegment_(segmentInfo);
2434 });
2435 return;
2436 }
2437
2438 this.updateTransmuxerAndRequestSegment_(segmentInfo);
2439 }
2440
2441 updateTransmuxerAndRequestSegment_(segmentInfo) {
2442 // We'll update the source buffer's timestamp offset once we have transmuxed data, but
2443 // the transmuxer still needs to be updated before then.
2444 //
2445 // Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp
2446 // offset must be passed to the transmuxer for stream correcting adjustments.
2447 if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) {
2448 this.gopBuffer_.length = 0;
2449 // gopsToAlignWith was set before the GOP buffer was cleared
2450 segmentInfo.gopsToAlignWith = [];
2451 this.timeMapping_ = 0;
2452 // reset values in the transmuxer since a discontinuity should start fresh
2453 this.transmuxer_.postMessage({
2454 action: 'reset'
2455 });
2456 this.transmuxer_.postMessage({
2457 action: 'setTimestampOffset',
2458 timestampOffset: segmentInfo.timestampOffset
2459 });
2460 }
2461
2462 const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo);
2463 const isEndOfStream = this.isEndOfStream_(
2464 segmentInfo.mediaIndex,
2465 segmentInfo.playlist,
2466 segmentInfo.partIndex
2467 );
2468 const isWalkingForward = this.mediaIndex !== null;
2469 const isDiscontinuity = segmentInfo.timeline !== this.currentTimeline_ &&
2470 // currentTimeline starts at -1, so we shouldn't end the timeline switching to 0,
2471 // the first timeline
2472 segmentInfo.timeline > 0;
2473 const isEndOfTimeline = isEndOfStream || (isWalkingForward && isDiscontinuity);
2474
2475 this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`);
2476
2477 // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes),
2478 // then this init segment has never been seen before and should be appended.
2479 //
2480 // At this point the content type (audio/video or both) is not yet known, but it should be safe to set
2481 // both to true and leave the decision of whether to append the init segment to append time.
2482 if (simpleSegment.map && !simpleSegment.map.bytes) {
2483 this.logger_('going to request init segment.');
2484 this.appendInitSegment_ = {
2485 video: true,
2486 audio: true
2487 };
2488 }
2489
2490 segmentInfo.abortRequests = mediaSegmentRequest({
2491 xhr: this.vhs_.xhr,
2492 xhrOptions: this.xhrOptions_,
2493 decryptionWorker: this.decrypter_,
2494 segment: simpleSegment,
2495 abortFn: this.handleAbort_.bind(this, segmentInfo),
2496 progressFn: this.handleProgress_.bind(this),
2497 trackInfoFn: this.handleTrackInfo_.bind(this),
2498 timingInfoFn: this.handleTimingInfo_.bind(this),
2499 videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'video', segmentInfo.requestId),
2500 audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'audio', segmentInfo.requestId),
2501 captionsFn: this.handleCaptions_.bind(this),
2502 isEndOfTimeline,
2503 endedTimelineFn: () => {
2504 this.logger_('received endedtimeline callback');
2505 },
2506 id3Fn: this.handleId3_.bind(this),
2507
2508 dataFn: this.handleData_.bind(this),
2509 doneFn: this.segmentRequestFinished_.bind(this),
2510 onTransmuxerLog: ({message, level, stream}) => {
2511 this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`);
2512 }
2513 });
2514 }
2515
2516 /**
2517 * trim the back buffer so that we don't have too much data
2518 * in the source buffer
2519 *
2520 * @private
2521 *
2522 * @param {Object} segmentInfo - the current segment
2523 */
2524 trimBackBuffer_(segmentInfo) {
2525 const removeToTime = safeBackBufferTrimTime(
2526 this.seekable_(),
2527 this.currentTime_(),
2528 this.playlist_.targetDuration || 10
2529 );
2530
2531 // Chrome has a hard limit of 150MB of
2532 // buffer and a very conservative "garbage collector"
2533 // We manually clear out the old buffer to ensure
2534 // we don't trigger the QuotaExceeded error
2535 // on the source buffer during subsequent appends
2536
2537 if (removeToTime > 0) {
2538 this.remove(0, removeToTime);
2539 }
2540 }
2541
2542 /**
2543 * created a simplified copy of the segment object with just the
2544 * information necessary to perform the XHR and decryption
2545 *
2546 * @private
2547 *
2548 * @param {Object} segmentInfo - the current segment
2549 * @return {Object} a simplified segment object copy
2550 */
2551 createSimplifiedSegmentObj_(segmentInfo) {
2552 const segment = segmentInfo.segment;
2553 const part = segmentInfo.part;
2554
2555 const simpleSegment = {
2556 resolvedUri: part ? part.resolvedUri : segment.resolvedUri,
2557 byterange: part ? part.byterange : segment.byterange,
2558 requestId: segmentInfo.requestId,
2559 transmuxer: segmentInfo.transmuxer,
2560 audioAppendStart: segmentInfo.audioAppendStart,
2561 gopsToAlignWith: segmentInfo.gopsToAlignWith,
2562 part: segmentInfo.part
2563 };
2564
2565 const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1];
2566
2567 if (previousSegment && previousSegment.timeline === segment.timeline) {
2568 // The baseStartTime of a segment is used to handle rollover when probing the TS
2569 // segment to retrieve timing information. Since the probe only looks at the media's
2570 // times (e.g., PTS and DTS values of the segment), and doesn't consider the
2571 // player's time (e.g., player.currentTime()), baseStartTime should reflect the
2572 // media time as well. transmuxedDecodeEnd represents the end time of a segment, in
2573 // seconds of media time, so should be used here. The previous segment is used since
2574 // the end of the previous segment should represent the beginning of the current
2575 // segment, so long as they are on the same timeline.
2576 if (previousSegment.videoTimingInfo) {
2577 simpleSegment.baseStartTime =
2578 previousSegment.videoTimingInfo.transmuxedDecodeEnd;
2579 } else if (previousSegment.audioTimingInfo) {
2580 simpleSegment.baseStartTime =
2581 previousSegment.audioTimingInfo.transmuxedDecodeEnd;
2582 }
2583 }
2584
2585 if (segment.key) {
2586 // if the media sequence is greater than 2^32, the IV will be incorrect
2587 // assuming 10s segments, that would be about 1300 years
2588 const iv = segment.key.iv || new Uint32Array([
2589 0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence
2590 ]);
2591
2592 simpleSegment.key = this.segmentKey(segment.key);
2593 simpleSegment.key.iv = iv;
2594 }
2595
2596 if (segment.map) {
2597 simpleSegment.map = this.initSegmentForMap(segment.map);
2598 }
2599
2600 return simpleSegment;
2601 }
2602
2603 saveTransferStats_(stats) {
2604 // every request counts as a media request even if it has been aborted
2605 // or canceled due to a timeout
2606 this.mediaRequests += 1;
2607
2608 if (stats) {
2609 this.mediaBytesTransferred += stats.bytesReceived;
2610 this.mediaTransferDuration += stats.roundTripTime;
2611 }
2612 }
2613
2614 saveBandwidthRelatedStats_(duration, stats) {
2615 // byteLength will be used for throughput, and should be based on bytes receieved,
2616 // which we only know at the end of the request and should reflect total bytes
2617 // downloaded rather than just bytes processed from components of the segment
2618 this.pendingSegment_.byteLength = stats.bytesReceived;
2619
2620 if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) {
2621 this.logger_(`Ignoring segment's bandwidth because its duration of ${duration}` +
2622 ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`);
2623 return;
2624 }
2625
2626 this.bandwidth = stats.bandwidth;
2627 this.roundTrip = stats.roundTripTime;
2628 }
2629
2630 handleTimeout_() {
2631 // although the VTT segment loader bandwidth isn't really used, it's good to
2632 // maintain functinality between segment loaders
2633 this.mediaRequestsTimedout += 1;
2634 this.bandwidth = 1;
2635 this.roundTrip = NaN;
2636 this.trigger('bandwidthupdate');
2637 this.trigger('timeout');
2638 }
2639
2640 /**
2641 * Handle the callback from the segmentRequest function and set the
2642 * associated SegmentLoader state and errors if necessary
2643 *
2644 * @private
2645 */
2646 segmentRequestFinished_(error, simpleSegment, result) {
2647 // TODO handle special cases, e.g., muxed audio/video but only audio in the segment
2648
2649 // check the call queue directly since this function doesn't need to deal with any
2650 // data, and can continue even if the source buffers are not set up and we didn't get
2651 // any data from the segment
2652 if (this.callQueue_.length) {
2653 this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result));
2654 return;
2655 }
2656
2657 this.saveTransferStats_(simpleSegment.stats);
2658
2659 // The request was aborted and the SegmentLoader has already been reset
2660 if (!this.pendingSegment_) {
2661 return;
2662 }
2663
2664 // the request was aborted and the SegmentLoader has already started
2665 // another request. this can happen when the timeout for an aborted
2666 // request triggers due to a limitation in the XHR library
2667 // do not count this as any sort of request or we risk double-counting
2668 if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
2669 return;
2670 }
2671
2672 // an error occurred from the active pendingSegment_ so reset everything
2673 if (error) {
2674 this.pendingSegment_ = null;
2675 this.state = 'READY';
2676
2677 // aborts are not a true error condition and nothing corrective needs to be done
2678 if (error.code === REQUEST_ERRORS.ABORTED) {
2679 return;
2680 }
2681
2682 this.pause();
2683
2684 // the error is really just that at least one of the requests timed-out
2685 // set the bandwidth to a very low value and trigger an ABR switch to
2686 // take emergency action
2687 if (error.code === REQUEST_ERRORS.TIMEOUT) {
2688 this.handleTimeout_();
2689 return;
2690 }
2691
2692 // if control-flow has arrived here, then the error is real
2693 // emit an error event to blacklist the current playlist
2694 this.mediaRequestsErrored += 1;
2695 this.error(error);
2696 this.trigger('error');
2697 return;
2698 }
2699
2700 const segmentInfo = this.pendingSegment_;
2701
2702 // the response was a success so set any bandwidth stats the request
2703 // generated for ABR purposes
2704 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats);
2705
2706 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
2707
2708 if (result.gopInfo) {
2709 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_);
2710 }
2711
2712 // Although we may have already started appending on progress, we shouldn't switch the
2713 // state away from loading until we are officially done loading the segment data.
2714 this.state = 'APPENDING';
2715
2716 // used for testing
2717 this.trigger('appending');
2718
2719 this.waitForAppendsToComplete_(segmentInfo);
2720 }
2721
2722 setTimeMapping_(timeline) {
2723 const timelineMapping = this.syncController_.mappingForTimeline(timeline);
2724
2725 if (timelineMapping !== null) {
2726 this.timeMapping_ = timelineMapping;
2727 }
2728 }
2729
2730 updateMediaSecondsLoaded_(segment) {
2731 if (typeof segment.start === 'number' && typeof segment.end === 'number') {
2732 this.mediaSecondsLoaded += segment.end - segment.start;
2733 } else {
2734 this.mediaSecondsLoaded += segment.duration;
2735 }
2736 }
2737
2738 shouldUpdateTransmuxerTimestampOffset_(timestampOffset) {
2739 if (timestampOffset === null) {
2740 return false;
2741 }
2742
2743 // note that we're potentially using the same timestamp offset for both video and
2744 // audio
2745
2746 if (this.loaderType_ === 'main' &&
2747 timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) {
2748 return true;
2749 }
2750
2751 if (!this.audioDisabled_ &&
2752 timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) {
2753 return true;
2754 }
2755
2756 return false;
2757 }
2758
2759 trueSegmentStart_({
2760 currentStart,
2761 playlist,
2762 mediaIndex,
2763 firstVideoFrameTimeForData,
2764 currentVideoTimestampOffset,
2765 useVideoTimingInfo,
2766 videoTimingInfo,
2767 audioTimingInfo
2768 }) {
2769 if (typeof currentStart !== 'undefined') {
2770 // if start was set once, keep using it
2771 return currentStart;
2772 }
2773
2774 if (!useVideoTimingInfo) {
2775 return audioTimingInfo.start;
2776 }
2777
2778 const previousSegment = playlist.segments[mediaIndex - 1];
2779
2780 // The start of a segment should be the start of the first full frame contained
2781 // within that segment. Since the transmuxer maintains a cache of incomplete data
2782 // from and/or the last frame seen, the start time may reflect a frame that starts
2783 // in the previous segment. Check for that case and ensure the start time is
2784 // accurate for the segment.
2785 if (mediaIndex === 0 ||
2786 !previousSegment ||
2787 typeof previousSegment.start === 'undefined' ||
2788 previousSegment.end !==
2789 (firstVideoFrameTimeForData + currentVideoTimestampOffset)) {
2790 return firstVideoFrameTimeForData;
2791 }
2792
2793 return videoTimingInfo.start;
2794 }
2795
2796 waitForAppendsToComplete_(segmentInfo) {
2797 const trackInfo = this.getCurrentMediaInfo_(segmentInfo);
2798
2799 if (!trackInfo) {
2800 this.error({
2801 message: 'No starting media returned, likely due to an unsupported media format.',
2802 blacklistDuration: Infinity
2803 });
2804 this.trigger('error');
2805 return;
2806 }
2807 // Although transmuxing is done, appends may not yet be finished. Throw a marker
2808 // on each queue this loader is responsible for to ensure that the appends are
2809 // complete.
2810 const {hasAudio, hasVideo, isMuxed} = trackInfo;
2811 const waitForVideo = this.loaderType_ === 'main' && hasVideo;
2812 const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed;
2813
2814 segmentInfo.waitingOnAppends = 0;
2815
2816 // segments with no data
2817 if (!segmentInfo.hasAppendedData_) {
2818 if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') {
2819 // When there's no audio or video data in the segment, there's no audio or video
2820 // timing information.
2821 //
2822 // If there's no audio or video timing information, then the timestamp offset
2823 // can't be adjusted to the appropriate value for the transmuxer and source
2824 // buffers.
2825 //
2826 // Therefore, the next segment should be used to set the timestamp offset.
2827 this.isPendingTimestampOffset_ = true;
2828 }
2829
2830 // override settings for metadata only segments
2831 segmentInfo.timingInfo = {start: 0};
2832 segmentInfo.waitingOnAppends++;
2833
2834 if (!this.isPendingTimestampOffset_) {
2835 // update the timestampoffset
2836 this.updateSourceBufferTimestampOffset_(segmentInfo);
2837
2838 // make sure the metadata queue is processed even though we have
2839 // no video/audio data.
2840 this.processMetadataQueue_();
2841 }
2842
2843 // append is "done" instantly with no data.
2844 this.checkAppendsDone_(segmentInfo);
2845 return;
2846 }
2847
2848 // Since source updater could call back synchronously, do the increments first.
2849 if (waitForVideo) {
2850 segmentInfo.waitingOnAppends++;
2851 }
2852 if (waitForAudio) {
2853 segmentInfo.waitingOnAppends++;
2854 }
2855
2856 if (waitForVideo) {
2857 this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo));
2858 }
2859 if (waitForAudio) {
2860 this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo));
2861 }
2862 }
2863
2864 checkAppendsDone_(segmentInfo) {
2865 if (this.checkForAbort_(segmentInfo.requestId)) {
2866 return;
2867 }
2868
2869 segmentInfo.waitingOnAppends--;
2870
2871 if (segmentInfo.waitingOnAppends === 0) {
2872 this.handleAppendsDone_();
2873 }
2874 }
2875
2876 checkForIllegalMediaSwitch(trackInfo) {
2877 const illegalMediaSwitchError =
2878 illegalMediaSwitch(this.loaderType_, this.getCurrentMediaInfo_(), trackInfo);
2879
2880 if (illegalMediaSwitchError) {
2881 this.error({
2882 message: illegalMediaSwitchError,
2883 blacklistDuration: Infinity
2884 });
2885 this.trigger('error');
2886 return true;
2887 }
2888
2889 return false;
2890 }
2891
2892 updateSourceBufferTimestampOffset_(segmentInfo) {
2893 if (segmentInfo.timestampOffset === null ||
2894 // we don't yet have the start for whatever media type (video or audio) has
2895 // priority, timing-wise, so we must wait
2896 typeof segmentInfo.timingInfo.start !== 'number' ||
2897 // already updated the timestamp offset for this segment
2898 segmentInfo.changedTimestampOffset ||
2899 // the alt audio loader should not be responsible for setting the timestamp offset
2900 this.loaderType_ !== 'main') {
2901 return;
2902 }
2903
2904 let didChange = false;
2905
2906 // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that
2907 // the timing info here comes from video. In the event that the audio is longer than
2908 // the video, this will trim the start of the audio.
2909 // This also trims any offset from 0 at the beginning of the media
2910 segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({
2911 videoTimingInfo: segmentInfo.segment.videoTimingInfo,
2912 audioTimingInfo: segmentInfo.segment.audioTimingInfo,
2913 timingInfo: segmentInfo.timingInfo
2914 });
2915 // In the event that there are part segment downloads, each will try to update the
2916 // timestamp offset. Retaining this bit of state prevents us from updating in the
2917 // future (within the same segment), however, there may be a better way to handle it.
2918 segmentInfo.changedTimestampOffset = true;
2919
2920 if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) {
2921 this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset);
2922 didChange = true;
2923 }
2924
2925 if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) {
2926 this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset);
2927 didChange = true;
2928 }
2929
2930 if (didChange) {
2931 this.trigger('timestampoffset');
2932 }
2933 }
2934
2935 getSegmentStartTimeForTimestampOffsetCalculation_({ videoTimingInfo, audioTimingInfo, timingInfo }) {
2936 if (!this.useDtsForTimestampOffset_) {
2937 return timingInfo.start;
2938 }
2939
2940 if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') {
2941 return videoTimingInfo.transmuxedDecodeStart;
2942 }
2943
2944 // handle audio only
2945 if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') {
2946 return audioTimingInfo.transmuxedDecodeStart;
2947 }
2948
2949 // handle content not transmuxed (e.g., MP4)
2950 return timingInfo.start;
2951 }
2952
2953 updateTimingInfoEnd_(segmentInfo) {
2954 segmentInfo.timingInfo = segmentInfo.timingInfo || {};
2955 const trackInfo = this.getMediaInfo_();
2956 const useVideoTimingInfo =
2957 this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo;
2958 const prioritizedTimingInfo = useVideoTimingInfo && segmentInfo.videoTimingInfo ?
2959 segmentInfo.videoTimingInfo : segmentInfo.audioTimingInfo;
2960
2961 if (!prioritizedTimingInfo) {
2962 return;
2963 }
2964 segmentInfo.timingInfo.end = typeof prioritizedTimingInfo.end === 'number' ?
2965 // End time may not exist in a case where we aren't parsing the full segment (one
2966 // current example is the case of fmp4), so use the rough duration to calculate an
2967 // end time.
2968 prioritizedTimingInfo.end : prioritizedTimingInfo.start + segmentInfo.duration;
2969 }
2970
2971 /**
2972 * callback to run when appendBuffer is finished. detects if we are
2973 * in a good state to do things with the data we got, or if we need
2974 * to wait for more
2975 *
2976 * @private
2977 */
2978 handleAppendsDone_() {
2979 // appendsdone can cause an abort
2980 if (this.pendingSegment_) {
2981 this.trigger('appendsdone');
2982 }
2983
2984 if (!this.pendingSegment_) {
2985 this.state = 'READY';
2986 // TODO should this move into this.checkForAbort to speed up requests post abort in
2987 // all appending cases?
2988 if (!this.paused()) {
2989 this.monitorBuffer_();
2990 }
2991 return;
2992 }
2993
2994 const segmentInfo = this.pendingSegment_;
2995
2996 // Now that the end of the segment has been reached, we can set the end time. It's
2997 // best to wait until all appends are done so we're sure that the primary media is
2998 // finished (and we have its end time).
2999 this.updateTimingInfoEnd_(segmentInfo);
3000 if (this.shouldSaveSegmentTimingInfo_) {
3001 // Timeline mappings should only be saved for the main loader. This is for multiple
3002 // reasons:
3003 //
3004 // 1) Only one mapping is saved per timeline, meaning that if both the audio loader
3005 // and the main loader try to save the timeline mapping, whichever comes later
3006 // will overwrite the first. In theory this is OK, as the mappings should be the
3007 // same, however, it breaks for (2)
3008 // 2) In the event of a live stream, the initial live point will make for a somewhat
3009 // arbitrary mapping. If audio and video streams are not perfectly in-sync, then
3010 // the mapping will be off for one of the streams, dependent on which one was
3011 // first saved (see (1)).
3012 // 3) Primary timing goes by video in VHS, so the mapping should be video.
3013 //
3014 // Since the audio loader will wait for the main loader to load the first segment,
3015 // the main loader will save the first timeline mapping, and ensure that there won't
3016 // be a case where audio loads two segments without saving a mapping (thus leading
3017 // to missing segment timing info).
3018 this.syncController_.saveSegmentTimingInfo({
3019 segmentInfo,
3020 shouldSaveTimelineMapping: this.loaderType_ === 'main'
3021 });
3022 }
3023
3024 const segmentDurationMessage =
3025 getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_);
3026
3027 if (segmentDurationMessage) {
3028 if (segmentDurationMessage.severity === 'warn') {
3029 videojs.log.warn(segmentDurationMessage.message);
3030 } else {
3031 this.logger_(segmentDurationMessage.message);
3032 }
3033 }
3034
3035 this.recordThroughput_(segmentInfo);
3036 this.pendingSegment_ = null;
3037 this.state = 'READY';
3038
3039 if (segmentInfo.isSyncRequest) {
3040 this.trigger('syncinfoupdate');
3041 // if the sync request was not appended
3042 // then it was not the correct segment.
3043 // throw it away and use the data it gave us
3044 // to get the correct one.
3045 if (!segmentInfo.hasAppendedData_) {
3046 this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`);
3047 return;
3048 }
3049 }
3050
3051 this.logger_(`Appended ${segmentInfoString(segmentInfo)}`);
3052
3053 this.addSegmentMetadataCue_(segmentInfo);
3054 this.fetchAtBuffer_ = true;
3055 if (this.currentTimeline_ !== segmentInfo.timeline) {
3056 this.timelineChangeController_.lastTimelineChange({
3057 type: this.loaderType_,
3058 from: this.currentTimeline_,
3059 to: segmentInfo.timeline
3060 });
3061 // If audio is not disabled, the main segment loader is responsible for updating
3062 // the audio timeline as well. If the content is video only, this won't have any
3063 // impact.
3064 if (this.loaderType_ === 'main' && !this.audioDisabled_) {
3065 this.timelineChangeController_.lastTimelineChange({
3066 type: 'audio',
3067 from: this.currentTimeline_,
3068 to: segmentInfo.timeline
3069 });
3070 }
3071 }
3072 this.currentTimeline_ = segmentInfo.timeline;
3073
3074 // We must update the syncinfo to recalculate the seekable range before
3075 // the following conditional otherwise it may consider this a bad "guess"
3076 // and attempt to resync when the post-update seekable window and live
3077 // point would mean that this was the perfect segment to fetch
3078 this.trigger('syncinfoupdate');
3079 const segment = segmentInfo.segment;
3080 const part = segmentInfo.part;
3081 const badSegmentGuess = segment.end &&
3082 this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3;
3083 const badPartGuess = part &&
3084 part.end && this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3;
3085
3086 // If we previously appended a segment/part that ends more than 3 part/targetDurations before
3087 // the currentTime_ that means that our conservative guess was too conservative.
3088 // In that case, reset the loader state so that we try to use any information gained
3089 // from the previous request to create a new, more accurate, sync-point.
3090 if (badSegmentGuess || badPartGuess) {
3091 this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`);
3092 this.resetEverything();
3093 return;
3094 }
3095
3096 const isWalkingForward = this.mediaIndex !== null;
3097
3098 // Don't do a rendition switch unless we have enough time to get a sync segment
3099 // and conservatively guess
3100 if (isWalkingForward) {
3101 this.trigger('bandwidthupdate');
3102 }
3103 this.trigger('progress');
3104
3105 this.mediaIndex = segmentInfo.mediaIndex;
3106 this.partIndex = segmentInfo.partIndex;
3107
3108 // any time an update finishes and the last segment is in the
3109 // buffer, end the stream. this ensures the "ended" event will
3110 // fire if playback reaches that point.
3111 if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) {
3112 this.endOfStream();
3113 }
3114
3115 // used for testing
3116 this.trigger('appended');
3117
3118 if (segmentInfo.hasAppendedData_) {
3119 this.mediaAppends++;
3120 }
3121
3122 if (!this.paused()) {
3123 this.monitorBuffer_();
3124 }
3125 }
3126
3127 /**
3128 * Records the current throughput of the decrypt, transmux, and append
3129 * portion of the semgment pipeline. `throughput.rate` is a the cumulative
3130 * moving average of the throughput. `throughput.count` is the number of
3131 * data points in the average.
3132 *
3133 * @private
3134 * @param {Object} segmentInfo the object returned by loadSegment
3135 */
3136 recordThroughput_(segmentInfo) {
3137 if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) {
3138 this.logger_(`Ignoring segment's throughput because its duration of ${segmentInfo.duration}` +
3139 ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`);
3140 return;
3141 }
3142
3143 const rate = this.throughput.rate;
3144 // Add one to the time to ensure that we don't accidentally attempt to divide
3145 // by zero in the case where the throughput is ridiculously high
3146 const segmentProcessingTime =
3147 Date.now() - segmentInfo.endOfAllRequests + 1;
3148 // Multiply by 8000 to convert from bytes/millisecond to bits/second
3149 const segmentProcessingThroughput =
3150 Math.floor((segmentInfo.byteLength / segmentProcessingTime) * 8 * 1000);
3151
3152 // This is just a cumulative moving average calculation:
3153 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
3154 this.throughput.rate +=
3155 (segmentProcessingThroughput - rate) / (++this.throughput.count);
3156 }
3157
3158 /**
3159 * Adds a cue to the segment-metadata track with some metadata information about the
3160 * segment
3161 *
3162 * @private
3163 * @param {Object} segmentInfo
3164 * the object returned by loadSegment
3165 * @method addSegmentMetadataCue_
3166 */
3167 addSegmentMetadataCue_(segmentInfo) {
3168 if (!this.segmentMetadataTrack_) {
3169 return;
3170 }
3171
3172 const segment = segmentInfo.segment;
3173 const start = segment.start;
3174 const end = segment.end;
3175
3176 // Do not try adding the cue if the start and end times are invalid.
3177 if (!finite(start) || !finite(end)) {
3178 return;
3179 }
3180
3181 removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
3182
3183 const Cue = window.WebKitDataCue || window.VTTCue;
3184 const value = {
3185 custom: segment.custom,
3186 dateTimeObject: segment.dateTimeObject,
3187 dateTimeString: segment.dateTimeString,
3188 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH,
3189 resolution: segmentInfo.playlist.attributes.RESOLUTION,
3190 codecs: segmentInfo.playlist.attributes.CODECS,
3191 byteLength: segmentInfo.byteLength,
3192 uri: segmentInfo.uri,
3193 timeline: segmentInfo.timeline,
3194 playlist: segmentInfo.playlist.id,
3195 start,
3196 end
3197 };
3198 const data = JSON.stringify(value);
3199 const cue = new Cue(start, end, data);
3200
3201 // Attach the metadata to the value property of the cue to keep consistency between
3202 // the differences of WebKitDataCue in safari and VTTCue in other browsers
3203 cue.value = value;
3204
3205 this.segmentMetadataTrack_.addCue(cue);
3206 }
3207}