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); } });