From db44dc82242fd9dd845024737e969fef0e4caae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Thu, 22 Jun 2023 22:11:45 +0200 Subject: [PATCH] feat: Add liveSync configuration to catch up on live streams (#5304) --- demo/common/message_ids.js | 3 ++ demo/config.js | 12 ++++++- demo/locales/en.json | 3 ++ demo/locales/source.json | 12 +++++++ externs/shaka/player.js | 17 +++++++++- lib/player.js | 55 ++++++++++++++++++++++++++++++++ lib/util/player_configuration.js | 3 ++ 7 files changed, 103 insertions(+), 2 deletions(-) diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index d9adce6065..8b852c9450 100644 --- a/demo/common/message_ids.js +++ b/demo/common/message_ids.js @@ -219,6 +219,9 @@ shakaDemo.MessageIds = { LCEVC_LOG_LEVEL: 'DEMO_LCEVC_LOG_LEVEL', LCEVC_SECTION_HEADER: 'DEMO_LCEVC_SECTION_HEADER', LIVE_SEGMENTS_DELAY: 'DEMO_LIVE_SEGMENTS_DELAY', + LIVE_SYNC: 'DEMO_LIVE_SYNC', + LIVE_SYNC_MAX_LATENCY: 'DEMO_LIVE_SYNC_MAX_LATENCY', + LIVE_SYNC_PLAYBACK_RATE: 'DEMO_LIVE_SYNC_PLAYBACK_RATE', LOG_LEVEL: 'DEMO_LOG_LEVEL', LOG_LEVEL_DEBUG: 'DEMO_LOG_LEVEL_DEBUG', LOG_LEVEL_INFO: 'DEMO_LOG_LEVEL_INFO', diff --git a/demo/config.js b/demo/config.js index 6fd343ce85..21c3328855 100644 --- a/demo/config.js +++ b/demo/config.js @@ -432,7 +432,17 @@ shakaDemo.Config = class { .addNumberInput_(MessageIds.MAX_DISABLED_TIME, 'streaming.maxDisabledTime') .addNumberInput_(MessageIds.SEGMENT_PREFETCH_LIMIT, - 'streaming.segmentPrefetchLimit'); + 'streaming.segmentPrefetchLimit') + .addBoolInput_(MessageIds.LIVE_SYNC, + 'streaming.liveSync') + .addNumberInput_(MessageIds.LIVE_SYNC_MAX_LATENCY, + 'streaming.liveSyncMaxLatency', + /* canBeDecimal= */ true, + /* canBeZero= */ true) + .addNumberInput_(MessageIds.LIVE_SYNC_PLAYBACK_RATE, + 'streaming.liveSyncPlaybackRate', + /* canBeDecimal= */ true, + /* canBeZero= */ false); if (!shakaDemoMain.getNativeControlsEnabled()) { this.addBoolInput_(MessageIds.ALWAYS_STREAM_TEXT, diff --git a/demo/locales/en.json b/demo/locales/en.json index 667a14462d..4057c042b1 100644 --- a/demo/locales/en.json +++ b/demo/locales/en.json @@ -134,6 +134,9 @@ "DEMO_LIVE": "Live", "DEMO_LIVE_SEARCH": "Live", "DEMO_LIVE_SEGMENTS_DELAY": "Live segments delay", + "DEMO_LIVE_SYNC": "Live sync", + "DEMO_LIVE_SYNC_MAX_LATENCY":"Max latency for live sync", + "DEMO_LIVE_SYNC_PLAYBACK_RATE":"Playback rate for live sync", "DEMO_LOG_LEVEL": "Log Level", "DEMO_LOG_LEVEL_DEBUG": "Debug", "DEMO_LOG_LEVEL_INFO": "Info", diff --git a/demo/locales/source.json b/demo/locales/source.json index 4ea488cca0..6f9b793133 100644 --- a/demo/locales/source.json +++ b/demo/locales/source.json @@ -543,6 +543,18 @@ "description": "The name of a configuration value.", "message": "Live segments delay" }, + "DEMO_LIVE_SYNC": { + "description": "The name of a configuration value.", + "message": "Live sync" + }, + "DEMO_LIVE_SYNC_MAX_LATENCY": { + "description": "The name of a configuration value.", + "message": "Max latency for live sync" + }, + "DEMO_LIVE_SYNC_PLAYBACK_RATE": { + "description": "The name of a configuration value.", + "message": "Playback rate for live sync" + }, "DEMO_LOG_LEVEL": { "description": "The name of a configuration value.", "message": "Log Level" diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 36688fbc00..87ac11d544 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -1078,7 +1078,10 @@ shaka.extern.ManifestConfiguration; * observeQualityChanges: boolean, * maxDisabledTime: number, * parsePrftBox: boolean, - * segmentPrefetchLimit: number + * segmentPrefetchLimit: number, + * liveSync: boolean, + * liveSyncMaxLatency: number, + * liveSyncPlaybackRate: number * }} * * @description @@ -1193,6 +1196,18 @@ shaka.extern.ManifestConfiguration; * ahead of playhead in parallel. * If 0, the segments will be fetched sequentially. * Defaults to 0. + * @property {boolean} liveSync + * Enable the live stream sync against the live edge by changing the playback + * rate. Defaults to false. + * Note: on some SmartTVs, if this is activated, it may not work or the sound + * may be lost when activated. + * @property {number} liveSyncMaxLatency + * Maximum acceptable latency, in seconds. Effective only if liveSync is + * true. Defaults to 1. + * @property {number} liveSyncPlaybackRate + * Playback rate used for latency chasing. It is recommended to use a value + * between 1 and 2. Effective only if liveSync is true. Defaults to + * 1.1. * @exportDoc */ shaka.extern.StreamingConfiguration; diff --git a/lib/player.js b/lib/player.js index 69663abcd0..82b061aaf9 100644 --- a/lib/player.js +++ b/lib/player.js @@ -2275,6 +2275,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { 'arbitrary language initially'); } + if (this.isLive() && this.config_.streaming.liveSync) { + const onTimeUpdate = () => this.onTimeUpdate_(); + this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate); + } + this.fullyLoaded_ = true; // Wait for the 'loadedmetadata' event to measure load() latency. @@ -2627,6 +2632,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { fullyLoaded.reject(abortedError); return Promise.resolve(); // Abort complete. }).chain(() => { + if (this.isLive() && this.config_.streaming.liveSync) { + const onTimeUpdate = () => this.onTimeUpdate_(); + this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate); + } + this.fullyLoaded_ = true; }); } @@ -5692,6 +5702,51 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } } + /** + * Callback for liveSync + * + * @private + */ + onTimeUpdate_() { + // If the live stream has reached its end, do not sync. + if (!this.isLive()) { + return; + } + const seekRange = this.seekRange(); + if (!Number.isFinite(seekRange.end)) { + return; + } + const currentTime = this.video_.currentTime; + if (currentTime < seekRange.start) { + // Bad stream? + return; + } + const liveSyncMaxLatency = this.config_.streaming.liveSyncMaxLatency; + const liveSyncPlaybackRate = this.config_.streaming.liveSyncPlaybackRate; + const playbackRate = this.video_.playbackRate; + const latency = seekRange.end - this.video_.currentTime; + let offset = 0; + // In src= mode, the seek range isn't updated frequently enough, so we need + // to fudge the latency number with an offset. The playback rate is used + // as an offset, since that is the amount we catch up 1 second of + // accelerated playback. + if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { + const buffered = this.video_.buffered; + if (buffered.length > 0) { + const bufferedEnd = buffered.end(buffered.length - 1); + offset = Math.max(liveSyncPlaybackRate, bufferedEnd - seekRange.end); + } + } + + if ((latency - offset) > liveSyncMaxLatency) { + if (playbackRate != liveSyncPlaybackRate) { + this.trickPlay(liveSyncPlaybackRate); + } + } else if (playbackRate !== 1 && playbackRate !== 0) { + this.cancelTrickPlay(); + } + } + /** * Callback from Playhead. * diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 71b1105cc3..a466145487 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -209,6 +209,9 @@ shaka.util.PlayerConfiguration = class { // When low latency streaming is enabled, segmentPrefetchLimit will // default to 2 if not specified. segmentPrefetchLimit: 0, + liveSync: false, + liveSyncMaxLatency: 1, + liveSyncPlaybackRate: 1.1, }; // WebOS, Tizen, and Chromecast have long hardware pipelines that respond