From d64060a3ad28dcfc54e8f03a0d0c440b2c076732 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Wed, 14 Jun 2023 13:46:16 +0200 Subject: [PATCH 1/5] feat: Add liveSync configuration --- 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 | 53 ++++++++++++++++++++++++++++++++ lib/util/player_configuration.js | 3 ++ 7 files changed, 101 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..8510b2c8ac 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= */ false) + .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..96a403ce00 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.2. * @exportDoc */ shaka.extern.StreamingConfiguration; diff --git a/lib/player.js b/lib/player.js index 67adda8146..54b4ba0cd8 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; }); } @@ -5691,6 +5701,49 @@ 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; + // HLS in SRC= doesn't update every time the seekrange, so we need calculate + // a safe offset + 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..6671edac7c 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.2, }; // WebOS, Tizen, and Chromecast have long hardware pipelines that respond From 000e98f3d03742528f90147ac1eb033f6311f152 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Wed, 14 Jun 2023 17:32:25 +0200 Subject: [PATCH 2/5] Change default value to 1.1 --- externs/shaka/player.js | 2 +- lib/util/player_configuration.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 96a403ce00..87ac11d544 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -1207,7 +1207,7 @@ shaka.extern.ManifestConfiguration; * @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.2. + * 1.1. * @exportDoc */ shaka.extern.StreamingConfiguration; diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 6671edac7c..a466145487 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -211,7 +211,7 @@ shaka.util.PlayerConfiguration = class { segmentPrefetchLimit: 0, liveSync: false, liveSyncMaxLatency: 1, - liveSyncPlaybackRate: 1.2, + liveSyncPlaybackRate: 1.1, }; // WebOS, Tizen, and Chromecast have long hardware pipelines that respond From a8bc3584a8e9c196ff63be539e798de870a37d95 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Wed, 14 Jun 2023 17:32:49 +0200 Subject: [PATCH 3/5] Fix comparison --- lib/player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/player.js b/lib/player.js index 54b4ba0cd8..13b83bdffb 100644 --- a/lib/player.js +++ b/lib/player.js @@ -5716,7 +5716,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return; } const currentTime = this.video_.currentTime; - if (currentTime > seekRange.start) { + if (currentTime < seekRange.start) { // Bad stream? return; } From ef70a61fd2742d7086b9fbf8f15a7033b516f09b Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Thu, 15 Jun 2023 10:39:00 +0200 Subject: [PATCH 4/5] Fix demo --- demo/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/config.js b/demo/config.js index 8510b2c8ac..21c3328855 100644 --- a/demo/config.js +++ b/demo/config.js @@ -438,7 +438,7 @@ shakaDemo.Config = class { .addNumberInput_(MessageIds.LIVE_SYNC_MAX_LATENCY, 'streaming.liveSyncMaxLatency', /* canBeDecimal= */ true, - /* canBeZero= */ false) + /* canBeZero= */ true) .addNumberInput_(MessageIds.LIVE_SYNC_PLAYBACK_RATE, 'streaming.liveSyncPlaybackRate', /* canBeDecimal= */ true, From b1e42c3192c441d95517d59859b9e9d7184b6f0d Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Sat, 17 Jun 2023 18:18:04 +0200 Subject: [PATCH 5/5] Change comment --- lib/player.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/player.js b/lib/player.js index 13b83bdffb..76b7c3201f 100644 --- a/lib/player.js +++ b/lib/player.js @@ -5725,8 +5725,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const playbackRate = this.video_.playbackRate; const latency = seekRange.end - this.video_.currentTime; let offset = 0; - // HLS in SRC= doesn't update every time the seekrange, so we need calculate - // a safe offset + // 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) {