/**
* @file segment-loader.js
*/
import {getMediaInfoForTime_ as getMediaInfoForTime} from './playlist';
import videojs from 'video.js';
import SourceUpdater from './source-updater';
import {Decrypter} from 'aes-decrypter';
import Config from './config';
import window from 'global/window';
import SyncController from './sync-controller';
// in ms
const CHECK_BUFFER_DELAY = 500;
const c = 'console';
// temporary, switchable debug logging
const log = function() {
if (window.logit) {
window[c].log.apply(window[c], arguments);
}
};
/**
* Determines if we should call endOfStream on the media source based
* on the state of the buffer or if appened segment was the final
* segment in the playlist.
*
* @param {Object} playlist a media playlist object
* @param {Object} mediaSource the MediaSource object
* @param {Number} segmentIndex the index of segment we last appended
* @returns {Boolean} do we need to call endOfStream on the MediaSource
*/
const detectEndOfStream = function(playlist, mediaSource, segmentIndex) {
if (!playlist) {
return false;
}
let segments = playlist.segments;
// determine a few boolean values to help make the branch below easier
// to read
let appendedLastSegment = segmentIndex === segments.length;
// if we've buffered to the end of the video, we need to call endOfStream
// so that MediaSources can trigger the `ended` event when it runs out of
// buffered data instead of waiting for me
return playlist.endList &&
mediaSource.readyState === 'open' &&
appendedLastSegment;
};
/**
* Turns segment byterange into a string suitable for use in
* HTTP Range requests
*/
const byterangeStr = function(byterange) {
let byterangeStart;
let byterangeEnd;
// `byterangeEnd` is one less than `offset + length` because the HTTP range
// header uses inclusive ranges
byterangeEnd = byterange.offset + byterange.length - 1;
byterangeStart = byterange.offset;
return 'bytes=' + byterangeStart + '-' + byterangeEnd;
};
/**
* Defines headers for use in the xhr request for a particular segment.
*/
const segmentXhrHeaders = function(segment) {
let headers = {};
if ('byterange' in segment) {
headers.Range = byterangeStr(segment.byterange);
}
return headers;
};
/**
* Returns a unique string identifier for a media initialization
* segment.
*/
const initSegmentId = function(initSegment) {
let byterange = initSegment.byterange || {
length: Infinity,
offset: 0
};
return [
byterange.length, byterange.offset, initSegment.resolvedUri
].join(',');
};
/**
* An object that manages segment loading and appending.
*
* @class SegmentLoader
* @param {Object} options required and optional options
* @extends videojs.EventTarget
*/
export default class SegmentLoader extends videojs.EventTarget {
constructor(options) {
super();
let settings;
// check pre-conditions
if (!options) {
throw new TypeError('Initialization options are required');
}
if (typeof options.currentTime !== 'function') {
throw new TypeError('No currentTime getter specified');
}
if (!options.mediaSource) {
throw new TypeError('No MediaSource specified');
}
settings = videojs.mergeOptions(videojs.options.hls, options);
// public properties
this.state = 'INIT';
this.bandwidth = settings.bandwidth;
this.roundTrip = NaN;
this.resetStats_();
this.mediaIndex = null;
// private settings
this.hasPlayed_ = settings.hasPlayed;
this.currentTime_ = settings.currentTime;
this.seekable_ = settings.seekable;
this.seeking_ = settings.seeking;
this.setCurrentTime_ = settings.setCurrentTime;
this.mediaSource_ = settings.mediaSource;
this.hls_ = settings.hls;
// private instance variables
this.checkBufferTimeout_ = null;
this.error_ = void 0;
this.currentTimeline_ = -1;
this.xhr_ = null;
this.pendingSegment_ = null;
this.mimeType_ = null;
this.sourceUpdater_ = null;
this.xhrOptions_ = null;
// Fragmented mp4 playback
this.activeInitSegmentId_ = null;
this.initSegments_ = {};
// Manages the tracking and generation of sync-points, mappings
// between a time in the display time and a segment index within
// a playlist
this.syncController_ = new SyncController();
this.syncPoint_ = {
segmentIndex: 0,
time: 0
};
// ...for determining the fetch location
this.fetchAtBuffer_ = false;
}
/**
* reset all of our media stats
*
* @private
*/
resetStats_() {
this.mediaBytesTransferred = 0;
this.mediaRequests = 0;
this.mediaTransferDuration = 0;
this.mediaSecondsLoaded = 0;
}
/**
* dispose of the SegmentLoader and reset to the default state
*/
dispose() {
this.state = 'DISPOSED';
this.abort_();
if (this.sourceUpdater_) {
this.sourceUpdater_.dispose();
}
this.resetStats_();
}
/**
* abort anything that is currently doing on with the SegmentLoader
* and reset to a default state
*/
abort() {
if (this.state !== 'WAITING') {
return;
}
this.abort_();
// don't wait for buffer check timeouts to begin fetching the
// next segment
if (!this.paused()) {
this.state = 'READY';
this.fillBuffer_();
}
}
/**
* set an error on the segment loader and null out any pending segements
*
* @param {Error} error the error to set on the SegmentLoader
* @return {Error} the error that was set or that is currently set
*/
error(error) {
if (typeof error !== 'undefined') {
this.error_ = error;
}
this.pendingSegment_ = null;
return this.error_;
}
/**
* load a playlist and start to fill the buffer
*/
load() {
// un-pause
this.monitorBuffer_();
// if we don't have a playlist yet, keep waiting for one to be
// specified
if (!this.playlist_) {
return;
}
// not sure if this is the best place for this
this.syncController_.setDateTimeMapping(this.playlist_);
// if all the configuration is ready, initialize and begin loading
if (this.state === 'INIT' && this.mimeType_) {
return this.init_();
}
// if we're in the middle of processing a segment already, don't
// kick off an additional segment request
if (!this.sourceUpdater_ ||
(this.state !== 'READY' &&
this.state !== 'INIT')) {
return;
}
this.state = 'READY';
this.fillBuffer_();
}
/**
* set a playlist on the segment loader
*
* @param {PlaylistLoader} media the playlist to set on the segment loader
*/
playlist(newPlaylist, options = {}) {
if (!newPlaylist) {
return;
}
let oldPlaylist = this.playlist_;
let segmentInfo = this.pendingSegment_;
if (this.mediaIndex !== null) {
// We reloaded the same playlist so we are in a live scenario
// and we will likely need to adjust the mediaIndex
if (oldPlaylist &&
oldPlaylist.uri === newPlaylist.uri) {
let mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
this.mediaIndex -= mediaSequenceDiff;
if (segmentInfo && !segmentInfo.isSyncRequest) {
segmentInfo.mediaIndex -= mediaSequenceDiff;
}
this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
} else {
// We must "resync" the fetcher when we switch renditions
this.resyncLoader();
}
} else if (!this.hasPlayed_()) {
newPlaylist.syncInfo = {
mediaSequence: newPlaylist.mediaSequence,
time: 0
};
}
this.playlist_ = newPlaylist;
this.xhrOptions_ = options;
// if we were unpaused but waiting for a playlist, start
// buffering now
if (this.mimeType_ && this.state === 'INIT' && !this.paused()) {
return this.init_();
}
}
/**
* Prevent the loader from fetching additional segments. If there
* is a segment request outstanding, it will finish processing
* before the loader halts. A segment loader can be unpaused by
* calling load().
*/
pause() {
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
this.checkBufferTimeout_ = null;
}
}
/**
* Returns whether the segment loader is fetching additional
* segments when given the opportunity. This property can be
* modified through calls to pause() and load().
*/
paused() {
return this.checkBufferTimeout_ === null;
}
/**
* create/set the following mimetype on the SourceBuffer through a
* SourceUpdater
*
* @param {String} mimeType the mime type string to use
*/
mimeType(mimeType) {
if (this.mimeType_) {
return;
}
this.mimeType_ = mimeType;
// if we were unpaused but waiting for a sourceUpdater, start
// buffering now
if (this.playlist_ &&
this.state === 'INIT' &&
!this.paused()) {
this.init_();
}
}
/**
* As long as the SegmentLoader is in the READY state, periodically
* invoke fillBuffer_().
*
* @private
*/
monitorBuffer_() {
if (this.state === 'READY') {
this.fillBuffer_();
}
if (this.checkBufferTimeout_) {
window.clearTimeout(this.checkBufferTimeout_);
}
this.checkBufferTimeout_ = window.setTimeout(this.monitorBuffer_.bind(this),
CHECK_BUFFER_DELAY);
}
/**
* The segment loader has no recourse except to fetch a segment in the
* current playlist and use the internal timestamps in that segment to
* generate a syncPoint. This function returns a good candidate index
* for that process.
*
* @param {Object} playlist - the playlist object to look for a
* @returns {Number} An index of a segment from the playlist to load
*/
getSyncSegmentCandidate_(playlist) {
if (this.currentTimeline_ === -1) {
return 0;
}
let segmentIndexArray = playlist.segments
.map((s, i) => {
return {
timeline: s.timeline,
segmentIndex: i
};
}).filter(s => s.timeline === this.currentTimeline_);
if (segmentIndexArray.length) {
return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex;
}
return Math.max(playlist.segments.length - 1, 0);
}
/**
* Determines what segment request should be made, given current playback
* state.
*
* @param {TimeRanges} buffered - the state of the buffer
* @param {Object} playlist - the playlist object to fetch segments from
* @param {Number} mediaIndex - the previous mediaIndex fetched or null
* @param {Boolean} hasPlayed - a flag indicating whether we have played or not
* @param {Number} currentTime - the playback position in seconds
* @param {Object} syncPoint - a segment info object that describes the
* @returns {Object} a segment request object that describes the segment to load
*/
checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) {
let lastBufferedEnd = 0;
let startOfSegment;
if (buffered.length) {
lastBufferedEnd = buffered.end(buffered.length - 1);
}
let bufferedTime = Math.max(0, lastBufferedEnd - currentTime);
if (!playlist.segments.length) {
return null;
}
log('cB_', 'mediaIndex:', mediaIndex, 'hasPlayed:', hasPlayed, 'currentTime:', currentTime, 'syncPoint:', syncPoint, 'fetchAtBuffer:', this.fetchAtBuffer_);
log('cB_ 2', 'bufferedTime:', bufferedTime);
// if there is plenty of content buffered, and the video has
// been played before relax for awhile
if (bufferedTime >= Config.GOAL_BUFFER_LENGTH) {
return null;
}
// if the video has not yet played once, and we already have
// one segment downloaded do nothing
if (!hasPlayed && bufferedTime >= 1) {
return null;
}
// When the syncPoint is null, there is no way of determining a good
// conservative segment index to fetch from
// The best thing to do here is to get the kind of sync-point data by
// making a request
if (syncPoint === null) {
mediaIndex = this.getSyncSegmentCandidate_(playlist);
log('getSync', mediaIndex);
return this.generateSegmentInfo_(playlist, mediaIndex, null, true);
}
// Under normal playback conditions fetching is a simple walk forward
if (mediaIndex !== null) {
log('++', mediaIndex + 1);
let segment = playlist.segments[mediaIndex];
if (segment && segment.end) {
startOfSegment = segment.end;
} else {
startOfSegment = lastBufferedEnd;
}
return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false);
}
// There is a sync-point but the lack of a mediaIndex indicates that
// we need to make a good conservative guess about which segment to
// fetch
if (this.fetchAtBuffer_) {
// Find the segment containing the end of the buffer
let mediaSourceInfo = getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time);
mediaIndex = mediaSourceInfo.mediaIndex;
startOfSegment = mediaSourceInfo.startTime;
} else {
// Find the segment containing currentTime
let mediaSourceInfo = getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time);
mediaIndex = mediaSourceInfo.mediaIndex;
startOfSegment = mediaSourceInfo.startTime;
}
log('gMIFT', mediaIndex, 'sos', startOfSegment);
return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false);
}
generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) {
if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) {
return null;
}
let segment = playlist.segments[mediaIndex];
return {
// resolve the segment URL relative to the playlist
uri: segment.resolvedUri,
// the segment's mediaIndex at the time it was requested
mediaIndex,
// whether or not to update the SegmentLoader's state with this
// segment's mediaIndex
isSyncRequest,
startOfSegment,
// the segment's playlist
playlist,
// unencrypted bytes of the segment
bytes: null,
// when a key is defined for this segment, the encrypted bytes
encryptedBytes: null,
// The target timestampOffset for this segment when we append it
// to the source buffer
timestampOffset: null,
// The timeline that the segment is in
timeline: segment.timeline,
// The expected duration of the segment in seconds
duration: segment.duration
};
}
/**
* abort all pending xhr requests and null any pending segements
*
* @private
*/
abort_() {
if (this.xhr_) {
this.xhr_.abort();
}
// clear out the segment being processed
this.pendingSegment_ = null;
}
/**
* Once all the starting parameters have been specified, begin
* operation. This method should only be invoked from the INIT
* state.
*
* @private
*/
init_() {
this.state = 'READY';
this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_);
this.resetEverything();
return this.fillBuffer_();
}
/**
* fill the buffer with segements unless the
* sourceBuffers are currently updating
*
* @private
*/
fillBuffer_() {
if (this.sourceUpdater_.updating()) {
return;
}
if (!this.syncPoint_) {
this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_,
this.mediaSource_.duration,
this.currentTimeline_);
}
// see if we need to begin loading immediately
let segmentInfo = this.checkBuffer_(this.sourceUpdater_.buffered(),
this.playlist_,
this.mediaIndex,
this.hasPlayed_(),
this.currentTime_(),
this.syncPoint_);
if (!segmentInfo) {
return;
}
let isEndOfStream = detectEndOfStream(this.playlist_,
this.mediaSource_,
segmentInfo.mediaIndex);
if (isEndOfStream) {
this.mediaSource_.endOfStream();
return;
}
if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 &&
this.mediaSource_.readyState === 'ended' &&
!this.seeking_()) {
return;
}
// We will need to change timestampOffset of the sourceBuffer if either of
// the following conditions are true:
// - The segment.timeline !== this.currentTimeline
// (we are crossing a discontinuity somehow)
// - The "timestampOffset" for the start of this segment is less than
// the currently set timestampOffset
if (segmentInfo.timeline !== this.currentTimeline_ ||
((segmentInfo.startOfSegment !== null) &&
segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset())) {
this.syncController_.reset();
segmentInfo.timestampOffset = segmentInfo.startOfSegment;
}
this.currentTimeline_ = segmentInfo.timeline;
this.loadSegment_(segmentInfo);
}
/**
* trim the back buffer so we only remove content
* on segment boundaries
*
* @private
*
* @param {Object} segmentInfo - the current segment
* @returns {Number} removeToTime - the end point in time, in seconds
* that the the buffer should be trimmed.
*/
trimBuffer_(segmentInfo) {
let seekable = this.seekable_();
let currentTime = this.currentTime_();
let removeToTime = 0;
// Chrome has a hard limit of 150mb of
// buffer and a very conservative "garbage collector"
// We manually clear out the old buffer to ensure
// we don't trigger the QuotaExceeded error
// on the source buffer during subsequent appends
// If we have a seekable range use that as the limit for what can be removed safely
// otherwise remove anything older than 1 minute before the current play head
if (seekable.length &&
seekable.start(0) > 0 &&
seekable.start(0) < currentTime) {
removeToTime = seekable.start(0);
} else {
removeToTime = currentTime - 60;
}
return removeToTime;
}
/**
* load a specific segment from a request into the buffer
*
* @private
*/
loadSegment_(segmentInfo) {
let segment;
let keyXhr;
let initSegmentXhr;
let segmentXhr;
let removeToTime = 0;
removeToTime = this.trimBuffer_(segmentInfo);
if (removeToTime > 0) {
this.sourceUpdater_.remove(0, removeToTime);
}
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
// optionally, request the decryption key
if (segment.key) {
let keyRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
uri: segment.key.resolvedUri,
responseType: 'arraybuffer'
});
keyXhr = this.hls_.xhr(keyRequestOptions, this.handleResponse_.bind(this));
}
// optionally, request the associated media init segment
if (segment.map &&
!this.initSegments_[initSegmentId(segment.map)]) {
let initSegmentOptions = videojs.mergeOptions(this.xhrOptions_, {
uri: segment.map.resolvedUri,
responseType: 'arraybuffer',
headers: segmentXhrHeaders(segment.map)
});
initSegmentXhr = this.hls_.xhr(initSegmentOptions,
this.handleResponse_.bind(this));
}
this.pendingSegment_ = segmentInfo;
let segmentRequestOptions = videojs.mergeOptions(this.xhrOptions_, {
uri: segmentInfo.uri,
responseType: 'arraybuffer',
headers: segmentXhrHeaders(segment)
});
segmentXhr = this.hls_.xhr(segmentRequestOptions, this.handleResponse_.bind(this));
this.xhr_ = {
keyXhr,
initSegmentXhr,
segmentXhr,
abort() {
if (this.segmentXhr) {
// Prevent error handler from running.
this.segmentXhr.onreadystatechange = null;
this.segmentXhr.abort();
this.segmentXhr = null;
}
if (this.initSegmentXhr) {
// Prevent error handler from running.
this.initSegmentXhr.onreadystatechange = null;
this.initSegmentXhr.abort();
this.initSegmentXhr = null;
}
if (this.keyXhr) {
// Prevent error handler from running.
this.keyXhr.onreadystatechange = null;
this.keyXhr.abort();
this.keyXhr = null;
}
}
};
this.state = 'WAITING';
}
/**
* triggered when a segment response is received
*
* @private
*/
handleResponse_(error, request) {
let segmentInfo;
let segment;
let keyXhrRequest;
let view;
// timeout of previously aborted request
if (!this.xhr_ ||
(request !== this.xhr_.segmentXhr &&
request !== this.xhr_.keyXhr &&
request !== this.xhr_.initSegmentXhr)) {
return;
}
segmentInfo = this.pendingSegment_;
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
// if a request times out, reset bandwidth tracking
if (request.timedout) {
this.abort_();
this.bandwidth = 1;
this.roundTrip = NaN;
this.state = 'READY';
return this.trigger('progress');
}
// trigger an event for other errors
if (!request.aborted && error) {
// abort will clear xhr_
keyXhrRequest = this.xhr_.keyXhr;
this.abort_();
this.error({
status: request.status,
message: request === keyXhrRequest ?
'HLS key request error at URL: ' + segment.key.uri :
'HLS segment request error at URL: ' + segmentInfo.uri,
code: 2,
xhr: request
});
this.state = 'READY';
this.pause();
return this.trigger('error');
}
// stop processing if the request was aborted
if (!request.response) {
this.abort_();
return;
}
if (request === this.xhr_.segmentXhr) {
// the segment request is no longer outstanding
this.xhr_.segmentXhr = null;
// calculate the download bandwidth based on segment request
this.roundTrip = request.roundTripTime;
this.bandwidth = request.bandwidth;
this.mediaBytesTransferred += request.bytesReceived || 0;
this.mediaRequests += 1;
this.mediaTransferDuration += request.roundTripTime || 0;
if (segment.key) {
segmentInfo.encryptedBytes = new Uint8Array(request.response);
} else {
segmentInfo.bytes = new Uint8Array(request.response);
}
}
if (request === this.xhr_.keyXhr) {
keyXhrRequest = this.xhr_.segmentXhr;
// the key request is no longer outstanding
this.xhr_.keyXhr = null;
if (request.response.byteLength !== 16) {
this.abort_();
this.error({
status: request.status,
message: 'Invalid HLS key at URL: ' + segment.key.uri,
code: 2,
xhr: request
});
this.state = 'READY';
this.pause();
return this.trigger('error');
}
view = new DataView(request.response);
segment.key.bytes = new Uint32Array([
view.getUint32(0),
view.getUint32(4),
view.getUint32(8),
view.getUint32(12)
]);
// if the media sequence is greater than 2^32, the IV will be incorrect
// assuming 10s segments, that would be about 1300 years
segment.key.iv = segment.key.iv || new Uint32Array([
0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence
]);
}
if (request === this.xhr_.initSegmentXhr) {
// the init segment request is no longer outstanding
this.xhr_.initSegmentXhr = null;
segment.map.bytes = new Uint8Array(request.response);
this.initSegments_[initSegmentId(segment.map)] = segment.map;
}
if (!this.xhr_.segmentXhr && !this.xhr_.keyXhr && !this.xhr_.initSegmentXhr) {
this.xhr_ = null;
this.processResponse_();
}
}
/**
* Delete all the buffered data and reset the SegmentLoader
*/
resetEverything() {
this.resetLoader();
this.remove(0, Infinity);
}
/**
* Force the SegmentLoader to resync and start loading around the currentTime instead
* of starting at the end of the buffer
*
* Useful for fast quality changes
*/
resetLoader() {
this.fetchAtBuffer_ = false;
this.resyncLoader();
}
/**
* Force the SegmentLoader to restart synchronization and make a conservative guess
* before returning to the simple walk-forward method
*/
resyncLoader() {
this.mediaIndex = null;
this.syncPoint_ = null;
}
/**
* Remove any data in the source buffer between start and end times
* @param {Number} start - the start time of the region to remove from the buffer
* @param {Number} end - the end time of the region to remove from the buffer
*/
remove(start, end) {
if (this.sourceUpdater_) {
this.sourceUpdater_.remove(start, end);
}
}
/**
* Decrypt the segment that is being loaded if necessary
*
* @private
*/
processResponse_() {
let segmentInfo;
let segment;
this.state = 'DECRYPTING';
segmentInfo = this.pendingSegment_;
segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
if (segment.key) {
// this is an encrypted segment
// incrementally decrypt the segment
/* eslint-disable no-new, handle-callback-err */
new Decrypter(segmentInfo.encryptedBytes,
segment.key.bytes,
segment.key.iv,
(function(err, bytes) {
// err always null
segmentInfo.bytes = bytes;
this.handleSegment_();
}).bind(this));
/* eslint-enable */
} else {
this.handleSegment_();
}
}
/**
* append a decrypted segement to the SourceBuffer through a SourceUpdater
*
* @private
*/
handleSegment_() {
this.state = 'APPENDING';
let segmentInfo = this.pendingSegment_;
let segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
this.syncController_.probeSegmentInfo(segmentInfo);
if (segmentInfo.isSyncRequest) {
this.pendingSegment_ = null;
this.state = 'READY';
return;
}
if (segmentInfo.timestampOffset !== null &&
segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
}
// if the media initialization segment is changing, append it
// before the content segment
if (segment.map) {
let initId = initSegmentId(segment.map);
if (!this.activeInitSegmentId_ ||
this.activeInitSegmentId_ !== initId) {
let initSegment = this.initSegments_[initId];
this.sourceUpdater_.appendBuffer(initSegment.bytes, () => {
this.activeInitSegmentId_ = initId;
});
}
}
if (typeof segment.start === 'number' && typeof segment.end === 'number') {
this.mediaSecondsLoaded += segment.end - segment.start;
} else {
this.mediaSecondsLoaded += segment.duration;
}
this.sourceUpdater_.appendBuffer(segmentInfo.bytes,
this.handleUpdateEnd_.bind(this));
}
/**
* callback to run when appendBuffer is finished. detects if we are
* in a good state to do things with the data we got, or if we need
* to wait for more
*
* @private
*/
handleUpdateEnd_() {
let segmentInfo = this.pendingSegment_;
log('handleUpdateEnd_');
if (!segmentInfo.isSyncRequest) {
this.mediaIndex = segmentInfo.mediaIndex;
this.fetchAtBuffer_ = true;
}
this.pendingSegment_ = null;
let currentMediaIndex = segmentInfo.mediaIndex;
currentMediaIndex +=
segmentInfo.playlist.mediaSequence - this.playlist_.mediaSequence;
// any time an update finishes and the last segment is in the
// buffer, end the stream. this ensures the "ended" event will
// fire if playback reaches that point.
let isEndOfStream = detectEndOfStream(segmentInfo.playlist,
this.mediaSource_,
currentMediaIndex + 1);
if (isEndOfStream) {
this.mediaSource_.endOfStream();
}
this.state = 'READY';
this.trigger('progress');
if (!this.paused()) {
this.fillBuffer_();
}
}
}