Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions samples/players-synchronization/globalSync.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title>Follower client</title>

<script src="../../dist/dash.all.debug.js"></script>
<script src="./playerSynchronizationPlugin.js"></script>

<!-- Bootstrap core CSS -->
<link href="../lib/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="../lib/main.css" rel="stylesheet" />

<style>
video {
width: 640px;
height: 360px;
}
</style>
</head>

<body>
<main>
<div class="container py-4">
<header class="pb-3 mb-4 border-bottom">
<h1>Follower client</h1>
</header>
<div class="row">
<div>
<div class="input-group mb-2">
<button id="load-button" type="button" class="btn btn-primary" aria-haspopup="true" aria-expanded="false">Load</button>
<select id="url-select" class="dropdown-toggle">
<option
value="https://livesim.dashif.org/livesim2/WAVE/vectors/cfhd_sets/12.5_25_50/t1/2022-10-17/stream.mpd">
Live Sim</option>
<option value="https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd">Low latency Live
</option>
</select>
<input id="custom-url" type="text" class="form-control" placeholder="Paste your own DASH manifest URL here" style="max-width: 350px; margin-left: 8px;">
</div>
<video controls="true"></video>
</div>
</div>
<footer class="pt-3 mt-4 text-muted border-top">
&copy; DASH-IF
</footer>
</div>
</main>
<style>
.hide-controls::-webkit-media-controls {
display: none !important;
}

.hide-controls::-moz-media-controls {
display: none !important;
}

.hide-controls::-ms-media-controls {
display: none !important;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
var video,
player

video = document.querySelector("video");
player = dashjs.MediaPlayer().create();
window.player = player;

player.initialize(video, '', true);
playerSynchronization.addFollower(player, {
globalSync: true,
url: 'http://localhost:3000/',
frameDelay: 1,
seekTreshold: 0.4,
catchUpRate: 2,
syncInterval: 5000
});

const loadButton = document.getElementById('load-button');
const urlSelect = document.getElementById('url-select');
const customUrl = document.getElementById('custom-url');

loadButton.addEventListener('click', () => {
const enteredUrl = customUrl.value.trim();
const selectedUrl = urlSelect.value;
player.attachSource(enteredUrl !== '' ? enteredUrl : selectedUrl);
});
});
</script>
</body>

</html>
194 changes: 194 additions & 0 deletions samples/players-synchronization/playerSynchronizationPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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 } = cmsdData;
if (latency) {
player.updateSettings({
streaming: {
delay: {
liveDelay: latency
},
liveCatchup: {
enabled: true,
}
}
});
}
}
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);
if (config.globalSync) {
return;
}
setInterval(() => {
syncPlayer(player, config);
}, config.syncInterval ?? 5000);

let seeking = false;
player.on(dashjs.MediaPlayer.events.PLAYBACK_SEEKED, () => {
if (!seeking) {
syncPlayer(player, config);
seeking = true;
}
});
}
};
})();