diff --git a/samples/alternative-demo/demo.html b/samples/alternative-demo/demo.html
new file mode 100644
index 0000000000..46cbe62c0c
--- /dev/null
+++ b/samples/alternative-demo/demo.html
@@ -0,0 +1,467 @@
+
+
+
+
+ Alternative Config
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Type
+
Alternative Url
+
Presentation Time (ms)
+
Duration (ms)
+
Earliest Resolution Time Offset (ms)
+
Max Duration (ms)
+
Return Offset (ms)
+
Clip
+
Start At Playhead
+
+
+
+
+
+
+
+
+
Manifest will appear here...
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/develop.html b/samples/develop.html
index e3ec0e9dac..1d42a97e8f 100644
--- a/samples/develop.html
+++ b/samples/develop.html
@@ -43,15 +43,69 @@
}
diff --git a/samples/players-synchronization/followerClient.html b/samples/players-synchronization/followerClient.html
new file mode 100644
index 0000000000..08f581f429
--- /dev/null
+++ b/samples/players-synchronization/followerClient.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+ Follower client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/players-synchronization/globalSync.html b/samples/players-synchronization/globalSync.html
new file mode 100644
index 0000000000..5453c0596e
--- /dev/null
+++ b/samples/players-synchronization/globalSync.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+ Follower client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/players-synchronization/leaderClient.html b/samples/players-synchronization/leaderClient.html
new file mode 100644
index 0000000000..aedcc4ceaa
--- /dev/null
+++ b/samples/players-synchronization/leaderClient.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+ Leader client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..30b1f7379b
--- /dev/null
+++ b/samples/players-synchronization/playerSynchronizationPlugin.js
@@ -0,0 +1,179 @@
+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) => {
+ console.log('CMSD Header:', response);
+ // const cmsdHeader = response.headers('CMSD-Dynamic');
+ // if (!cmsdHeader) {
+ // return null;
+ // }
+
+ // const match = cmsdHeader.match(/com\.svta-syncinfo="([^"]+)"/);
+ // if (!match || !match[1]) {
+ // return null;
+ // }
+
+ // const [latencyTarget, referencePlayhead, referenceTimestamp] = match[1].split(',').map(Number);
+ // return { latencyTarget, referencePlayhead, referenceTimestamp };
+ };
+
+ 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) {
+ console.log('CMSD Data:', cmsdData)
+ }
+ 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;
+ }
+ });
+ }
+ };
+})();
diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js
index c0d88e3937..d5c0f337f2 100644
--- a/src/streaming/constants/Constants.js
+++ b/src/streaming/constants/Constants.js
@@ -229,7 +229,7 @@ export default {
* TODO: Confirm keys and create CMCD AVAILABLE KEYS arrays for CMCD v2 and Response Mode
* TODO: Add support for more keys
*/
- CMCD_AVAILABLE_KEYS_EVENT: ['bg', 'br', 'bs', 'bl', 'mtp', 'sf', 'e', 'int', 'sta', 'ts', 'sid', 'cid', 'sf', 'v', 'lb', 'pr', 'ltc', 'msd'],
+ CMCD_AVAILABLE_KEYS_EVENT: ['bg', 'br', 'bs', 'bl', 'mtp', 'sf', 'e', 'int', 'sta', 'ts', 'sid', 'cid', 'sf', 'v', 'lb', 'pr', 'ltc', 'msd', 'pt'],
/**
* @constant {string} CMCD_AVAILABLE_REQUESTS specifies all the availables requests type for CMCD metrics.
diff --git a/src/streaming/models/CmcdModel.js b/src/streaming/models/CmcdModel.js
index 4143921ecf..2c9ffe1590 100644
--- a/src/streaming/models/CmcdModel.js
+++ b/src/streaming/models/CmcdModel.js
@@ -44,6 +44,7 @@ import {CmcdStreamType} from '@svta/common-media-library/cmcd/CmcdStreamType';
import {CmcdStreamingFormat} from '@svta/common-media-library/cmcd/CmcdStreamingFormat';
import {encodeCmcd} from '@svta/common-media-library/cmcd/encodeCmcd';
import {toCmcdHeaders} from '@svta/common-media-library/cmcd/toCmcdHeaders';
+import CustomParametersModel from '../models/CustomParametersModel.js';
const CMCD_VERSION = 1;
const DEFAULT_INCLUDE_IN_REQUESTS = 'segment';
@@ -61,6 +62,7 @@ function CmcdModel() {
serviceDescriptionController,
throughputController,
streamProcessors,
+ customParametersModel,
_eventTimeoutId,
_msdSent,
_lastMediaTypeRequest,
@@ -77,6 +79,7 @@ function CmcdModel() {
function setup() {
dashManifestModel = DashManifestModel(context).getInstance();
logger = debug.getLogger(instance);
+ customParametersModel = CustomParametersModel(context).getInstance();
_resetInitialSettings();
}
@@ -97,6 +100,7 @@ function CmcdModel() {
eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance);
eventBus.on(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_PLAYING, _onAlternativeStarted, instance);
eventBus.on(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_ENDED, _onAlternativeEnded, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance);
const cmcdEventMode = _getCmcdEventData();
if (cmcdEventMode){
@@ -236,43 +240,85 @@ function CmcdModel() {
//_sendCmcdEventData(_getCmcdEventData(),'c')
}
+ function _onPlaybackTimeUpdated(data) {
+ // Revisar pt
+ internalData.pt = data.time;
+ internalData.ts = Date.now();
+ }
+
function _sendCmcdEventData(cmcdEventMode, eventKeyValue = null) {
const cmcdData = _getGenericCmcdData(null);
+ var requestUrl = cmcdEventMode.requestUrl;
+ var headers = {}
// Add the event key data.
cmcdData.e = eventKeyValue
const filteredCmcdData = _applyWhitelist(cmcdData, 3);
-
- var requestUrl = cmcdEventMode.requestUrl;
- var headers = {}
-
- if (cmcdEventMode.mode === Constants.CMCD_MODE_QUERY) {
- const additionalQueryParameter = [];
- const cmcdQueryParams = encodeCmcd(filteredCmcdData);
- if (cmcdQueryParams) {
- additionalQueryParameter.push({key: CMCD_PARAM, value: cmcdQueryParams});
+ _applyRequestInterceptors({
+ url: requestUrl,
+ method: cmcdEventMode.requestMethod,
+ headers,
+ filteredCmcdData
+ }).then(request => {
+ if (cmcdEventMode.mode === Constants.CMCD_MODE_QUERY) {
+ const cmcdQueryParams = encodeCmcd(filteredCmcdData);
+ if (cmcdQueryParams) {
+ request.url = Utils.addAdditionalQueryParameterToUrl(request.url, [
+ { key: CMCD_PARAM, value: cmcdQueryParams }
+ ]);
+ }
+ } else if (cmcdEventMode.mode === Constants.CMCD_MODE_HEADER) {
+ request.headers = toCmcdHeaders(filteredCmcdData);
}
- requestUrl = Utils.addAdditionalQueryParameterToUrl(requestUrl, additionalQueryParameter);
- } else if (cmcdEventMode.mode === Constants.CMCD_MODE_HEADER) {
- headers = toCmcdHeaders(filteredCmcdData)
- }
- fetch(requestUrl, {
- method: cmcdEventMode.requestMethod,
- headers: headers
+ return fetch(request.url, {
+ method: request.method,
+ headers: request.headers
+ });
}).then(response => {
- console.log('Event CMCD data sent successfully:', response);
+ response.cmcdMode = 'event';
+ return _applyResponseInterceptors(response);
}).catch(error => {
console.error('Error sending event CMCD data:', error);
});
+
+ }
+
+ function _applyRequestInterceptors(httpRequest) {
+ const interceptors = customParametersModel.getRequestInterceptors();
+ if (!interceptors) {
+ return Promise.resolve(httpRequest);
+ }
+
+ return interceptors.reduce((prev, next) => {
+ return prev.then((request) => {
+ return next(request);
+ });
+ }, Promise.resolve(httpRequest));
+ }
+
+ function _applyResponseInterceptors(response) {
+ const interceptors = customParametersModel.getResponseInterceptors();
+ if (!interceptors) {
+ return Promise.resolve(response);
+ }
+
+ return interceptors.reduce((prev, next) => {
+ return prev.then(resp => {
+ return next(resp);
+ });
+ }, Promise.resolve(response));
}
function _startCmcdEventTimer(interval, eventMode) {
- _eventTimeoutId = setTimeout(() => {
+ setInterval(() => {
_sendCmcdEventData(eventMode, 't')
- // Restart the timer
- _startCmcdEventTimer(interval, eventMode);
- }, interval);
+ }, interval);
+ // _eventTimeoutId = setTimeout(() => {
+ // _sendCmcdEventData(eventMode, 't')
+ // // Restart the timer
+ // _startCmcdEventTimer(interval, eventMode);
+ // }, interval);
}
function _getCmcdEventData() {
@@ -749,12 +795,14 @@ function CmcdModel() {
data.sf = internalData.sf;
}
+ data.pt = internalData.pt ?? 0;
+
// Add v2 mandatory keys
if (request && internalData.v === 2) {
data.url = request.url.split('?')[0]; // remove potential cmcd query params
}
if (internalData.v === 2) {
- data.ts = Date.now();
+ data.ts = internalData.ts;
}
if (internalData.bg){
data.bg = internalData.bg
@@ -941,6 +989,7 @@ function CmcdModel() {
eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance);
eventBus.off(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_PLAYING, _onAlternativeStarted, instance);
eventBus.off(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_ENDED, _onAlternativeEnded, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onPlaybackTimeUpdated, instance);
_resetInitialSettings();
}