diff --git a/externs/shaka/abr_manager.js b/externs/shaka/abr_manager.js index 3dee567de..5de91bf89 100644 --- a/externs/shaka/abr_manager.js +++ b/externs/shaka/abr_manager.js @@ -29,11 +29,25 @@ shakaExtern.AbrManager = function() {}; +/** + * @typedef {function(!Object., 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.)} switchCallback - * A callback which implementations call to switch streams. + * @param {shakaExtern.AbrManager.SwitchCallback} switchCallback * @exportDoc */ shakaExtern.AbrManager.prototype.init = function(switchCallback) {}; diff --git a/lib/abr/simple_abr_manager.js b/lib/abr/simple_abr_manager.js index 977153afa..5713e4a21 100644 --- a/lib/abr/simple_abr_manager.js +++ b/lib/abr/simple_abr_manager.js @@ -32,7 +32,7 @@ goog.require('shaka.log'); * @export */ shaka.abr.SimpleAbrManager = function() { - /** @private {?function(!Object.)} */ + /** @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); } }; diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index f892a4843..2b382caa0 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -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 diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index be5ead6e0..5eb1cb8dc 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -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; diff --git a/lib/player.js b/lib/player.js index 084951b33..27c20e6e5 100644 --- a/lib/player.js +++ b/lib/player.js @@ -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.} 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_(); } diff --git a/lib/util/error.js b/lib/util/error.js index 94899c58c..38da2728e 100644 --- a/lib/util/error.js +++ b/lib/util/error.js @@ -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. - * - *
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. diff --git a/test/abr/simple_abr_manager_unit.js b/test/abr/simple_abr_manager_unit.js index ffcadb605..33c989ead 100644 --- a/test/abr/simple_abr_manager_unit.js +++ b/test/abr/simple_abr_manager_unit.js @@ -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); + }); }); diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index 7e0592fb8..a2dab28f6 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -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, diff --git a/test/player_unit.js b/test/player_unit.js index 3992147a8..36c1e3901 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -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); }); }); diff --git a/test/test/util/fake_media_source_engine.js b/test/test/util/fake_media_source_engine.js index 702d59612..ec731dcad 100644 --- a/test/test/util/fake_media_source_engine.js +++ b/test/test/util/fake_media_source_engine.js @@ -50,6 +50,9 @@ shaka.test.FakeMediaSourceEngine = function(segmentData, opt_drift) { /** @private {!Object.} */ 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