mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-18 16:36:56 +03:00
Allow AbrManager to clear ahead of the playhead
SimpleAbrManager will now leave 5s of video ahead of the playhead and clear the rest when upgrading to a higher resolution. Also smooths transitions for overlapping segments by scheduling an abort() call to reset MediaSource internals after removing content or changing the append window. Related to issue #435 Change-Id: Ie036515388e1e7e4b3b8f3ab9922e3d5e7ed2627
This commit is contained in:
@@ -29,11 +29,25 @@
|
||||
shakaExtern.AbrManager = function() {};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {function(!Object.<string, !shakaExtern.Stream>, number=)}
|
||||
* A callback which implementations call to switch streams.
|
||||
*
|
||||
* The first argument is a map of content types to chosen streams.
|
||||
*
|
||||
* The second argument is an optional number of seconds of content to leave in
|
||||
* the buffer ahead of the playhead. Anything beyond that will be cleared.
|
||||
* This is used to make a resolution change take effect sooner, at the cost of
|
||||
* wasting previously downloaded segments. If undefined, nothing will be
|
||||
* cleared.
|
||||
*/
|
||||
shakaExtern.AbrManager.SwitchCallback;
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the AbrManager.
|
||||
*
|
||||
* @param {function(!Object.<string, !shakaExtern.Stream>)} switchCallback
|
||||
* A callback which implementations call to switch streams.
|
||||
* @param {shakaExtern.AbrManager.SwitchCallback} switchCallback
|
||||
* @exportDoc
|
||||
*/
|
||||
shakaExtern.AbrManager.prototype.init = function(switchCallback) {};
|
||||
|
||||
@@ -32,7 +32,7 @@ goog.require('shaka.log');
|
||||
* @export
|
||||
*/
|
||||
shaka.abr.SimpleAbrManager = function() {
|
||||
/** @private {?function(!Object.<string, !shakaExtern.Stream>)} */
|
||||
/** @private {?shakaExtern.AbrManager.SwitchCallback} */
|
||||
this.switch_ = null;
|
||||
|
||||
/** @private {boolean} */
|
||||
@@ -105,6 +105,16 @@ shaka.abr.SimpleAbrManager.BANDWIDTH_UPGRADE_TARGET_ = 0.85;
|
||||
shaka.abr.SimpleAbrManager.BANDWIDTH_DOWNGRADE_TARGET_ = 0.95;
|
||||
|
||||
|
||||
/**
|
||||
* The number of seconds of content to leave in buffer ahead of the playhead
|
||||
* when upgrading video. This makes video upgrades visible sooner.
|
||||
*
|
||||
* @private
|
||||
* @const {number}
|
||||
*/
|
||||
shaka.abr.SimpleAbrManager.UPGRADE_LEAVE_IN_BUFFER_ = 5;
|
||||
|
||||
|
||||
/** @override */
|
||||
shaka.abr.SimpleAbrManager.prototype.stop = function() {
|
||||
this.switch_ = null;
|
||||
@@ -206,12 +216,18 @@ shaka.abr.SimpleAbrManager.prototype.suggestStreams_ = function() {
|
||||
var oldVideo = this.streamsByType_['video'];
|
||||
var chosen = this.chooseStreams_();
|
||||
if (chosen['audio'] != oldAudio || chosen['video'] != oldVideo) {
|
||||
var opt_leaveInBuffer = undefined;
|
||||
if (oldVideo && chosen.video &&
|
||||
chosen.video.bandwidth > oldVideo.bandwidth) {
|
||||
opt_leaveInBuffer = shaka.abr.SimpleAbrManager.UPGRADE_LEAVE_IN_BUFFER_;
|
||||
}
|
||||
var currentBandwidthKbps =
|
||||
Math.round(this.bandwidthEstimator_.getBandwidthEstimate() / 1000.0);
|
||||
shaka.log.debug(
|
||||
'Calling switch_()...',
|
||||
'bandwidth=' + currentBandwidthKbps + ' kbps');
|
||||
this.switch_(chosen);
|
||||
'bandwidth=' + currentBandwidthKbps + ' kbps',
|
||||
'opt_leaveInBuffer=', opt_leaveInBuffer);
|
||||
this.switch_(chosen, opt_leaveInBuffer);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -379,9 +379,17 @@ shaka.media.MediaSourceEngine.prototype.remove =
|
||||
if (contentType == 'text') {
|
||||
return this.textEngine_.remove(startTime, endTime);
|
||||
}
|
||||
return this.enqueueOperation_(
|
||||
contentType,
|
||||
this.remove_.bind(this, contentType, startTime, endTime));
|
||||
return Promise.all([
|
||||
this.enqueueOperation_(
|
||||
contentType,
|
||||
this.remove_.bind(this, contentType, startTime, endTime)),
|
||||
// Queue an abort() to help MSE splice together overlapping segments.
|
||||
// We may have overlap if part of an already-decoded segment is removed
|
||||
// and replaced by another representation, as happens during adaptation.
|
||||
this.enqueueOperation_(
|
||||
contentType,
|
||||
this.abort_.bind(this, contentType))
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -437,9 +445,17 @@ shaka.media.MediaSourceEngine.prototype.setAppendWindowEnd = function(
|
||||
this.textEngine_.setAppendWindowEnd(appendWindowEnd);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.enqueueOperation_(
|
||||
contentType,
|
||||
this.setAppendWindowEnd_.bind(this, contentType, appendWindowEnd));
|
||||
return Promise.all([
|
||||
// Queue an abort() to help MSE splice together overlapping segments.
|
||||
// We set appendWindowEnd when we change periods in DASH content, and the
|
||||
// period transition may result in overlap.
|
||||
this.enqueueOperation_(
|
||||
contentType,
|
||||
this.abort_.bind(this, contentType)),
|
||||
this.enqueueOperation_(
|
||||
contentType,
|
||||
this.setAppendWindowEnd_.bind(this, contentType, appendWindowEnd))
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -483,6 +499,16 @@ shaka.media.MediaSourceEngine.prototype.setDuration = function(duration) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the current MediaSource duration.
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
shaka.media.MediaSourceEngine.prototype.getDuration = function() {
|
||||
return this.mediaSource_.duration;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Append data to the SourceBuffer.
|
||||
* @param {string} contentType
|
||||
@@ -506,11 +532,41 @@ shaka.media.MediaSourceEngine.prototype.append_ =
|
||||
*/
|
||||
shaka.media.MediaSourceEngine.prototype.remove_ =
|
||||
function(contentType, startTime, endTime) {
|
||||
if (endTime <= startTime) {
|
||||
// Ignore removal of inverted or empty ranges.
|
||||
// Fake 'updateend' event to resolve the operation.
|
||||
this.onUpdateEnd_(contentType);
|
||||
return;
|
||||
}
|
||||
|
||||
// This will trigger an 'updateend' event.
|
||||
this.sourceBuffers_[contentType].remove(startTime, endTime);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Call abort() on the SourceBuffer.
|
||||
* This resets MSE's last_decode_timestamp on all track buffers, which should
|
||||
* trigger the splicing logic for overlapping segments.
|
||||
* @param {string} contentType
|
||||
* @private
|
||||
*/
|
||||
shaka.media.MediaSourceEngine.prototype.abort_ = function(contentType) {
|
||||
// Save the append window end, which is reset on abort().
|
||||
var appendWindowEnd = this.sourceBuffers_[contentType].appendWindowEnd;
|
||||
|
||||
// This will not trigger an 'updateend' event, since nothing is happening.
|
||||
// This is only to reset MSE internals, not to abort an actual operation.
|
||||
this.sourceBuffers_[contentType].abort();
|
||||
|
||||
// Restore the append window end.
|
||||
this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
|
||||
|
||||
// Fake 'updateend' event to resolve the operation.
|
||||
this.onUpdateEnd_(contentType);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the SourceBuffer's timestamp offset.
|
||||
* @param {string} contentType
|
||||
|
||||
@@ -185,6 +185,7 @@ shaka.media.StreamingEngine = function(
|
||||
* performingUpdate: boolean,
|
||||
* updateTimer: ?number,
|
||||
* waitingToClearBuffer: boolean,
|
||||
* leaveInBuffer: number,
|
||||
* clearingBuffer: boolean,
|
||||
* recovering: boolean
|
||||
* }}
|
||||
@@ -219,6 +220,9 @@ shaka.media.StreamingEngine = function(
|
||||
* @property {boolean} waitingToClearBuffer
|
||||
* True indicates that the buffer must be cleared after the current update
|
||||
* finishes.
|
||||
* @property {number} leaveInBuffer
|
||||
* The amount of content to leave in buffer ahead of the playhead when we stop
|
||||
* waiting to clear the buffer and actually clear it.
|
||||
* @property {boolean} clearingBuffer
|
||||
* True indicates that the buffer is being cleared.
|
||||
* @property {boolean} recovering
|
||||
@@ -398,12 +402,10 @@ shaka.media.StreamingEngine.prototype.notifyNewStream = function(type, stream) {
|
||||
*
|
||||
* @param {string} contentType |stream|'s content type.
|
||||
* @param {shakaExtern.Stream} stream
|
||||
* @param {boolean=} opt_clearBuffer
|
||||
* @param {number=} opt_leaveInBuffer
|
||||
*/
|
||||
shaka.media.StreamingEngine.prototype.switch = function(
|
||||
contentType, stream, opt_clearBuffer) {
|
||||
// TODO: Change opt_clearBuffer to a number so only part of the buffer is
|
||||
// cleared.
|
||||
contentType, stream, opt_leaveInBuffer) {
|
||||
var mediaState = this.mediaStates_[contentType];
|
||||
goog.asserts.assert(mediaState, 'switch: expected mediaState to exist');
|
||||
if (!mediaState) return;
|
||||
@@ -434,17 +436,17 @@ shaka.media.StreamingEngine.prototype.switch = function(
|
||||
var streamTag = shaka.media.StreamingEngine.logPrefix_(mediaState);
|
||||
shaka.log.debug('switch: switching to Stream ' + streamTag);
|
||||
|
||||
if (opt_clearBuffer) {
|
||||
if (opt_leaveInBuffer != undefined) {
|
||||
// Ignore if we are already clearing the buffer.
|
||||
if (!mediaState.waitingToClearBuffer && !mediaState.clearingBuffer) {
|
||||
if (mediaState.performingUpdate) {
|
||||
// We are performing an update, so we have to wait until it's finished.
|
||||
// onUpdate_() will call clearBuffer_() when the update has
|
||||
// finished.
|
||||
mediaState.waitingToClearBuffer = true;
|
||||
this.waitToClearBuffer_(mediaState, opt_leaveInBuffer);
|
||||
} else {
|
||||
this.cancelUpdate_(mediaState);
|
||||
this.clearBuffer_(mediaState);
|
||||
this.clearBuffer_(mediaState, opt_leaveInBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -471,13 +473,11 @@ shaka.media.StreamingEngine.prototype.seeked = function() {
|
||||
var bufferedAhead = this.mediaSourceEngine_.bufferedAheadOf(
|
||||
type, playheadTime, 0.1);
|
||||
if (bufferedAhead > 0) {
|
||||
// The playhead has moved into a buffered region, so we don't need to
|
||||
// clear the buffer.
|
||||
// The playhead has moved into a buffered region.
|
||||
shaka.log.debug(logPrefix,
|
||||
'seeked: buffered seek:',
|
||||
'playheadTime=' + playheadTime,
|
||||
'bufferedAhead=' + bufferedAhead);
|
||||
mediaState.waitingToClearBuffer = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -494,7 +494,7 @@ shaka.media.StreamingEngine.prototype.seeked = function() {
|
||||
// onUpdate_() will call clearBuffer_() when the update has
|
||||
// finished.
|
||||
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: currently updating');
|
||||
mediaState.waitingToClearBuffer = true;
|
||||
this.waitToClearBuffer_(mediaState, 0 /* leaveInBuffer */);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -513,7 +513,25 @@ shaka.media.StreamingEngine.prototype.seeked = function() {
|
||||
// buffer right away. Note: clearBuffer_() will schedule the next update.
|
||||
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: handling right now');
|
||||
this.cancelUpdate_(mediaState);
|
||||
this.clearBuffer_(mediaState);
|
||||
this.clearBuffer_(mediaState, 0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
|
||||
* @param {number} leaveInBuffer
|
||||
* @private
|
||||
*/
|
||||
shaka.media.StreamingEngine.prototype.waitToClearBuffer_ =
|
||||
function(mediaState, leaveInBuffer) {
|
||||
if (mediaState.waitingToClearBuffer) {
|
||||
// We're already waiting, so keep the minimum of the leaveInBuffer values.
|
||||
mediaState.leaveInBuffer =
|
||||
Math.min(mediaState.leaveInBuffer, leaveInBuffer);
|
||||
} else {
|
||||
mediaState.waitingToClearBuffer = true;
|
||||
mediaState.leaveInBuffer = leaveInBuffer;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -565,6 +583,7 @@ shaka.media.StreamingEngine.prototype.initStreams_ = function(streamsByType) {
|
||||
performingUpdate: false,
|
||||
updateTimer: null,
|
||||
waitingToClearBuffer: false,
|
||||
leaveInBuffer: 0,
|
||||
clearingBuffer: false,
|
||||
recovering: false
|
||||
};
|
||||
@@ -722,7 +741,7 @@ shaka.media.StreamingEngine.prototype.onUpdate_ = function(mediaState) {
|
||||
if (mediaState.waitingToClearBuffer) {
|
||||
// Note: clearBuffer_() will schedule the next update.
|
||||
shaka.log.debug(logPrefix, 'skipping update and clearing the buffer');
|
||||
this.clearBuffer_(mediaState);
|
||||
this.clearBuffer_(mediaState, mediaState.leaveInBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -871,8 +890,6 @@ shaka.media.StreamingEngine.prototype.update_ = function(mediaState) {
|
||||
*/
|
||||
shaka.media.StreamingEngine.prototype.getTimeNeeded_ = function(
|
||||
mediaState, playheadTime, bufferedAhead, bufferEnd) {
|
||||
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
|
||||
|
||||
// Get the next timestamp we need. We must use |lastSegmentReference|
|
||||
// to determine this and not the actual buffer for two reasons:
|
||||
// 1. actual segments end slightly before their advertised end times, so
|
||||
@@ -880,59 +897,8 @@ shaka.media.StreamingEngine.prototype.getTimeNeeded_ = function(
|
||||
// 2. there may be drift (the timestamps in the segments are ahead/behind
|
||||
// of the timestamps in the manifest), but we need drift free times when
|
||||
// comparing times against presentation and Period boundaries.
|
||||
|
||||
if (bufferedAhead == 0) {
|
||||
// The playhead is in an unbuffered region.
|
||||
if (bufferEnd == null) {
|
||||
// The buffer is empty.
|
||||
if (mediaState.lastStream != null ||
|
||||
mediaState.lastSegmentReference) {
|
||||
shaka.log.error(logPrefix, 'lastSegmentReference should be null');
|
||||
throw new shaka.util.Error(
|
||||
shaka.util.Error.Category.STREAMING,
|
||||
shaka.util.Error.Code.INCONSISTENT_BUFFER_STATE,
|
||||
mediaState.type);
|
||||
}
|
||||
return playheadTime;
|
||||
} else if (bufferEnd > playheadTime) {
|
||||
// We may find ourseleves in this state for two reasons:
|
||||
// 1. there is a significant amount of positive drift; or
|
||||
// 2. the user agent seeked backwards but seeked() was not called or has
|
||||
// not been called yet (because it's a race).
|
||||
// For case 1 we'll idle forever, and for case 2 we'll end up buffering a
|
||||
// segment, removing it, and then buffering it again (note that this case
|
||||
// should be rare).
|
||||
shaka.log.debug(logPrefix,
|
||||
'playhead in unbuffered region (behind buffer):',
|
||||
'playheadTime=' + playheadTime,
|
||||
'bufferEnd=' + bufferEnd);
|
||||
} else {
|
||||
// We may find ourseleves in this state for four reasons:
|
||||
// 1. the playhead is exactly at the end of the buffer;
|
||||
// 2. the browser allowed the playhead to proceed past the end of
|
||||
// the buffer (either under normal or accelerated playback rates);
|
||||
// 3. there is a significant amount of negative drift; or
|
||||
// 4. the user agent seeked forwards but seeked() was not called or has
|
||||
// not been called yet (because it's a race).
|
||||
// For cases 1 and 2 we'll end up buffering the next segment we want
|
||||
// anyways, for case 3 we'll end up buffering behind the playhead until
|
||||
// we catch up, and for case 4 we'll proceed as in case 2 of the previous
|
||||
// block.
|
||||
shaka.log.debug(logPrefix,
|
||||
'playhead in unbuffered region (ahead of buffer):',
|
||||
'playheadTime=' + playheadTime,
|
||||
'bufferEnd=' + bufferEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// The buffer is non-empty.
|
||||
if (mediaState.lastStream == null ||
|
||||
mediaState.lastSegmentReference == null) {
|
||||
shaka.log.error(logPrefix, 'lastSegmentReference should not be null');
|
||||
throw new shaka.util.Error(
|
||||
shaka.util.Error.Category.STREAMING,
|
||||
shaka.util.Error.Code.INCONSISTENT_BUFFER_STATE,
|
||||
mediaState.type);
|
||||
if (!mediaState.lastStream || !mediaState.lastSegmentReference) {
|
||||
return playheadTime;
|
||||
}
|
||||
|
||||
var lastPeriodIndex =
|
||||
@@ -1655,10 +1621,11 @@ shaka.media.StreamingEngine.prototype.fetch_ = function(reference) {
|
||||
* Clears the buffer and schedules another update.
|
||||
*
|
||||
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
|
||||
* @param {number} leaveInBuffer
|
||||
* @private
|
||||
*/
|
||||
shaka.media.StreamingEngine.prototype.clearBuffer_ = function(
|
||||
mediaState) {
|
||||
mediaState, leaveInBuffer) {
|
||||
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
|
||||
|
||||
goog.asserts.assert(
|
||||
@@ -1669,9 +1636,19 @@ shaka.media.StreamingEngine.prototype.clearBuffer_ = function(
|
||||
mediaState.clearingBuffer = true;
|
||||
|
||||
shaka.log.debug(logPrefix, 'clearing buffer');
|
||||
this.mediaSourceEngine_.clear(mediaState.type).then(function() {
|
||||
var removeStart = this.playhead_.getTime() + leaveInBuffer;
|
||||
var duration = this.mediaSourceEngine_.getDuration();
|
||||
var p = leaveInBuffer ?
|
||||
this.mediaSourceEngine_.remove(mediaState.type, removeStart, duration) :
|
||||
this.mediaSourceEngine_.clear(mediaState.type);
|
||||
p.then(function() {
|
||||
if (this.destroyed_) return;
|
||||
shaka.log.debug(logPrefix, 'cleared buffer');
|
||||
if (leaveInBuffer) {
|
||||
shaka.log.debug(logPrefix, 'cleared ahead, left', leaveInBuffer);
|
||||
shaka.log.debug(logPrefix, 'removed from', removeStart, 'to', duration);
|
||||
} else {
|
||||
shaka.log.debug(logPrefix, 'cleared buffer');
|
||||
}
|
||||
mediaState.lastStream = null;
|
||||
mediaState.lastSegmentReference = null;
|
||||
mediaState.clearingBuffer = false;
|
||||
|
||||
+9
-4
@@ -1303,7 +1303,8 @@ shaka.Player.prototype.deferredSwitch_ = function(
|
||||
if (this.switchingPeriods_) {
|
||||
this.deferredSwitches_[type] = {stream: stream, clear: clear};
|
||||
} else {
|
||||
this.streamingEngine_.switch(type, stream, clear);
|
||||
var opt_leaveInBuffer = clear ? 0 : undefined;
|
||||
this.streamingEngine_.switch(type, stream, opt_leaveInBuffer);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1474,7 +1475,8 @@ shaka.Player.prototype.canSwitch_ = function() {
|
||||
// If we still have deferred switches, switch now.
|
||||
for (var type in this.deferredSwitches_) {
|
||||
var info = this.deferredSwitches_[type];
|
||||
this.streamingEngine_.switch(type, info.stream, info.clear);
|
||||
var opt_leaveInBuffer = info.clear ? 0 : undefined;
|
||||
this.streamingEngine_.switch(type, info.stream, opt_leaveInBuffer);
|
||||
}
|
||||
this.deferredSwitches_ = {};
|
||||
};
|
||||
@@ -1484,9 +1486,10 @@ shaka.Player.prototype.canSwitch_ = function() {
|
||||
* Callback from AbrManager.
|
||||
*
|
||||
* @param {!Object.<string, !shakaExtern.Stream>} streamsByType
|
||||
* @param {number=} opt_leaveInBuffer
|
||||
* @private
|
||||
*/
|
||||
shaka.Player.prototype.switch_ = function(streamsByType) {
|
||||
shaka.Player.prototype.switch_ = function(streamsByType, opt_leaveInBuffer) {
|
||||
shaka.log.debug('switch_');
|
||||
|
||||
// We have adapted to a new stream, record it in the history. Only add if
|
||||
@@ -1506,7 +1509,9 @@ shaka.Player.prototype.switch_ = function(streamsByType) {
|
||||
|
||||
if (this.streamingEngine_) {
|
||||
for (var type in streamsByType) {
|
||||
this.streamingEngine_.switch(type, streamsByType[type]);
|
||||
this.streamingEngine_.switch(type, streamsByType[type],
|
||||
// Only apply opt_leaveInBuffer to video streams.
|
||||
type == 'video' ? opt_leaveInBuffer : undefined);
|
||||
}
|
||||
this.onAdaptation_();
|
||||
}
|
||||
|
||||
@@ -379,17 +379,6 @@ shaka.util.Error.Code = {
|
||||
'ALL_STREAMS_RESTRICTED': 4012,
|
||||
|
||||
|
||||
/**
|
||||
* The StreamingEngine appended a segment but the SourceBuffer is empty, or
|
||||
* the StreamingEngine removed all segments and the SourceBuffer is
|
||||
* non-empty.
|
||||
*
|
||||
* This is an unrecoverable error.
|
||||
*
|
||||
* <br> error.data[0] is the type of content which caused the error.
|
||||
*/
|
||||
'INCONSISTENT_BUFFER_STATE': 5000,
|
||||
|
||||
/**
|
||||
* The StreamingEngine called onChooseStreams() but the callback receiver
|
||||
* did not return the correct number or type of Streams.
|
||||
|
||||
@@ -155,7 +155,8 @@ describe('SimpleAbrManager', function() {
|
||||
// called.
|
||||
abrManager.segmentDownloaded(3000, 4000, bytesPerSecond);
|
||||
|
||||
expect(switchCallback).toHaveBeenCalledWith({
|
||||
expect(switchCallback).toHaveBeenCalled();
|
||||
expect(switchCallback.calls.argsFor(0)[0]).toEqual({
|
||||
'audio': jasmine.objectContaining({bandwidth: audioBandwidth}),
|
||||
'video': jasmine.objectContaining({bandwidth: videoBandwidth})
|
||||
});
|
||||
@@ -209,7 +210,8 @@ describe('SimpleAbrManager', function() {
|
||||
}).then(function() {
|
||||
abrManager.segmentDownloaded(6000, 7000, bytesPerSecond);
|
||||
|
||||
expect(switchCallback).toHaveBeenCalledWith({
|
||||
expect(switchCallback).toHaveBeenCalled();
|
||||
expect(switchCallback.calls.argsFor(0)[0]).toEqual({
|
||||
'audio': jasmine.objectContaining({bandwidth: audioBandwidth}),
|
||||
'video': jasmine.objectContaining({bandwidth: videoBandwidth})
|
||||
});
|
||||
@@ -261,7 +263,8 @@ describe('SimpleAbrManager', function() {
|
||||
}).then(function() {
|
||||
abrManager.segmentDownloaded(12000, 13000, bytesPerSecond);
|
||||
|
||||
expect(switchCallback).toHaveBeenCalledWith({
|
||||
expect(switchCallback).toHaveBeenCalled();
|
||||
expect(switchCallback.calls.argsFor(0)[0]).toEqual({
|
||||
'audio': jasmine.objectContaining({bandwidth: audioBandwidth}),
|
||||
'video': jasmine.objectContaining({bandwidth: videoBandwidth})
|
||||
});
|
||||
@@ -294,4 +297,63 @@ describe('SimpleAbrManager', function() {
|
||||
expect(switchCallback).not.toHaveBeenCalled();
|
||||
}).catch(fail).then(done);
|
||||
});
|
||||
|
||||
it('clears ahead on upgrade', function(done) {
|
||||
// Simulate some segments being downloaded at a high rate, to trigger an
|
||||
// upgrade.
|
||||
var audioBandwidth = 5e5;
|
||||
var videoBandwidth = 4e6;
|
||||
var bytesPerSecond = 1.1 * (audioBandwidth + videoBandwidth) / 8.0;
|
||||
|
||||
abrManager.chooseStreams(streamSetsByType);
|
||||
|
||||
abrManager.segmentDownloaded(0, 1000, bytesPerSecond);
|
||||
abrManager.segmentDownloaded(1000, 2000, bytesPerSecond);
|
||||
|
||||
abrManager.enable();
|
||||
|
||||
// Move outside the startup interval.
|
||||
loop = shaka.test.Util.fakeEventLoop(
|
||||
startupInterval + 1, originalSetTimeout);
|
||||
loop.then(function() {
|
||||
// Make another call to segmentDownloaded(). switchCallback() will be
|
||||
// called to upgrade.
|
||||
abrManager.segmentDownloaded(3000, 4000, bytesPerSecond);
|
||||
|
||||
// The second parameter is the number of seconds to leave in buffer.
|
||||
expect(switchCallback).toHaveBeenCalledWith(
|
||||
jasmine.any(Object), jasmine.any(Number));
|
||||
}).catch(fail).then(done);
|
||||
});
|
||||
|
||||
it('does not clear ahead on downgrade', function(done) {
|
||||
// Simulate some segments being downloaded at a low rate, to trigger a
|
||||
// downgrade.
|
||||
var audioBandwidth = 5e5;
|
||||
var videoBandwidth = 5e5;
|
||||
var bytesPerSecond = 1.1 * (audioBandwidth + videoBandwidth) / 8.0;
|
||||
|
||||
// Set the default high so that the initial choice will be high-quality.
|
||||
abrManager.setDefaultEstimate(4e6);
|
||||
abrManager.chooseStreams(streamSetsByType);
|
||||
|
||||
abrManager.segmentDownloaded(0, 1000, bytesPerSecond);
|
||||
abrManager.segmentDownloaded(1000, 2000, bytesPerSecond);
|
||||
|
||||
abrManager.enable();
|
||||
|
||||
// Move outside the startup interval.
|
||||
loop = shaka.test.Util.fakeEventLoop(
|
||||
startupInterval + 1, originalSetTimeout);
|
||||
loop.then(function() {
|
||||
// Make another call to segmentDownloaded(). switchCallback() will be
|
||||
// called to upgrade.
|
||||
abrManager.segmentDownloaded(3000, 4000, bytesPerSecond);
|
||||
|
||||
// The second parameter is undefined to indicate that the buffer should
|
||||
// not be cleared
|
||||
expect(switchCallback).toHaveBeenCalledWith(
|
||||
jasmine.any(Object), undefined);
|
||||
}).catch(fail).then(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,15 +241,13 @@ describe('MediaSourceEngine', function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
// Wait a tick between each updateend() and the status check that follows.
|
||||
Promise.resolve().then(function() {
|
||||
expect(p1.status).toBe('resolved');
|
||||
p1.then(function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(2);
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
return p2;
|
||||
}).then(function() {
|
||||
expect(p2.status).toBe('resolved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -272,15 +270,15 @@ describe('MediaSourceEngine', function() {
|
||||
audioSourceBuffer.updateend();
|
||||
videoSourceBuffer.updateend();
|
||||
// Wait a tick between each updateend() and the status check that follows.
|
||||
Promise.resolve().then(function() {
|
||||
expect(p1.status).toBe('resolved');
|
||||
p1.then(function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
expect(p3.status).toBe('resolved');
|
||||
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(2);
|
||||
|
||||
return p3;
|
||||
}).then(function() {
|
||||
audioSourceBuffer.updateend();
|
||||
return p2;
|
||||
}).then(function() {
|
||||
expect(p2.status).toBe('resolved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -376,15 +374,13 @@ describe('MediaSourceEngine', function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
// Wait a tick between each updateend() and the status check that follows.
|
||||
Promise.resolve().then(function() {
|
||||
expect(p1.status).toBe('resolved');
|
||||
p1.then(function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(6, 10);
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
return p2;
|
||||
}).then(function() {
|
||||
expect(p2.status).toBe('resolved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -406,16 +402,14 @@ describe('MediaSourceEngine', function() {
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
videoSourceBuffer.updateend();
|
||||
// Wait a tick between each updateend() and the status check that follows.
|
||||
Promise.resolve().then(function() {
|
||||
expect(p1.status).toBe('resolved');
|
||||
p1.then(function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
expect(p3.status).toBe('resolved');
|
||||
expect(audioSourceBuffer.remove).toHaveBeenCalledWith(6, 10);
|
||||
|
||||
return p3;
|
||||
}).then(function() {
|
||||
audioSourceBuffer.updateend();
|
||||
return p2;
|
||||
}).then(function() {
|
||||
expect(p2.status).toBe('resolved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -522,18 +516,15 @@ describe('MediaSourceEngine', function() {
|
||||
expect(p3.status).toBe('pending');
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
// Wait a tick between each updateend() and the status check that follows.
|
||||
Promise.resolve().then(function() {
|
||||
expect(p1.status).toBe('resolved');
|
||||
p1.then(function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
expect(p3.status).toBe('pending');
|
||||
videoSourceBuffer.updateend();
|
||||
return p2;
|
||||
}).then(function() {
|
||||
expect(p2.status).toBe('resolved');
|
||||
// blocking multiple queues takes an extra tick to process:
|
||||
return p3;
|
||||
}).then(function() {}).then(function() {
|
||||
expect(mockMediaSource.endOfStream).toHaveBeenCalled();
|
||||
expect(p3.status).toBe('resolved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -619,18 +610,15 @@ describe('MediaSourceEngine', function() {
|
||||
expect(p3.status).toBe('pending');
|
||||
|
||||
audioSourceBuffer.updateend();
|
||||
// Wait a tick between each updateend() and the status check that follows.
|
||||
Promise.resolve().then(function() {
|
||||
expect(p1.status).toBe('resolved');
|
||||
p1.then(function() {
|
||||
expect(p2.status).toBe('pending');
|
||||
expect(p3.status).toBe('pending');
|
||||
videoSourceBuffer.updateend();
|
||||
return p2;
|
||||
}).then(function() {
|
||||
expect(p2.status).toBe('resolved');
|
||||
// blocking multiple queues takes an extra tick to process:
|
||||
return p3;
|
||||
}).then(function() {}).then(function() {
|
||||
expect(mockMediaSource.durationSetter_).toHaveBeenCalledWith(100);
|
||||
expect(p3.status).toBe('resolved');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -829,6 +817,7 @@ describe('MediaSourceEngine', function() {
|
||||
|
||||
function createMockSourceBuffer() {
|
||||
return {
|
||||
abort: jasmine.createSpy('abort'),
|
||||
appendBuffer: jasmine.createSpy('appendBuffer'),
|
||||
remove: jasmine.createSpy('remove'),
|
||||
updating: false,
|
||||
|
||||
+2
-2
@@ -755,7 +755,7 @@ describe('Player', function() {
|
||||
expect(tracks[1].id).toBe(stream.id);
|
||||
player.selectTrack(tracks[1]);
|
||||
expect(streamingEngine.switch)
|
||||
.toHaveBeenCalledWith('audio', stream, false);
|
||||
.toHaveBeenCalledWith('audio', stream, undefined);
|
||||
});
|
||||
|
||||
it('still switches streams if called during startup', function() {
|
||||
@@ -780,7 +780,7 @@ describe('Player', function() {
|
||||
var period = manifest.periods[0];
|
||||
var stream = period.streamSets[0].streams[1];
|
||||
expect(streamingEngine.switch)
|
||||
.toHaveBeenCalledWith('audio', stream, false);
|
||||
.toHaveBeenCalledWith('audio', stream, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ shaka.test.FakeMediaSourceEngine = function(segmentData, opt_drift) {
|
||||
/** @private {!Object.<string, number>} */
|
||||
this.timestampOffsets_ = {};
|
||||
|
||||
/** @private {number} */
|
||||
this.duration_ = Infinity;
|
||||
|
||||
for (var type in segmentData) {
|
||||
var data = segmentData[type];
|
||||
|
||||
@@ -78,6 +81,7 @@ shaka.test.FakeMediaSourceEngine = function(segmentData, opt_drift) {
|
||||
spyOn(this, 'setTimestampOffset').and.callThrough();
|
||||
spyOn(this, 'setAppendWindowEnd').and.callThrough();
|
||||
spyOn(this, 'setDuration').and.callThrough();
|
||||
spyOn(this, 'getDuration').and.callThrough();
|
||||
};
|
||||
|
||||
|
||||
@@ -270,10 +274,17 @@ shaka.test.FakeMediaSourceEngine.prototype.endOfStream = function(opt_reason) {
|
||||
|
||||
/** @override */
|
||||
shaka.test.FakeMediaSourceEngine.prototype.setDuration = function(duration) {
|
||||
this.duration_ = duration;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
shaka.test.FakeMediaSourceEngine.prototype.getDuration = function() {
|
||||
return this.duration_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {number} ts
|
||||
|
||||
Reference in New Issue
Block a user