You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1403 lines
48 KiB
1403 lines
48 KiB
/**
|
|
* @file segment-loader.js
|
|
*/
|
|
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', {
|
|
value: true
|
|
});
|
|
|
|
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
|
|
|
|
var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
|
|
|
|
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
|
|
|
|
var _playlist = require('./playlist');
|
|
|
|
var _playlist2 = _interopRequireDefault(_playlist);
|
|
|
|
var _videoJs = require('video.js');
|
|
|
|
var _videoJs2 = _interopRequireDefault(_videoJs);
|
|
|
|
var _sourceUpdater = require('./source-updater');
|
|
|
|
var _sourceUpdater2 = _interopRequireDefault(_sourceUpdater);
|
|
|
|
var _config = require('./config');
|
|
|
|
var _config2 = _interopRequireDefault(_config);
|
|
|
|
var _globalWindow = require('global/window');
|
|
|
|
var _globalWindow2 = _interopRequireDefault(_globalWindow);
|
|
|
|
var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs = require('videojs-contrib-media-sources/es5/remove-cues-from-track.js');
|
|
|
|
var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2 = _interopRequireDefault(_videojsContribMediaSourcesEs5RemoveCuesFromTrackJs);
|
|
|
|
var _binUtils = require('./bin-utils');
|
|
|
|
var _mediaSegmentRequest = require('./media-segment-request');
|
|
|
|
var _ranges = require('./ranges');
|
|
|
|
var _playlistSelectors = require('./playlist-selectors');
|
|
|
|
// in ms
|
|
var CHECK_BUFFER_DELAY = 500;
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) {
|
|
if (!playlist || !mediaSource) {
|
|
return false;
|
|
}
|
|
|
|
var segments = playlist.segments;
|
|
|
|
// determine a few boolean values to help make the branch below easier
|
|
// to read
|
|
var 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;
|
|
};
|
|
|
|
var finite = function finite(num) {
|
|
return typeof num === 'number' && isFinite(num);
|
|
};
|
|
|
|
var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) {
|
|
// Although these checks should most likely cover non 'main' types, for now it narrows
|
|
// the scope of our checks.
|
|
if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) {
|
|
return null;
|
|
}
|
|
|
|
if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) {
|
|
return 'Neither audio nor video found in segment.';
|
|
}
|
|
|
|
if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) {
|
|
return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.';
|
|
}
|
|
|
|
if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) {
|
|
return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.';
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
exports.illegalMediaSwitch = illegalMediaSwitch;
|
|
/**
|
|
* Calculates a time value that is safe to remove from the back buffer without interupting
|
|
* playback.
|
|
*
|
|
* @param {TimeRange} seekable
|
|
* The current seekable range
|
|
* @param {Number} currentTime
|
|
* The current time of the player
|
|
* @param {Number} targetDuration
|
|
* The target duration of the current playlist
|
|
* @return {Number}
|
|
* Time that is safe to remove from the back buffer without interupting playback
|
|
*/
|
|
var safeBackBufferTrimTime = function safeBackBufferTrimTime(seekable, currentTime, targetDuration) {
|
|
var removeToTime = undefined;
|
|
|
|
if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
|
|
// If we have a seekable range use that as the limit for what can be removed safely
|
|
removeToTime = seekable.start(0);
|
|
} else {
|
|
// otherwise remove anything older than 30 seconds before the current play head
|
|
removeToTime = currentTime - 30;
|
|
}
|
|
|
|
// Don't allow removing from the buffer within target duration of current time
|
|
// to avoid the possibility of removing the GOP currently being played which could
|
|
// cause playback stalls.
|
|
return Math.min(removeToTime, currentTime - targetDuration);
|
|
};
|
|
|
|
exports.safeBackBufferTrimTime = safeBackBufferTrimTime;
|
|
/**
|
|
* An object that manages segment loading and appending.
|
|
*
|
|
* @class SegmentLoader
|
|
* @param {Object} options required and optional options
|
|
* @extends videojs.EventTarget
|
|
*/
|
|
|
|
var SegmentLoader = (function (_videojs$EventTarget) {
|
|
_inherits(SegmentLoader, _videojs$EventTarget);
|
|
|
|
function SegmentLoader(settings) {
|
|
var _this = this;
|
|
|
|
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
|
|
|
|
_classCallCheck(this, SegmentLoader);
|
|
|
|
_get(Object.getPrototypeOf(SegmentLoader.prototype), 'constructor', this).call(this);
|
|
// check pre-conditions
|
|
if (!settings) {
|
|
throw new TypeError('Initialization settings are required');
|
|
}
|
|
if (typeof settings.currentTime !== 'function') {
|
|
throw new TypeError('No currentTime getter specified');
|
|
}
|
|
if (!settings.mediaSource) {
|
|
throw new TypeError('No MediaSource specified');
|
|
}
|
|
// public properties
|
|
this.state = 'INIT';
|
|
this.bandwidth = settings.bandwidth;
|
|
this.throughput = { rate: 0, count: 0 };
|
|
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.duration_ = settings.duration;
|
|
this.mediaSource_ = settings.mediaSource;
|
|
this.hls_ = settings.hls;
|
|
this.loaderType_ = settings.loaderType;
|
|
this.startingMedia_ = void 0;
|
|
this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
|
|
this.goalBufferLength_ = settings.goalBufferLength;
|
|
|
|
// private instance variables
|
|
this.checkBufferTimeout_ = null;
|
|
this.error_ = void 0;
|
|
this.currentTimeline_ = -1;
|
|
this.pendingSegment_ = null;
|
|
this.mimeType_ = null;
|
|
this.sourceUpdater_ = null;
|
|
this.xhrOptions_ = null;
|
|
|
|
// Fragmented mp4 playback
|
|
this.activeInitSegmentId_ = null;
|
|
this.initSegments_ = {};
|
|
|
|
this.decrypter_ = settings.decrypter;
|
|
|
|
// 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_ = settings.syncController;
|
|
this.syncPoint_ = {
|
|
segmentIndex: 0,
|
|
time: 0
|
|
};
|
|
|
|
this.syncController_.on('syncinfoupdate', function () {
|
|
return _this.trigger('syncinfoupdate');
|
|
});
|
|
|
|
this.mediaSource_.addEventListener('sourceopen', function () {
|
|
return _this.ended_ = false;
|
|
});
|
|
|
|
// ...for determining the fetch location
|
|
this.fetchAtBuffer_ = false;
|
|
|
|
if (options.debug) {
|
|
this.logger_ = _videoJs2['default'].log.bind(_videoJs2['default'], 'segment-loader', this.loaderType_, '->');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* reset all of our media stats
|
|
*
|
|
* @private
|
|
*/
|
|
|
|
_createClass(SegmentLoader, [{
|
|
key: 'resetStats_',
|
|
value: function resetStats_() {
|
|
this.mediaBytesTransferred = 0;
|
|
this.mediaRequests = 0;
|
|
this.mediaRequestsAborted = 0;
|
|
this.mediaRequestsTimedout = 0;
|
|
this.mediaRequestsErrored = 0;
|
|
this.mediaTransferDuration = 0;
|
|
this.mediaSecondsLoaded = 0;
|
|
}
|
|
|
|
/**
|
|
* dispose of the SegmentLoader and reset to the default state
|
|
*/
|
|
}, {
|
|
key: 'dispose',
|
|
value: function dispose() {
|
|
this.state = 'DISPOSED';
|
|
this.pause();
|
|
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
|
|
*/
|
|
}, {
|
|
key: 'abort',
|
|
value: function abort() {
|
|
if (this.state !== 'WAITING') {
|
|
if (this.pendingSegment_) {
|
|
this.pendingSegment_ = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.abort_();
|
|
|
|
// We aborted the requests we were waiting on, so reset the loader's state to READY
|
|
// since we are no longer "waiting" on any requests. XHR callback is not always run
|
|
// when the request is aborted. This will prevent the loader from being stuck in the
|
|
// WAITING state indefinitely.
|
|
this.state = 'READY';
|
|
|
|
// don't wait for buffer check timeouts to begin fetching the
|
|
// next segment
|
|
if (!this.paused()) {
|
|
this.monitorBuffer_();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* abort all pending xhr requests and null any pending segements
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'abort_',
|
|
value: function abort_() {
|
|
if (this.pendingSegment_) {
|
|
this.pendingSegment_.abortRequests();
|
|
}
|
|
|
|
// clear out the segment being processed
|
|
this.pendingSegment_ = null;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
}, {
|
|
key: 'error',
|
|
value: function error(_error) {
|
|
if (typeof _error !== 'undefined') {
|
|
this.error_ = _error;
|
|
}
|
|
|
|
this.pendingSegment_ = null;
|
|
return this.error_;
|
|
}
|
|
}, {
|
|
key: 'endOfStream',
|
|
value: function endOfStream() {
|
|
this.ended_ = true;
|
|
this.pause();
|
|
this.trigger('ended');
|
|
}
|
|
|
|
/**
|
|
* Indicates which time ranges are buffered
|
|
*
|
|
* @return {TimeRange}
|
|
* TimeRange object representing the current buffered ranges
|
|
*/
|
|
}, {
|
|
key: 'buffered_',
|
|
value: function buffered_() {
|
|
if (!this.sourceUpdater_) {
|
|
return _videoJs2['default'].createTimeRanges();
|
|
}
|
|
|
|
return this.sourceUpdater_.buffered();
|
|
}
|
|
|
|
/**
|
|
* Gets and sets init segment for the provided map
|
|
*
|
|
* @param {Object} map
|
|
* The map object representing the init segment to get or set
|
|
* @param {Boolean=} set
|
|
* If true, the init segment for the provided map should be saved
|
|
* @return {Object}
|
|
* map object for desired init segment
|
|
*/
|
|
}, {
|
|
key: 'initSegment',
|
|
value: function initSegment(map) {
|
|
var set = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];
|
|
|
|
if (!map) {
|
|
return null;
|
|
}
|
|
|
|
var id = (0, _binUtils.initSegmentId)(map);
|
|
var storedMap = this.initSegments_[id];
|
|
|
|
if (set && !storedMap && map.bytes) {
|
|
this.initSegments_[id] = storedMap = {
|
|
resolvedUri: map.resolvedUri,
|
|
byterange: map.byterange,
|
|
bytes: map.bytes
|
|
};
|
|
}
|
|
|
|
return storedMap || map;
|
|
}
|
|
|
|
/**
|
|
* Returns true if all configuration required for loading is present, otherwise false.
|
|
*
|
|
* @return {Boolean} True if the all configuration is ready for loading
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'couldBeginLoading_',
|
|
value: function couldBeginLoading_() {
|
|
return this.playlist_ && (
|
|
// the source updater is created when init_ is called, so either having a
|
|
// source updater or being in the INIT state with a mimeType is enough
|
|
// to say we have all the needed configuration to start loading.
|
|
this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused();
|
|
}
|
|
|
|
/**
|
|
* load a playlist and start to fill the buffer
|
|
*/
|
|
}, {
|
|
key: 'load',
|
|
value: function 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.couldBeginLoading_()) {
|
|
return this.init_();
|
|
}
|
|
|
|
// if we're in the middle of processing a segment already, don't
|
|
// kick off an additional segment request
|
|
if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') {
|
|
return;
|
|
}
|
|
|
|
this.state = 'READY';
|
|
}
|
|
|
|
/**
|
|
* Once all the starting parameters have been specified, begin
|
|
* operation. This method should only be invoked from the INIT
|
|
* state.
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'init_',
|
|
value: function init_() {
|
|
this.state = 'READY';
|
|
this.sourceUpdater_ = new _sourceUpdater2['default'](this.mediaSource_, this.mimeType_);
|
|
this.resetEverything();
|
|
return this.monitorBuffer_();
|
|
}
|
|
|
|
/**
|
|
* set a playlist on the segment loader
|
|
*
|
|
* @param {PlaylistLoader} media the playlist to set on the segment loader
|
|
*/
|
|
}, {
|
|
key: 'playlist',
|
|
value: function playlist(newPlaylist) {
|
|
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
|
|
|
|
if (!newPlaylist) {
|
|
return;
|
|
}
|
|
|
|
var oldPlaylist = this.playlist_;
|
|
var segmentInfo = this.pendingSegment_;
|
|
|
|
this.playlist_ = newPlaylist;
|
|
this.xhrOptions_ = options;
|
|
|
|
// when we haven't started playing yet, the start of a live playlist
|
|
// is always our zero-time so force a sync update each time the playlist
|
|
// is refreshed from the server
|
|
if (!this.hasPlayed_()) {
|
|
newPlaylist.syncInfo = {
|
|
mediaSequence: newPlaylist.mediaSequence,
|
|
time: 0
|
|
};
|
|
}
|
|
|
|
// in VOD, this is always a rendition switch (or we updated our syncInfo above)
|
|
// in LIVE, we always want to update with new playlists (including refreshes)
|
|
this.trigger('syncinfoupdate');
|
|
|
|
// if we were unpaused but waiting for a playlist, start
|
|
// buffering now
|
|
if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
|
return this.init_();
|
|
}
|
|
|
|
if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
|
|
if (this.mediaIndex !== null) {
|
|
// we must "resync" the segment loader when we switch renditions and
|
|
// the segment loader is already synced to the previous rendition
|
|
this.resyncLoader();
|
|
}
|
|
|
|
// the rest of this function depends on `oldPlaylist` being defined
|
|
return;
|
|
}
|
|
|
|
// we reloaded the same playlist so we are in a live scenario
|
|
// and we will likely need to adjust the mediaIndex
|
|
var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
|
|
|
|
this.logger_('mediaSequenceDiff', mediaSequenceDiff);
|
|
|
|
// update the mediaIndex on the SegmentLoader
|
|
// this is important because we can abort a request and this value must be
|
|
// equal to the last appended mediaIndex
|
|
if (this.mediaIndex !== null) {
|
|
this.mediaIndex -= mediaSequenceDiff;
|
|
}
|
|
|
|
// update the mediaIndex on the SegmentInfo object
|
|
// this is important because we will update this.mediaIndex with this value
|
|
// in `handleUpdateEnd_` after the segment has been successfully appended
|
|
if (segmentInfo) {
|
|
segmentInfo.mediaIndex -= mediaSequenceDiff;
|
|
|
|
// we need to update the referenced segment so that timing information is
|
|
// saved for the new playlist's segment, however, if the segment fell off the
|
|
// playlist, we can leave the old reference and just lose the timing info
|
|
if (segmentInfo.mediaIndex >= 0) {
|
|
segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
|
|
}
|
|
}
|
|
|
|
this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
|
|
}
|
|
|
|
/**
|
|
* 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().
|
|
*/
|
|
}, {
|
|
key: 'pause',
|
|
value: function pause() {
|
|
if (this.checkBufferTimeout_) {
|
|
_globalWindow2['default'].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().
|
|
*/
|
|
}, {
|
|
key: 'paused',
|
|
value: function 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
|
|
*/
|
|
}, {
|
|
key: 'mimeType',
|
|
value: function mimeType(_mimeType) {
|
|
if (this.mimeType_) {
|
|
return;
|
|
}
|
|
|
|
this.mimeType_ = _mimeType;
|
|
// if we were unpaused but waiting for a sourceUpdater, start
|
|
// buffering now
|
|
if (this.state === 'INIT' && this.couldBeginLoading_()) {
|
|
this.init_();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete all the buffered data and reset the SegmentLoader
|
|
*/
|
|
}, {
|
|
key: 'resetEverything',
|
|
value: function resetEverything() {
|
|
this.ended_ = false;
|
|
this.resetLoader();
|
|
this.remove(0, this.duration_());
|
|
this.trigger('reseteverything');
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
}, {
|
|
key: 'resetLoader',
|
|
value: function 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
|
|
*/
|
|
}, {
|
|
key: 'resyncLoader',
|
|
value: function resyncLoader() {
|
|
this.mediaIndex = null;
|
|
this.syncPoint_ = null;
|
|
this.abort();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
}, {
|
|
key: 'remove',
|
|
value: function remove(start, end) {
|
|
if (this.sourceUpdater_) {
|
|
this.sourceUpdater_.remove(start, end);
|
|
}
|
|
(0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.segmentMetadataTrack_);
|
|
}
|
|
|
|
/**
|
|
* (re-)schedule monitorBufferTick_ to run as soon as possible
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'monitorBuffer_',
|
|
value: function monitorBuffer_() {
|
|
if (this.checkBufferTimeout_) {
|
|
_globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
|
|
}
|
|
|
|
this.checkBufferTimeout_ = _globalWindow2['default'].setTimeout(this.monitorBufferTick_.bind(this), 1);
|
|
}
|
|
|
|
/**
|
|
* As long as the SegmentLoader is in the READY state, periodically
|
|
* invoke fillBuffer_().
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'monitorBufferTick_',
|
|
value: function monitorBufferTick_() {
|
|
if (this.state === 'READY') {
|
|
this.fillBuffer_();
|
|
}
|
|
|
|
if (this.checkBufferTimeout_) {
|
|
_globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
|
|
}
|
|
|
|
this.checkBufferTimeout_ = _globalWindow2['default'].setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY);
|
|
}
|
|
|
|
/**
|
|
* fill the buffer with segements unless the sourceBuffers are
|
|
* currently updating
|
|
*
|
|
* Note: this function should only ever be called by monitorBuffer_
|
|
* and never directly
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'fillBuffer_',
|
|
value: function fillBuffer_() {
|
|
if (this.sourceUpdater_.updating()) {
|
|
return;
|
|
}
|
|
|
|
if (!this.syncPoint_) {
|
|
this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
|
|
}
|
|
|
|
// see if we need to begin loading immediately
|
|
var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);
|
|
|
|
if (!segmentInfo) {
|
|
return;
|
|
}
|
|
|
|
var isEndOfStream = detectEndOfStream(this.playlist_, this.mediaSource_, segmentInfo.mediaIndex);
|
|
|
|
if (isEndOfStream) {
|
|
this.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.loadSegment_(segmentInfo);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
}, {
|
|
key: 'checkBuffer_',
|
|
value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) {
|
|
var lastBufferedEnd = 0;
|
|
var startOfSegment = undefined;
|
|
|
|
if (buffered.length) {
|
|
lastBufferedEnd = buffered.end(buffered.length - 1);
|
|
}
|
|
|
|
var bufferedTime = Math.max(0, lastBufferedEnd - currentTime);
|
|
|
|
if (!playlist.segments.length) {
|
|
return null;
|
|
}
|
|
|
|
// if there is plenty of content buffered, and the video has
|
|
// been played before relax for awhile
|
|
if (bufferedTime >= this.goalBufferLength_()) {
|
|
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;
|
|
}
|
|
|
|
this.logger_('checkBuffer_', 'mediaIndex:', mediaIndex, 'hasPlayed:', hasPlayed, 'currentTime:', currentTime, 'syncPoint:', syncPoint, 'fetchAtBuffer:', this.fetchAtBuffer_, 'bufferedTime:', bufferedTime);
|
|
|
|
// 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);
|
|
this.logger_('getSync', 'mediaIndex:', mediaIndex);
|
|
return this.generateSegmentInfo_(playlist, mediaIndex, null, true);
|
|
}
|
|
|
|
// Under normal playback conditions fetching is a simple walk forward
|
|
if (mediaIndex !== null) {
|
|
this.logger_('walkForward', 'mediaIndex:', mediaIndex + 1);
|
|
var 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
|
|
var mediaSourceInfo = _playlist2['default'].getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time);
|
|
|
|
mediaIndex = mediaSourceInfo.mediaIndex;
|
|
startOfSegment = mediaSourceInfo.startTime;
|
|
} else {
|
|
// Find the segment containing currentTime
|
|
var mediaSourceInfo = _playlist2['default'].getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time);
|
|
|
|
mediaIndex = mediaSourceInfo.mediaIndex;
|
|
startOfSegment = mediaSourceInfo.startTime;
|
|
}
|
|
this.logger_('getMediaIndexForTime', 'mediaIndex:', mediaIndex, 'startOfSegment:', startOfSegment);
|
|
|
|
return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
}, {
|
|
key: 'getSyncSegmentCandidate_',
|
|
value: function getSyncSegmentCandidate_(playlist) {
|
|
var _this2 = this;
|
|
|
|
if (this.currentTimeline_ === -1) {
|
|
return 0;
|
|
}
|
|
|
|
var segmentIndexArray = playlist.segments.map(function (s, i) {
|
|
return {
|
|
timeline: s.timeline,
|
|
segmentIndex: i
|
|
};
|
|
}).filter(function (s) {
|
|
return s.timeline === _this2.currentTimeline_;
|
|
});
|
|
|
|
if (segmentIndexArray.length) {
|
|
return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex;
|
|
}
|
|
|
|
return Math.max(playlist.segments.length - 1, 0);
|
|
}
|
|
}, {
|
|
key: 'generateSegmentInfo_',
|
|
value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) {
|
|
if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) {
|
|
return null;
|
|
}
|
|
|
|
var segment = playlist.segments[mediaIndex];
|
|
|
|
return {
|
|
requestId: 'segment-loader-' + Math.random(),
|
|
// resolve the segment URL relative to the playlist
|
|
uri: segment.resolvedUri,
|
|
// the segment's mediaIndex at the time it was requested
|
|
mediaIndex: mediaIndex,
|
|
// whether or not to update the SegmentLoader's state with this
|
|
// segment's mediaIndex
|
|
isSyncRequest: isSyncRequest,
|
|
startOfSegment: startOfSegment,
|
|
// the segment's playlist
|
|
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,
|
|
// retain the segment in case the playlist updates while doing an async process
|
|
segment: segment
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determines if the network has enough bandwidth to complete the current segment
|
|
* request in a timely manner. If not, the request will be aborted early and bandwidth
|
|
* updated to trigger a playlist switch.
|
|
*
|
|
* @param {Object} stats
|
|
* Object containing stats about the request timing and size
|
|
* @return {Boolean} True if the request was aborted, false otherwise
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'abortRequestEarly_',
|
|
value: function abortRequestEarly_(stats) {
|
|
if (this.hls_.tech_.paused() ||
|
|
// Don't abort if the current playlist is on the lowestEnabledRendition
|
|
// TODO: Replace using timeout with a boolean indicating whether this playlist is
|
|
// the lowestEnabledRendition.
|
|
!this.xhrOptions_.timeout ||
|
|
// Don't abort if we have no bandwidth information to estimate segment sizes
|
|
!this.playlist_.attributes.BANDWIDTH) {
|
|
return false;
|
|
}
|
|
|
|
// Wait at least 1 second since the first byte of data has been received before
|
|
// using the calculated bandwidth from the progress event to allow the bitrate
|
|
// to stabilize
|
|
if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
|
|
return false;
|
|
}
|
|
|
|
var currentTime = this.currentTime_();
|
|
var measuredBandwidth = stats.bandwidth;
|
|
var segmentDuration = this.pendingSegment_.duration;
|
|
|
|
var requestTimeRemaining = _playlist2['default'].estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived);
|
|
|
|
// Subtract 1 from the timeUntilRebuffer so we still consider an early abort
|
|
// if we are only left with less than 1 second when the request completes.
|
|
// A negative timeUntilRebuffering indicates we are already rebuffering
|
|
var timeUntilRebuffer = (0, _ranges.timeUntilRebuffer)(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1;
|
|
|
|
// Only consider aborting early if the estimated time to finish the download
|
|
// is larger than the estimated time until the player runs out of forward buffer
|
|
if (requestTimeRemaining <= timeUntilRebuffer) {
|
|
return false;
|
|
}
|
|
|
|
var switchCandidate = (0, _playlistSelectors.minRebufferMaxBandwidthSelector)({
|
|
master: this.hls_.playlists.master,
|
|
currentTime: currentTime,
|
|
bandwidth: measuredBandwidth,
|
|
duration: this.duration_(),
|
|
segmentDuration: segmentDuration,
|
|
timeUntilRebuffer: timeUntilRebuffer,
|
|
currentTimeline: this.currentTimeline_,
|
|
syncController: this.syncController_
|
|
});
|
|
|
|
if (!switchCandidate) {
|
|
return;
|
|
}
|
|
|
|
var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer;
|
|
|
|
var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;
|
|
|
|
var minimumTimeSaving = 0.5;
|
|
|
|
// If we are already rebuffering, increase the amount of variance we add to the
|
|
// potential round trip time of the new request so that we are not too aggressive
|
|
// with switching to a playlist that might save us a fraction of a second.
|
|
if (timeUntilRebuffer <= _ranges.TIME_FUDGE_FACTOR) {
|
|
minimumTimeSaving = 1;
|
|
}
|
|
|
|
if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) {
|
|
return false;
|
|
}
|
|
|
|
// set the bandwidth to that of the desired playlist being sure to scale by
|
|
// BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
|
|
// don't trigger a bandwidthupdate as the bandwidth is artifial
|
|
this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * _config2['default'].BANDWIDTH_VARIANCE + 1;
|
|
this.abort();
|
|
this.trigger('earlyabort');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* XHR `progress` event handler
|
|
*
|
|
* @param {Event}
|
|
* The XHR `progress` event
|
|
* @param {Object} simpleSegment
|
|
* A simplified segment object copy
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'handleProgress_',
|
|
value: function handleProgress_(event, simpleSegment) {
|
|
if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) {
|
|
return;
|
|
}
|
|
|
|
this.trigger('progress');
|
|
}
|
|
|
|
/**
|
|
* load a specific segment from a request into the buffer
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'loadSegment_',
|
|
value: function loadSegment_(segmentInfo) {
|
|
this.state = 'WAITING';
|
|
this.pendingSegment_ = segmentInfo;
|
|
this.trimBackBuffer_(segmentInfo);
|
|
|
|
segmentInfo.abortRequests = (0, _mediaSegmentRequest.mediaSegmentRequest)(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.createSimplifiedSegmentObj_(segmentInfo),
|
|
// progress callback
|
|
this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this));
|
|
}
|
|
|
|
/**
|
|
* trim the back buffer so that we don't have too much data
|
|
* in the source buffer
|
|
*
|
|
* @private
|
|
*
|
|
* @param {Object} segmentInfo - the current segment
|
|
*/
|
|
}, {
|
|
key: 'trimBackBuffer_',
|
|
value: function trimBackBuffer_(segmentInfo) {
|
|
var removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10);
|
|
|
|
// 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 (removeToTime > 0) {
|
|
this.remove(0, removeToTime);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* created a simplified copy of the segment object with just the
|
|
* information necessary to perform the XHR and decryption
|
|
*
|
|
* @private
|
|
*
|
|
* @param {Object} segmentInfo - the current segment
|
|
* @returns {Object} a simplified segment object copy
|
|
*/
|
|
}, {
|
|
key: 'createSimplifiedSegmentObj_',
|
|
value: function createSimplifiedSegmentObj_(segmentInfo) {
|
|
var segment = segmentInfo.segment;
|
|
var simpleSegment = {
|
|
resolvedUri: segment.resolvedUri,
|
|
byterange: segment.byterange,
|
|
requestId: segmentInfo.requestId
|
|
};
|
|
|
|
if (segment.key) {
|
|
// if the media sequence is greater than 2^32, the IV will be incorrect
|
|
// assuming 10s segments, that would be about 1300 years
|
|
var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]);
|
|
|
|
simpleSegment.key = {
|
|
resolvedUri: segment.key.resolvedUri,
|
|
iv: iv
|
|
};
|
|
}
|
|
|
|
if (segment.map) {
|
|
simpleSegment.map = this.initSegment(segment.map);
|
|
}
|
|
|
|
return simpleSegment;
|
|
}
|
|
|
|
/**
|
|
* Handle the callback from the segmentRequest function and set the
|
|
* associated SegmentLoader state and errors if necessary
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'segmentRequestFinished_',
|
|
value: function segmentRequestFinished_(error, simpleSegment) {
|
|
// every request counts as a media request even if it has been aborted
|
|
// or canceled due to a timeout
|
|
this.mediaRequests += 1;
|
|
|
|
if (simpleSegment.stats) {
|
|
this.mediaBytesTransferred += simpleSegment.stats.bytesReceived;
|
|
this.mediaTransferDuration += simpleSegment.stats.roundTripTime;
|
|
}
|
|
|
|
// The request was aborted and the SegmentLoader has already been reset
|
|
if (!this.pendingSegment_) {
|
|
this.mediaRequestsAborted += 1;
|
|
return;
|
|
}
|
|
|
|
// the request was aborted and the SegmentLoader has already started
|
|
// another request. this can happen when the timeout for an aborted
|
|
// request triggers due to a limitation in the XHR library
|
|
// do not count this as any sort of request or we risk double-counting
|
|
if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
|
|
return;
|
|
}
|
|
|
|
// an error occurred from the active pendingSegment_ so reset everything
|
|
if (error) {
|
|
this.pendingSegment_ = null;
|
|
this.state = 'READY';
|
|
|
|
// the requests were aborted just record the aborted stat and exit
|
|
// this is not a true error condition and nothing corrective needs
|
|
// to be done
|
|
if (error.code === _mediaSegmentRequest.REQUEST_ERRORS.ABORTED) {
|
|
this.mediaRequestsAborted += 1;
|
|
return;
|
|
}
|
|
|
|
this.pause();
|
|
|
|
// the error is really just that at least one of the requests timed-out
|
|
// set the bandwidth to a very low value and trigger an ABR switch to
|
|
// take emergency action
|
|
if (error.code === _mediaSegmentRequest.REQUEST_ERRORS.TIMEOUT) {
|
|
this.mediaRequestsTimedout += 1;
|
|
this.bandwidth = 1;
|
|
this.roundTrip = NaN;
|
|
this.trigger('bandwidthupdate');
|
|
return;
|
|
}
|
|
|
|
// if control-flow has arrived here, then the error is real
|
|
// emit an error event to blacklist the current playlist
|
|
this.mediaRequestsErrored += 1;
|
|
this.error(error);
|
|
this.trigger('error');
|
|
return;
|
|
}
|
|
|
|
// the response was a success so set any bandwidth stats the request
|
|
// generated for ABR purposes
|
|
this.bandwidth = simpleSegment.stats.bandwidth;
|
|
this.roundTrip = simpleSegment.stats.roundTripTime;
|
|
|
|
// if this request included an initialization segment, save that data
|
|
// to the initSegment cache
|
|
if (simpleSegment.map) {
|
|
simpleSegment.map = this.initSegment(simpleSegment.map, true);
|
|
}
|
|
|
|
this.processSegmentResponse_(simpleSegment);
|
|
}
|
|
|
|
/**
|
|
* Move any important data from the simplified segment object
|
|
* back to the real segment object for future phases
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'processSegmentResponse_',
|
|
value: function processSegmentResponse_(simpleSegment) {
|
|
var segmentInfo = this.pendingSegment_;
|
|
|
|
segmentInfo.bytes = simpleSegment.bytes;
|
|
if (simpleSegment.map) {
|
|
segmentInfo.segment.map.bytes = simpleSegment.map.bytes;
|
|
}
|
|
|
|
segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
|
|
this.handleSegment_();
|
|
}
|
|
|
|
/**
|
|
* append a decrypted segement to the SourceBuffer through a SourceUpdater
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'handleSegment_',
|
|
value: function handleSegment_() {
|
|
var _this3 = this;
|
|
|
|
if (!this.pendingSegment_) {
|
|
this.state = 'READY';
|
|
return;
|
|
}
|
|
|
|
var segmentInfo = this.pendingSegment_;
|
|
var segment = segmentInfo.segment;
|
|
var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo);
|
|
|
|
// When we have our first timing info, determine what media types this loader is
|
|
// dealing with. Although we're maintaining extra state, it helps to preserve the
|
|
// separation of segment loader from the actual source buffers.
|
|
if (typeof this.startingMedia_ === 'undefined' && timingInfo && (
|
|
// Guard against cases where we're not getting timing info at all until we are
|
|
// certain that all streams will provide it.
|
|
timingInfo.containsAudio || timingInfo.containsVideo)) {
|
|
this.startingMedia_ = {
|
|
containsAudio: timingInfo.containsAudio,
|
|
containsVideo: timingInfo.containsVideo
|
|
};
|
|
}
|
|
|
|
var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo);
|
|
|
|
if (illegalMediaSwitchError) {
|
|
this.error({
|
|
message: illegalMediaSwitchError,
|
|
blacklistDuration: Infinity
|
|
});
|
|
this.trigger('error');
|
|
return;
|
|
}
|
|
|
|
if (segmentInfo.isSyncRequest) {
|
|
this.trigger('syncinfoupdate');
|
|
this.pendingSegment_ = null;
|
|
this.state = 'READY';
|
|
return;
|
|
}
|
|
|
|
if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
|
|
this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
|
|
// fired when a timestamp offset is set in HLS (can also identify discontinuities)
|
|
this.trigger('timestampoffset');
|
|
}
|
|
|
|
var timelineMapping = this.syncController_.mappingForTimeline(segmentInfo.timeline);
|
|
|
|
if (timelineMapping !== null) {
|
|
this.trigger({
|
|
type: 'segmenttimemapping',
|
|
mapping: timelineMapping
|
|
});
|
|
}
|
|
|
|
this.state = 'APPENDING';
|
|
|
|
// if the media initialization segment is changing, append it
|
|
// before the content segment
|
|
if (segment.map) {
|
|
(function () {
|
|
var initId = (0, _binUtils.initSegmentId)(segment.map);
|
|
|
|
if (!_this3.activeInitSegmentId_ || _this3.activeInitSegmentId_ !== initId) {
|
|
var initSegment = _this3.initSegment(segment.map);
|
|
|
|
_this3.sourceUpdater_.appendBuffer(initSegment.bytes, function () {
|
|
_this3.activeInitSegmentId_ = initId;
|
|
});
|
|
}
|
|
})();
|
|
}
|
|
|
|
segmentInfo.byteLength = segmentInfo.bytes.byteLength;
|
|
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
|
|
*/
|
|
}, {
|
|
key: 'handleUpdateEnd_',
|
|
value: function handleUpdateEnd_() {
|
|
this.logger_('handleUpdateEnd_', 'segmentInfo:', this.pendingSegment_);
|
|
|
|
if (!this.pendingSegment_) {
|
|
this.state = 'READY';
|
|
if (!this.paused()) {
|
|
this.monitorBuffer_();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var segmentInfo = this.pendingSegment_;
|
|
var segment = segmentInfo.segment;
|
|
var isWalkingForward = this.mediaIndex !== null;
|
|
|
|
this.pendingSegment_ = null;
|
|
this.recordThroughput_(segmentInfo);
|
|
this.addSegmentMetadataCue_(segmentInfo);
|
|
|
|
this.state = 'READY';
|
|
|
|
this.mediaIndex = segmentInfo.mediaIndex;
|
|
this.fetchAtBuffer_ = true;
|
|
this.currentTimeline_ = segmentInfo.timeline;
|
|
|
|
// We must update the syncinfo to recalculate the seekable range before
|
|
// the following conditional otherwise it may consider this a bad "guess"
|
|
// and attempt to resync when the post-update seekable window and live
|
|
// point would mean that this was the perfect segment to fetch
|
|
this.trigger('syncinfoupdate');
|
|
|
|
// If we previously appended a segment that ends more than 3 targetDurations before
|
|
// the currentTime_ that means that our conservative guess was too conservative.
|
|
// In that case, reset the loader state so that we try to use any information gained
|
|
// from the previous request to create a new, more accurate, sync-point.
|
|
if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) {
|
|
this.resetEverything();
|
|
return;
|
|
}
|
|
|
|
// Don't do a rendition switch unless we have enough time to get a sync segment
|
|
// and conservatively guess
|
|
if (isWalkingForward) {
|
|
this.trigger('bandwidthupdate');
|
|
}
|
|
this.trigger('progress');
|
|
|
|
// 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.
|
|
var isEndOfStream = detectEndOfStream(segmentInfo.playlist, this.mediaSource_, segmentInfo.mediaIndex + 1);
|
|
|
|
if (isEndOfStream) {
|
|
this.endOfStream();
|
|
}
|
|
|
|
if (!this.paused()) {
|
|
this.monitorBuffer_();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Records the current throughput of the decrypt, transmux, and append
|
|
* portion of the semgment pipeline. `throughput.rate` is a the cumulative
|
|
* moving average of the throughput. `throughput.count` is the number of
|
|
* data points in the average.
|
|
*
|
|
* @private
|
|
* @param {Object} segmentInfo the object returned by loadSegment
|
|
*/
|
|
}, {
|
|
key: 'recordThroughput_',
|
|
value: function recordThroughput_(segmentInfo) {
|
|
var rate = this.throughput.rate;
|
|
// Add one to the time to ensure that we don't accidentally attempt to divide
|
|
// by zero in the case where the throughput is ridiculously high
|
|
var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1;
|
|
// Multiply by 8000 to convert from bytes/millisecond to bits/second
|
|
var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000);
|
|
|
|
// This is just a cumulative moving average calculation:
|
|
// newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
|
|
this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count;
|
|
}
|
|
|
|
/**
|
|
* A debugging logger noop that is set to console.log only if debugging
|
|
* is enabled globally
|
|
*
|
|
* @private
|
|
*/
|
|
}, {
|
|
key: 'logger_',
|
|
value: function logger_() {}
|
|
|
|
/**
|
|
* Adds a cue to the segment-metadata track with some metadata information about the
|
|
* segment
|
|
*
|
|
* @private
|
|
* @param {Object} segmentInfo
|
|
* the object returned by loadSegment
|
|
* @method addSegmentMetadataCue_
|
|
*/
|
|
}, {
|
|
key: 'addSegmentMetadataCue_',
|
|
value: function addSegmentMetadataCue_(segmentInfo) {
|
|
if (!this.segmentMetadataTrack_) {
|
|
return;
|
|
}
|
|
|
|
var segment = segmentInfo.segment;
|
|
var start = segment.start;
|
|
var end = segment.end;
|
|
|
|
// Do not try adding the cue if the start and end times are invalid.
|
|
if (!finite(start) || !finite(end)) {
|
|
return;
|
|
}
|
|
|
|
(0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.segmentMetadataTrack_);
|
|
|
|
var Cue = _globalWindow2['default'].WebKitDataCue || _globalWindow2['default'].VTTCue;
|
|
var value = {
|
|
bandwidth: segmentInfo.playlist.attributes.BANDWIDTH,
|
|
resolution: segmentInfo.playlist.attributes.RESOLUTION,
|
|
codecs: segmentInfo.playlist.attributes.CODECS,
|
|
byteLength: segmentInfo.byteLength,
|
|
uri: segmentInfo.uri,
|
|
timeline: segmentInfo.timeline,
|
|
playlist: segmentInfo.playlist.uri,
|
|
start: start,
|
|
end: end
|
|
};
|
|
var data = JSON.stringify(value);
|
|
var cue = new Cue(start, end, data);
|
|
|
|
// Attach the metadata to the value property of the cue to keep consistency between
|
|
// the differences of WebKitDataCue in safari and VTTCue in other browsers
|
|
cue.value = value;
|
|
|
|
this.segmentMetadataTrack_.addCue(cue);
|
|
}
|
|
}]);
|
|
|
|
return SegmentLoader;
|
|
})(_videoJs2['default'].EventTarget);
|
|
|
|
exports['default'] = SegmentLoader; |