Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reBufferingGoal is not respected #6433

Merged
merged 9 commits into from May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 28 additions & 3 deletions lib/media/play_rate_controller.js
Iragne marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -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;

Expand All @@ -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.
*
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/media/stall_detector.js
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/player.js
Expand Up @@ -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);
}
Expand Down
48 changes: 48 additions & 0 deletions test/media/play_rate_controller_unit.js
Expand Up @@ -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);
});
});
89 changes: 81 additions & 8 deletions test/player_integration.js
Expand Up @@ -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});
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions test/player_unit.js
Expand Up @@ -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);
Expand Down