Skip to content

Commit

Permalink
feat: Dynamic target latency (#6858)
Browse files Browse the repository at this point in the history
This adds a new feature for adjusting the target latency closer to the
live edge when playback is stable and back off away from the live edge
when playback rebuffers. It is off by default and will only run a
default of 5 times before it will stop trying to adjust the target
latency.

Closes #6131.

---------

Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
  • Loading branch information
gkatsev and avelad authored Jun 25, 2024
1 parent 9b1ef04 commit 68b4777
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 5 deletions.
16 changes: 15 additions & 1 deletion demo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,21 @@ shakaDemo.Config = class {
/* canBeDecimal= */ true)
.addBoolInput_('Panic Mode', 'streaming.liveSync.panicMode')
.addNumberInput_('Panic Mode Threshold',
'streaming.liveSync.panicThreshold');
'streaming.liveSync.panicThreshold')
.addBoolInput_('Dynamic Target Latency',
'streaming.liveSync.dynamicTargetLatency.enabled')
.addNumberInput_('Dynamic Target Latency Stability Threshold',
'streaming.liveSync.dynamicTargetLatency.stabilityThreshold')
.addNumberInput_('Dynamic Target Latency Rebuffer Increment',
'streaming.liveSync.dynamicTargetLatency.rebufferIncrement',
/* canBeDecimal= */ true,
/* canBeZero= */ true)
.addNumberInput_('Dynamic Target Latency Max Attempts',
'streaming.liveSync.dynamicTargetLatency.maxAttempts')
.addNumberInput_('Dynamic Target Latency Max Latency',
'streaming.liveSync.dynamicTargetLatency.maxLatency')
.addNumberInput_('Dynamic Target Latency Min Latency',
'streaming.liveSync.dynamicTargetLatency.minLatency');
}

/** @private */
Expand Down
51 changes: 50 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,48 @@ shaka.extern.MssManifestConfiguration;
*/
shaka.extern.ManifestConfiguration;

/**
* @typedef {{
* enabled: boolean,
* stabilityThreshold: number,
* rebufferIncrement: number,
* maxAttempts: number,
* maxLatency: number,
* minLatency: number
* }}
*
* @description
* Dynamic Target Latency configuration options.
*
* @property {boolean} enabled
* If <code>true</code>, dynamic latency for live sync is enabled. When
* enabled, the target latency will be adjusted closer to the min latency
* when playback is stable (see <code>stabilityThreshold</code>). If
* there are rebuffering events, then the target latency will move towards
* the max latency value in increments of <code>rebufferIncrement</code>.
* Defaults to <code>false</code>.
* @property {number} rebufferIncrement
* The value, in seconds, to increment the target latency towards
* <code>maxLatency</code> after a rebuffering event. Defaults to
* <code>0.5</code>.
* @property {number} stabilityThreshold
* Number of seconds after a rebuffering before we are considered stable and
* will move the target latency towards <code>minLatency</code>
* value. Defaults to <code>60</code>
* @property {number} maxAttempts
* Number of times that dynamic target latency will back off to
* <code>maxLatency</code> and attempt to adjust it closer to
* <code>minLatency</code>. Defaults to <code>10</code>
* @property {number} maxLatency
* The latency to use when a rebuffering event causes us to back off from
* the live edge. Defaults to <code>4</code>
* @property {number} minLatency
* The latency to work towards when the network is stable and we want to get
* closer to the live edge. Defaults to <code>1</code>
* @exportDoc
*/
shaka.extern.DynamicTargetLatencyConfiguration;


/**
* @typedef {{
Expand All @@ -1271,7 +1313,8 @@ shaka.extern.ManifestConfiguration;
* maxPlaybackRate: number,
* minPlaybackRate: number,
* panicMode: boolean,
* panicThreshold: number
* panicThreshold: number,
* dynamicTargetLatency: shaka.extern.DynamicTargetLatencyConfiguration
* }}
*
* @description
Expand Down Expand Up @@ -1304,6 +1347,12 @@ shaka.extern.ManifestConfiguration;
* @property {number} panicThreshold
* Number of seconds that playback stays in panic mode after a rebuffering.
* Defaults to <code>60</code>
* @property {shaka.extern.DynamicTargetLatencyConfiguration}
* dynamicTargetLatency
*
* The dynamic target latency config for dynamically adjusting the target
* latency to be closer to edge when network conditions are good and to back
* off when network conditions are bad.
* @exportDoc
*/
shaka.extern.LiveSyncConfiguration;
Expand Down
74 changes: 73 additions & 1 deletion lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
/** @private {?shaka.extern.PlayerConfiguration} */
this.config_ = this.defaultConfig_();

/** @private {?number} */
this.currentTargetLatency_ = null;

/** @private {number} */
this.rebufferingCount_ = -1;

/** @private {?number} */
this.targetLatencyReached_ = null;

/**
* The TextDisplayerFactory that was last used to make a text displayer.
* Stored so that we can tell if a new type of text displayer is desired.
Expand Down Expand Up @@ -1453,6 +1462,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.stats_ = new shaka.util.Stats(); // Replace with a clean object.
this.lastTextFactory_ = null;

this.targetLatencyReached_ = null;
this.currentTargetLatency_ = null;
this.rebufferingCount_ = -1;

this.externalSrcEqualsThumbnailsStreams_ = [];

this.completionPercent_ = NaN;
Expand Down Expand Up @@ -6315,6 +6328,30 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.cmcdManager_.setBuffering(isBuffering);
}
this.updateStateHistory_();

const dynamicTargetLatency =
this.config_.streaming.liveSync.dynamicTargetLatency.enabled;
const maxAttempts =
this.config_.streaming.liveSync.dynamicTargetLatency.maxAttempts;


if (dynamicTargetLatency && isBuffering &&
this.rebufferingCount_ < maxAttempts) {
const maxLatency =
this.config_.streaming.liveSync.dynamicTargetLatency.maxLatency;

const targetLatencyTolerance =
this.config_.streaming.liveSync.targetLatencyTolerance;
const rebufferIncrement =
this.config_.streaming.liveSync.dynamicTargetLatency
.rebufferIncrement;
if (this.currentTargetLatency_) {
this.currentTargetLatency_ = Math.min(
this.currentTargetLatency_ +
++this.rebufferingCount_ * rebufferIncrement,
maxLatency - targetLatencyTolerance);
}
}
}

// Surface the buffering event so that the app knows if/when we are
Expand Down Expand Up @@ -6441,16 +6478,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
return;
}

let targetLatency;
let maxLatency;
let maxPlaybackRate;
let minLatency;
let minPlaybackRate;
const targetLatencyTolerance =
this.config_.streaming.liveSync.targetLatencyTolerance;
const dynamicTargetLatency =
this.config_.streaming.liveSync.dynamicTargetLatency.enabled;
const stabilityThreshold =
this.config_.streaming.liveSync.dynamicTargetLatency.stabilityThreshold;

if (this.config_.streaming.liveSync &&
this.config_.streaming.liveSync.enabled) {
const targetLatency = this.config_.streaming.liveSync.targetLatency;
targetLatency = this.config_.streaming.liveSync.targetLatency;
maxLatency = targetLatency + targetLatencyTolerance;
minLatency = Math.max(0, targetLatency - targetLatencyTolerance);
maxPlaybackRate = this.config_.streaming.liveSync.maxPlaybackRate;
Expand All @@ -6459,6 +6501,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
// serviceDescription must override if it is defined in the MPD and
// liveSync configuration is not set.
if (this.manifest_ && this.manifest_.serviceDescription) {
targetLatency = this.manifest_.serviceDescription.targetLatency;
if (this.manifest_.serviceDescription.targetLatency != null) {
maxLatency = this.manifest_.serviceDescription.targetLatency +
targetLatencyTolerance;
Expand All @@ -6481,6 +6524,32 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
}

if (!this.currentTargetLatency_ && typeof targetLatency === 'number') {
this.currentTargetLatency_ = targetLatency;
}

const maxAttempts =
this.config_.streaming.liveSync.dynamicTargetLatency.maxAttempts;
if (dynamicTargetLatency && this.targetLatencyReached_ &&
this.currentTargetLatency_ !== null &&
typeof targetLatency === 'number' &&
this.rebufferingCount_ < maxAttempts &&
(Date.now() - this.targetLatencyReached_) > stabilityThreshold * 1000) {
const dynamicMinLatency =
this.config_.streaming.liveSync.dynamicTargetLatency.minLatency;
const latencyIncrement = (targetLatency - dynamicMinLatency) / 2;
this.currentTargetLatency_ = Math.max(
this.currentTargetLatency_ - latencyIncrement,
// current target latency should be within the tolerance of the min
// latency to not overshoot it
dynamicMinLatency + targetLatencyTolerance);
this.targetLatencyReached_ = Date.now();
}
if (dynamicTargetLatency && this.currentTargetLatency_ !== null) {
maxLatency = this.currentTargetLatency_ + targetLatencyTolerance;
minLatency = this.currentTargetLatency_ - targetLatencyTolerance;
}

const latency = seekRange.end - this.video_.currentTime;
let offset = 0;
// In src= mode, the seek range isn't updated frequently enough, so we need
Expand Down Expand Up @@ -6521,6 +6590,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
'Updating playbackRate to ' + maxPlaybackRate);
this.trickPlay(maxPlaybackRate);
}
this.targetLatencyReached_ = null;
} else if (minLatency && minPlaybackRate &&
(latency - offset) < minLatency) {
if (playbackRate != minPlaybackRate) {
Expand All @@ -6529,8 +6599,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
'Updating playbackRate to ' + minPlaybackRate);
this.trickPlay(minPlaybackRate);
}
this.targetLatencyReached_ = null;
} else if (playbackRate !== this.playRateController_.getDefaultRate()) {
this.cancelTrickPlay();
this.targetLatencyReached_ = Date.now();
}
}

Expand Down
8 changes: 8 additions & 0 deletions lib/util/player_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ shaka.util.PlayerConfiguration = class {
minPlaybackRate: 0.95,
panicMode: false,
panicThreshold: 60,
dynamicTargetLatency: {
enabled: false,
stabilityThreshold: 60,
rebufferIncrement: 0.5,
maxAttempts: 10,
maxLatency: 4,
minLatency: 1,
},
},
allowMediaSourceRecoveries: true,
minTimeBetweenRecoveries: 5,
Expand Down
4 changes: 2 additions & 2 deletions test/cast/cast_receiver_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
for (const message of messages) {
// Check that the update message is of a reasonable size. From previous
// testing we found that the socket would silently reject data that got
// too big. 6KB is safely below the limit.
expect(message.length).toBeLessThan(6000);
// too big. 7KB is safely below the limit.
expect(message.length).toBeLessThan(7000);
}
});

Expand Down

0 comments on commit 68b4777

Please sign in to comment.