Skip to content

Commit

Permalink
feat: add a live sync panic mode (#6149)
Browse files Browse the repository at this point in the history
This PR introduces a live sync panic mode
(`streaming.liveSyncPanicMode`) which sets the player into the
`streaming.liveSyncMinPlaybackRate` while we're within the
`streaming.liveSyncPanicThreshold`. This should help reduce the change
of subsequent rebuffering events by moving further away from the live
edge.

Related to #6131.
  • Loading branch information
gkatsev committed Jan 25, 2024
1 parent 701ec9b commit 65981e2
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 5 deletions.
3 changes: 3 additions & 0 deletions demo/config.js
Expand Up @@ -462,6 +462,9 @@ shakaDemo.Config = class {
'streaming.liveSyncMinPlaybackRate',
/* canBeDecimal= */ true,
/* canBeZero= */ false)
.addBoolInput_('Live Sync Panic Mode', 'streaming.liveSyncPanicMode')
.addNumberInput_('Live Sync Panic Mode Threshold',
'streaming.liveSyncPanicThreshold')
.addBoolInput_('Allow Media Source recoveries',
'streaming.allowMediaSourceRecoveries')
.addNumberInput_('Minimum time between recoveries',
Expand Down
12 changes: 11 additions & 1 deletion externs/shaka/player.js
Expand Up @@ -1164,6 +1164,8 @@ shaka.extern.ManifestConfiguration;
* liveSyncPlaybackRate: number,
* liveSyncMinLatency: number,
* liveSyncMinPlaybackRate: number,
* liveSyncPanicMode: boolean,
* liveSyncPanicThreshold: number,
* allowMediaSourceRecoveries: boolean,
* minTimeBetweenRecoveries: number
* }}
Expand Down Expand Up @@ -1265,7 +1267,7 @@ shaka.extern.ManifestConfiguration;
* If true, all emsg boxes are parsed and dispatched.
* @property {boolean} observeQualityChanges
* If true, monitor media quality changes and emit
* <code.shaka.Player.MediaQualityChangedEvent</code>.
* <code>shaka.Player.MediaQualityChangedEvent</code>.
* @property {number} maxDisabledTime
* The maximum time a variant can be disabled when NETWORK HTTP_ERROR
* is reached, in seconds.
Expand Down Expand Up @@ -1301,6 +1303,14 @@ shaka.extern.ManifestConfiguration;
* Minimum playback rate used for latency chasing. It is recommended to use a
* value between 0 and 1. Effective only if liveSync is true. Defaults to
* <code>1</code>.
* @property {boolean} liveSyncPanicMode
* If <code>true</code>, panic mode for live sync is enabled. When enabled,
* will set the playback rate to the <code>liveSyncMinPlaybackRate</code>
* until playback has continued past a rebuffering for longer than the
* <code>liveSyncPanicThreshold</code>. Defaults to <code>false</code>.
* @property {number} liveSyncPanicThreshold
* Number of seconds that playback stays in panic mode after a rebuffering.
* Defaults to <code>60</code>
* @property {boolean} allowMediaSourceRecoveries
* Indicate if we should recover from VIDEO_ERROR resetting Media Source.
* Defaults to <code>true</code>.
Expand Down
26 changes: 25 additions & 1 deletion lib/media/buffering_observer.js
Expand Up @@ -28,6 +28,9 @@ shaka.media.BufferingObserver = class {
this.thresholds_ = new Map()
.set(State.SATISFIED, thresholdWhenSatisfied)
.set(State.STARVING, thresholdWhenStarving);

/** @private {number} */
this.lastRebufferTime_ = 0;
}

/**
Expand Down Expand Up @@ -73,7 +76,13 @@ shaka.media.BufferingObserver = class {
this.previousState_ = newState;

// Return |true| only when the state has changed.
return oldState != newState;
const stateChanged = oldState != newState;

if (stateChanged && newState === State.SATISFIED) {
this.lastRebufferTime_ = Date.now();
}

return stateChanged;
}

/**
Expand All @@ -93,6 +102,21 @@ shaka.media.BufferingObserver = class {
getState() {
return this.previousState_;
}

/**
* Return the last time that the state went from |STARVING| to |SATISFIED|.
* @return {number}
*/
getLastRebufferTime() {
return this.lastRebufferTime_;
}

/**
* Reset the last rebuffer time to zero.
*/
resetLastRebufferTime() {
this.lastRebufferTime_ = 0;
}
};

/**
Expand Down
25 changes: 22 additions & 3 deletions lib/player.js
Expand Up @@ -2052,7 +2052,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
const isLive = this.isLive();

if (isLive && (this.config_.streaming.liveSync ||
this.manifest_.serviceDescription)) {
this.manifest_.serviceDescription ||
this.config_.streaming.liveSyncPanicMode)) {
const onTimeUpdate = () => this.onTimeUpdate_();
this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
} else if (!isLive) {
Expand Down Expand Up @@ -2383,7 +2384,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {

const isLive = this.isLive();

if (isLive && this.config_.streaming.liveSync) {
if (isLive && (this.config_.streaming.liveSync ||
this.config_.streaming.liveSyncPanicMode)) {
const onTimeUpdate = () => this.onTimeUpdate_();
this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
} else if (!isLive) {
Expand Down Expand Up @@ -5707,7 +5709,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
}

if (liveSyncMaxLatency && liveSyncPlaybackRate &&
const panicMode = this.config_.streaming.liveSyncPanicMode;
const panicThreshold = this.config_.streaming.liveSyncPanicThreshold * 1000;
const timeSinceLastRebuffer =
Date.now() - this.bufferObserver_.getLastRebufferTime();
if (panicMode && !liveSyncMinPlaybackRate) {
liveSyncMinPlaybackRate = this.config_.streaming.liveSyncMinPlaybackRate;
}

if (panicMode && liveSyncMinPlaybackRate &&
timeSinceLastRebuffer <= panicThreshold) {
if (playbackRate != liveSyncMinPlaybackRate) {
shaka.log.debug('Time since last rebuffer (' +
timeSinceLastRebuffer + 's) ' +
'is less than the liveSyncPanicThreshold (' + panicThreshold +
's). Updating playbackRate to ' + liveSyncMinPlaybackRate);
this.trickPlay(liveSyncMinPlaybackRate);
}
} else if (liveSyncMaxLatency && liveSyncPlaybackRate &&
(latency - offset) > liveSyncMaxLatency) {
if (playbackRate != liveSyncPlaybackRate) {
shaka.log.debug('Latency (' + latency + 's) ' +
Expand Down
2 changes: 2 additions & 0 deletions lib/util/player_configuration.js
Expand Up @@ -229,6 +229,8 @@ shaka.util.PlayerConfiguration = class {
liveSyncPlaybackRate: 1.1,
liveSyncMinLatency: 0,
liveSyncMinPlaybackRate: 1,
liveSyncPanicMode: false,
liveSyncPanicThreshold: 60,
allowMediaSourceRecoveries: true,
minTimeBetweenRecoveries: 5,
};
Expand Down
3 changes: 3 additions & 0 deletions test/media/buffering_observer_unit.js
Expand Up @@ -101,6 +101,7 @@ describe('BufferingObserver', () => {
beforeEach(() => {
controller.setState(State.STARVING);
expect(controller.getState()).toBe(State.STARVING);
expect(controller.getLastRebufferTime()).toBe(0);
});

it('becomes satisfied when enough content is buffered', () => {
Expand All @@ -122,6 +123,7 @@ describe('BufferingObserver', () => {
changed = controller.update(/* lead= */ 5, /* toEnd= */ false);
expect(changed).toBeTruthy();
expect(controller.getState()).toBe(State.SATISFIED);
expect(controller.getLastRebufferTime()).toBe(Date.now());
});

it('becomes satisfied when the end is buffered', () => {
Expand All @@ -138,6 +140,7 @@ describe('BufferingObserver', () => {
changed = controller.update(/* lead= */ 3, /* toEnd= */ true);
expect(changed).toBeTruthy();
expect(controller.getState()).toBe(State.SATISFIED);
expect(controller.getLastRebufferTime()).toBe(Date.now());
});
});
});

0 comments on commit 65981e2

Please sign in to comment.