From edb44779c012e98ab483636d2c86b312c8301fa2 Mon Sep 17 00:00:00 2001 From: Sebastian Piquerez Date: Fri, 2 May 2025 14:45:47 -0300 Subject: [PATCH 1/4] Add global synchronization functionality for DASH players --- .../players-synchronization/globalSync.html | 96 +++++++++ .../playerSynchronizationPlugin.js | 185 ++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 samples/players-synchronization/globalSync.html create mode 100644 samples/players-synchronization/playerSynchronizationPlugin.js diff --git a/samples/players-synchronization/globalSync.html b/samples/players-synchronization/globalSync.html new file mode 100644 index 0000000000..2ef5db2876 --- /dev/null +++ b/samples/players-synchronization/globalSync.html @@ -0,0 +1,96 @@ + + + + + + Follower client + + + + + + + + + + + + +
+
+
+

Follower client

+
+
+
+
+ + + +
+ +
+
+
+ © DASH-IF +
+
+
+ + + + + \ No newline at end of file diff --git a/samples/players-synchronization/playerSynchronizationPlugin.js b/samples/players-synchronization/playerSynchronizationPlugin.js new file mode 100644 index 0000000000..f76f7b3b09 --- /dev/null +++ b/samples/players-synchronization/playerSynchronizationPlugin.js @@ -0,0 +1,185 @@ +const CMCD_MODE_QUERY = 'query'; +let leaderTimestamp; +let leaderPlayhead; +let playbackRate; + +let lastInterval; + +(() => { + const syncPlayer = (player, config) => { + if (!leaderTimestamp || !leaderPlayhead || !playbackRate) { + return; + } + + const currentTimestamp = Date.now(); + const timeElapsed = (currentTimestamp - leaderTimestamp) / 1000; + const timeToSeek = leaderPlayhead + timeElapsed * playbackRate; + + const currentRepresentation = player.getCurrentRepresentationForType('video'); + const currentFrameRate = currentRepresentation.frameRate; + const frameDelay = Math.min(config.frameDelay, currentFrameRate); + + const SEEK_THRESHOLD = config.seekThreshold ?? (currentFrameRate / 100); + const SYNC_THRESHOLD = (currentFrameRate / 1000) * frameDelay; + + const getPlayersTimeDifference = () => Math.abs(timeToSeek - player.time()); + + const playersDifference = getPlayersTimeDifference(); + if (playersDifference > SEEK_THRESHOLD) { + player.seekToPresentationTime(timeToSeek); + player.setPlaybackRate(playbackRate); + if (lastInterval) { + clearInterval(lastInterval); + lastInterval = null; + } + } else if (playersDifference > SYNC_THRESHOLD) { + if (lastInterval) { + return; + } + + const isAheadOfTheLeader = player.time() > timeToSeek; + const catchUpRate = Math.max(config.catchUpRate, 1); + const speedModification = isAheadOfTheLeader ? 1 / catchUpRate : catchUpRate; + + player.setPlaybackRate(speedModification * playbackRate); + const interval = setInterval(() => { + lastInterval = interval; + const currentDifference = getPlayersTimeDifference(); + if (currentDifference <= SYNC_THRESHOLD) { + player.setPlaybackRate(playbackRate); + lastInterval = null; + clearInterval(interval); + } else if (currentDifference > SEEK_THRESHOLD) { + player.seekToPresentationTime(timeToSeek); + player.setPlaybackRate(playbackRate); + lastInterval = null; + clearInterval(interval); + } + }, 10); + } + }; + + const setupCMCD = (player, config) => { + player.updateSettings({ + streaming: { + cmcd: { + enabled: true, + version: 2, + reporting: { + eventMode: { + enabled: true, + mode: CMCD_MODE_QUERY, + interval: 5000, + enabledKeys: ['sid', 'cid', 'pr', 'pt', 'ts'], + requestUrl: config.url, + requestMethod: 'POST', + } + }, + sid: config.id, + mode: CMCD_MODE_QUERY, + } + } + }); + }; + + const parseCMSDHeader = (response) => { + const cmsdHeader = response.headers.get('cmsd-dynamic'); + + if (!cmsdHeader) { + return null; + } + + const latencyMatch = cmsdHeader.match(/com\.svta-latency="([^"]+)"/); + const latencyTargetsMatch = cmsdHeader.match(/com\.svta-latency-targets="([^"]+)"/); + + const latency = latencyMatch ? Number(latencyMatch[1]) : null; + const latencyTargets = latencyTargetsMatch ? latencyTargetsMatch[1].split(',').map(Number) : null; + + return { latency, latencyTargets }; + }; + + const configInterceptors = (player, config) => { + if (config.globalSync) { + globalSyncInterceptor(player, config); + } else if (config.leaderId) { + leaderSyncInterceptor(player, config); + } + + } + + const globalSyncInterceptor = (player) => { + player.addResponseInterceptor((response) => { + if (response.cmcdMode !== 'event') { + return Promise.resolve(response); + } + + const cmsdData = parseCMSDHeader(response); + + if (cmsdData) { + const { latency, latencyTargets } = cmsdData; + if (latency) { + leaderPlayhead = player.time() + player.getCurrentLiveLatency(latency) - latency; + leaderTimestamp = Date.now(); + playbackRate = 1; + } + } + return Promise.resolve(response); + }); + } + + const leaderSyncInterceptor = (player, config) => { + player.addRequestInterceptor((request) => { + const { filteredCmcdData } = request; + if (filteredCmcdData) { + filteredCmcdData['synchronization-leader-sid'] = config.leaderId; + } + return Promise.resolve(request); + }); + + let firstRun = true; + player.addResponseInterceptor((response) => { + if (response.cmcdMode === 'event') { + response.json().then((data) => { + if (!data || !data.ts || !data.pt) { + return Promise.resolve(response); + } + const { ts, pt, pr } = data; + leaderTimestamp = Number(ts); + leaderPlayhead = Number(pt); + playbackRate = Number(pr ?? 1); + + if (firstRun) { + syncPlayer(player, config); + firstRun = false; + } + + return Promise.resolve(response); + }); + } + return Promise.resolve(response); + }); + } + + + window.playerSynchronization = { + addLeader(player, config) { + setupCMCD(player, config); + }, + addFollower(player, config) { + setupCMCD(player, config); + configInterceptors(player, config); + + setInterval(() => { + syncPlayer(player, config); + }, config.syncInterval ?? 5000); + + let seeking = false; + player.on(dashjs.MediaPlayer.events.PLAYBACK_SEEKED, () => { + if (!seeking) { + syncPlayer(player, config); + seeking = true; + } + }); + } + }; +})(); From 120a93449f17c57a320bcf94751f85af139c9f79 Mon Sep 17 00:00:00 2001 From: Sebastian Piquerez Date: Fri, 2 May 2025 15:35:40 -0300 Subject: [PATCH 2/4] Remove default video URL from global synchronization sample --- samples/players-synchronization/globalSync.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/players-synchronization/globalSync.html b/samples/players-synchronization/globalSync.html index 2ef5db2876..6ffb17f1ce 100644 --- a/samples/players-synchronization/globalSync.html +++ b/samples/players-synchronization/globalSync.html @@ -31,8 +31,6 @@

Follower client

From 82b2a3461fcdc79bcebd48766a0fdf6ca33c6972 Mon Sep 17 00:00:00 2001 From: Sebastian Piquerez Date: Tue, 6 May 2025 11:13:24 -0300 Subject: [PATCH 4/4] Use setting delay with globla sync --- .../players-synchronization/globalSync.html | 3 ++- .../playerSynchronizationPlugin.js | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/samples/players-synchronization/globalSync.html b/samples/players-synchronization/globalSync.html index a22791795e..f5e01f1bd5 100644 --- a/samples/players-synchronization/globalSync.html +++ b/samples/players-synchronization/globalSync.html @@ -67,7 +67,8 @@

Follower client

video = document.querySelector("video"); player = dashjs.MediaPlayer().create(); - + window.player = player; + player.initialize(video, '', true); playerSynchronization.addFollower(player, { globalSync: true, diff --git a/samples/players-synchronization/playerSynchronizationPlugin.js b/samples/players-synchronization/playerSynchronizationPlugin.js index f76f7b3b09..d1669ba605 100644 --- a/samples/players-synchronization/playerSynchronizationPlugin.js +++ b/samples/players-synchronization/playerSynchronizationPlugin.js @@ -116,11 +116,18 @@ let lastInterval; const cmsdData = parseCMSDHeader(response); if (cmsdData) { - const { latency, latencyTargets } = cmsdData; + const { latency } = cmsdData; if (latency) { - leaderPlayhead = player.time() + player.getCurrentLiveLatency(latency) - latency; - leaderTimestamp = Date.now(); - playbackRate = 1; + player.updateSettings({ + streaming: { + delay: { + liveDelay: latency + }, + liveCatchup: { + enabled: true, + } + } + }); } } return Promise.resolve(response); @@ -168,7 +175,9 @@ let lastInterval; addFollower(player, config) { setupCMCD(player, config); configInterceptors(player, config); - + if (config.globalSync) { + return; + } setInterval(() => { syncPlayer(player, config); }, config.syncInterval ?? 5000);