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:
Joey Parrish
2016-07-12 13:18:46 -07:00
parent 28aac020a6
commit 6daa7f3b83
10 changed files with 251 additions and 132 deletions
+16 -2
View File
@@ -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) {};
+19 -3
View File
@@ -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);
}
};
+62 -6
View File
@@ -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
+48 -71
View File
@@ -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
View File
@@ -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_();
}
-11
View File
@@ -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.
+65 -3
View File
@@ -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);
});
});
+19 -30
View File
@@ -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
View File
@@ -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