diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js index d9adce60654..8b852c94506 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 6fd343ce857..8510b2c8ac3 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 667a14462d1..4057c042b12 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 4ea488cca07..6f9b7931333 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 36688fbc005..96a403ce002 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 67adda81468..760526202bc 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 71b1105cc31..6671edac7c2 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