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