diff --git a/demo/config.js b/demo/config.js
index d80552cc68..40c6eddf6b 100644
--- a/demo/config.js
+++ b/demo/config.js
@@ -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 */
diff --git a/externs/shaka/player.js b/externs/shaka/player.js
index c3eee6cd84..e62f40f0dd 100644
--- a/externs/shaka/player.js
+++ b/externs/shaka/player.js
@@ -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 true
, 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 stabilityThreshold
). If
+ * there are rebuffering events, then the target latency will move towards
+ * the max latency value in increments of rebufferIncrement
.
+ * Defaults to false
.
+ * @property {number} rebufferIncrement
+ * The value, in seconds, to increment the target latency towards
+ * maxLatency
after a rebuffering event. Defaults to
+ * 0.5
.
+ * @property {number} stabilityThreshold
+ * Number of seconds after a rebuffering before we are considered stable and
+ * will move the target latency towards minLatency
+ * value. Defaults to 60
+ * @property {number} maxAttempts
+ * Number of times that dynamic target latency will back off to
+ * maxLatency
and attempt to adjust it closer to
+ * minLatency
. Defaults to 10
+ * @property {number} maxLatency
+ * The latency to use when a rebuffering event causes us to back off from
+ * the live edge. Defaults to 4
+ * @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 1
+ * @exportDoc
+ */
+shaka.extern.DynamicTargetLatencyConfiguration;
+
/**
* @typedef {{
@@ -1271,7 +1313,8 @@ shaka.extern.ManifestConfiguration;
* maxPlaybackRate: number,
* minPlaybackRate: number,
* panicMode: boolean,
- * panicThreshold: number
+ * panicThreshold: number,
+ * dynamicTargetLatency: shaka.extern.DynamicTargetLatencyConfiguration
* }}
*
* @description
@@ -1304,6 +1347,12 @@ shaka.extern.ManifestConfiguration;
* @property {number} panicThreshold
* Number of seconds that playback stays in panic mode after a rebuffering.
* Defaults to 60
+ * @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;
diff --git a/lib/player.js b/lib/player.js
index c938ad5d62..86f1a4cc24 100644
--- a/lib/player.js
+++ b/lib/player.js
@@ -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.
@@ -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;
@@ -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
@@ -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;
@@ -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;
@@ -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
@@ -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) {
@@ -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();
}
}
diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js
index de697d5d36..564d506c4a 100644
--- a/lib/util/player_configuration.js
+++ b/lib/util/player_configuration.js
@@ -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,
diff --git a/test/cast/cast_receiver_integration.js b/test/cast/cast_receiver_integration.js
index d1ebe03bed..d298226efe 100644
--- a/test/cast/cast_receiver_integration.js
+++ b/test/cast/cast_receiver_integration.js
@@ -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);
}
});