diff --git a/lib/media/play_rate_controller.js b/lib/media/play_rate_controller.js index f0917d117c..bd3dbe1ab2 100644 --- a/lib/media/play_rate_controller.js +++ b/lib/media/play_rate_controller.js @@ -30,6 +30,9 @@ shaka.media.PlayRateController = class { /** @private {number} */ this.rate_ = this.harness_.getRate(); + /** @private {boolean} */ + this.isBuffering_ = false; + /** @private {number} */ this.pollRate_ = 0.25; @@ -50,6 +53,16 @@ shaka.media.PlayRateController = class { this.harness_ = null; } + /** + * Sets the buffering flag, which controls the effective playback rate. + * + * @param {boolean} isBuffering If true, forces playback rate to 0 internally. + */ + setBuffering(isBuffering) { + this.isBuffering_ = isBuffering; + this.apply_(); + } + /** * Set the playback rate. * @@ -93,11 +106,12 @@ shaka.media.PlayRateController = class { // Always stop the timer. We may not start it again. this.timer_.stop(); - shaka.log.v1('Changing effective playback rate to', this.rate_); + const rate = this.calculateCurrentRate_(); + shaka.log.v1('Changing effective playback rate to', rate); - if (this.rate_ >= 0) { + if (rate >= 0) { try { - this.applyRate_(this.rate_); + this.applyRate_(rate); return; } catch (e) { // Fall through to the next clause. @@ -117,6 +131,17 @@ shaka.media.PlayRateController = class { this.applyRate_(0); } + /** + * Calculate the rate that the controller wants the media element to have + * based on the current state of the controller. + * + * @return {number} + * @private + */ + calculateCurrentRate_() { + return this.isBuffering_ ? 0 : this.rate_; + } + /** * If the new rate is different than the media element's playback rate, this * will change the playback rate. If the rate does not need to change, it will diff --git a/lib/media/stall_detector.js b/lib/media/stall_detector.js index 1dbdefa411..6b52553c82 100644 --- a/lib/media/stall_detector.js +++ b/lib/media/stall_detector.js @@ -180,6 +180,12 @@ shaka.media.StallDetector.MediaElementImplementation = class { if (this.mediaElement_.buffered.length == 0) { return false; } + // the playback rate is at 0, the player is not stalled but simply pausing + // it's buffering due to not enough buffer to respect reBufferingGoal + // or minbufferTime + if (this.mediaElement_.playbackRate == 0) { + return false; + } return shaka.media.StallDetector.MediaElementImplementation.hasContentFor_( this.mediaElement_.buffered, diff --git a/lib/player.js b/lib/player.js index fa61ef0e14..55bb4bcd20 100644 --- a/lib/player.js +++ b/lib/player.js @@ -5717,6 +5717,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const loaded = this.stats_ && this.bufferObserver_ && this.playhead_; if (loaded) { + this.playRateController_.setBuffering(isBuffering); if (this.cmcdManager_) { this.cmcdManager_.setBuffering(isBuffering); } diff --git a/test/media/play_rate_controller_unit.js b/test/media/play_rate_controller_unit.js index 916166fc6e..25e715dd7a 100644 --- a/test/media/play_rate_controller_unit.js +++ b/test/media/play_rate_controller_unit.js @@ -66,4 +66,52 @@ describe('PlayRateController', () => { controller.set(1); expect(setPlayRateSpy).not.toHaveBeenCalled(); }); + + it('buffering state sets rate to zero', () => { + controller.setBuffering(true); + expect(setPlayRateSpy).toHaveBeenCalledWith(0); + + setPlayRateSpy.calls.reset(); + + controller.setBuffering(false); + expect(setPlayRateSpy).toHaveBeenCalledWith(1); + }); + + it('entering buffering state twice has no effect', () => { + controller.setBuffering(true); + expect(setPlayRateSpy).toHaveBeenCalledWith(0); + + // Reset the calls so that we can make sure it was not called again. + setPlayRateSpy.calls.reset(); + + controller.setBuffering(true); + expect(setPlayRateSpy).not.toHaveBeenCalled(); + }); + + it('leaving buffering state twice has no effect', () => { + controller.setBuffering(true); + controller.setBuffering(false); + + // Reset the calls so that we can make sure it was not called again. + setPlayRateSpy.calls.reset(); + + controller.setBuffering(false); + expect(setPlayRateSpy).not.toHaveBeenCalled(); + }); + + // When we set the rate while in a buffering state, we should see the new + // rate be used once we leave the buffering state. + it('set takes effect after buffering state ends', () => { + controller.setBuffering(true); + expect(setPlayRateSpy).toHaveBeenCalledWith(0); + + // Reset so that we can make sure it was not called after we call |set(4)|. + setPlayRateSpy.calls.reset(); + + controller.set(4); + expect(setPlayRateSpy).not.toHaveBeenCalled(); + + controller.setBuffering(false); + expect(setPlayRateSpy).toHaveBeenCalledWith(4); + }); }); diff --git a/test/player_integration.js b/test/player_integration.js index e1c11553b2..698176257f 100644 --- a/test/player_integration.js +++ b/test/player_integration.js @@ -948,6 +948,79 @@ describe('Player', () => { }); }); + describe('rebufferGoal', () => { + const startBuffering = jasmine.objectContaining({buffering: true}); + const endBuffering = jasmine.objectContaining({buffering: false}); + /** @type {!jasmine.Spy} */ + const onBuffering = jasmine.createSpy('onBuffering'); + /** @type {!jasmine.Spy} */ + const onSeeking = jasmine.createSpy('onSeeking'); + /** @type {!jasmine.Spy} */ + const onPlaying = jasmine.createSpy('onPlaying'); + /** @type {!jasmine.Spy} */ + const onTimeUpdate = jasmine.createSpy('onTimeUpdate'); + /** @type {!shaka.test.Waiter} */ + let waiter; + + beforeEach(() => { + player.addEventListener('buffering', Util.spyFunc(onBuffering)); + eventManager.listen(video, 'seeking', Util.spyFunc(onSeeking)); + eventManager.listen(video, 'timeupdate', Util.spyFunc(onTimeUpdate)); + eventManager.listen(video, 'playing', Util.spyFunc(onPlaying)); + video.autoplay = false; + waiter = new shaka.test.Waiter(eventManager) + .setPlayer(player) + .timeoutAfter(20) + .failOnTimeout(true); + }); + + it('state orchestration and buffer length', async () => { + // the expected player behaviours should be the following one + // buffering event start + // playing event but player is not playing + // because of the rebufferingGoal > to the buffer ) + // buffering event stop + // timeupdate + + // create a delay in the request to check the event's order + const netEngine = player.getNetworkingEngine(); + netEngine.registerResponseFilter( + async (type, response, context) => { + await shaka.test.Util.delay(2); + }); + + player.configure('streaming.rebufferingGoal', 25); + player.configure('streaming.bufferingGoal', 60); + player.configure('streaming.stallEnabled', true); + player.configure('streaming.stallThreshold', 1); + player.configure('manifest.dash.ignoreMinBufferTime', true); + video.autoplay = true; + await player.load('test:sintel_long_compiled'); + + expect(onBuffering).toHaveBeenCalledTimes(1); + expect(onBuffering).toHaveBeenCalledWith(startBuffering); + expect(onSeeking).not.toHaveBeenCalled(); + expect(onTimeUpdate).not.toHaveBeenCalled(); + expect(getBufferedAhead()).toBeLessThanOrEqual(25); + onBuffering.calls.reset(); + + await waiter.waitForEvent(video, 'playing'); + expect(getBufferedAhead()).toBeLessThanOrEqual(25); + expect(video.currentTime).toBe(0); + + await waiter.waitForEvent(player, 'buffering'); + await shaka.test.Util.delay(1); + expect(onSeeking).not.toHaveBeenCalled(); + expect(onTimeUpdate).toHaveBeenCalled(); + expect(onBuffering).toHaveBeenCalledTimes(1); + expect(onBuffering).toHaveBeenCalledWith(endBuffering); + await waiter.waitForEvent(video, 'timeupdate'); + // segment duration are 10s meaning + // that the expected buffer should be 30.. + expect(getBufferedAhead()).toBeGreaterThanOrEqual(25); + }); + }); + describe('buffering', () => { const startBuffering = jasmine.objectContaining({buffering: true}); const endBuffering = jasmine.objectContaining({buffering: false}); @@ -1066,14 +1139,6 @@ describe('Player', () => { expect(getBufferedBehind()).toBeLessThanOrEqual(10); }); - function getBufferedAhead() { - const end = shaka.media.TimeRangesUtils.bufferEnd(video.buffered); - if (end == null) { - return 0; - } - return end - video.currentTime; - } - function getBufferedBehind() { const start = shaka.media.TimeRangesUtils.bufferStart(video.buffered); if (start == null) { @@ -1083,6 +1148,14 @@ describe('Player', () => { } }); // describe('buffering') + function getBufferedAhead() { + const end = shaka.media.TimeRangesUtils.bufferEnd(video.buffered); + if (end == null) { + return 0; + } + return end - video.currentTime; + } + describe('configuration', () => { it('has the correct number of arguments in compiled callbacks', () => { // Get the default configuration for both the compiled & uncompiled diff --git a/test/player_unit.js b/test/player_unit.js index 419448cedf..3d887129e0 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -2710,6 +2710,7 @@ describe('Player', () => { }); it('tracks info about current stream', () => { + forceBufferingTo(false); let stats = player.getStats(); // Should have chosen the first of each type of stream. expect(stats.width).toBe(100);