Skip to content

Commit 007f727

Browse files
committed
alternative fixes for timing issues and cmcd reporting for vast events
1 parent 371ee99 commit 007f727

8 files changed

Lines changed: 122 additions & 14 deletions

File tree

samples/alternative/alternative-media-presentations.html

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ <h5>VOD Configuration</h5>
100100

101101
<div class="mb-3">
102102
<label class="form-label">Alternative VOD URL:</label>
103-
<input type="text" id="alternativeUrl" class="form-control" value="https://dash.akamaized.net/akamai/tos/tears-blend-no-steering.mpd" />
103+
<input type="text" id="alternativeUrl" class="form-control" value="http://localhost:3000/api/list-mpd?vasturl=http://localhost:3000/samples/dash-alt-mpd/vast-sample.xml" />
104104
</div>
105105

106106
<div class="event-type-selector">
@@ -422,6 +422,29 @@ <h6>Generated Manifest Events:</h6>
422422
// Load without events - use attachSource with the URL
423423
player.attachSource(manifestUrl);
424424
}
425+
426+
player.on("alternativeContentReady", function(e) {
427+
console.log(">>> Alternative content READY at:", performance.now());
428+
const altPlayer = e.player;
429+
if (altPlayer) {
430+
const reporter = altPlayer.getCmcdReporter();
431+
console.log(">>> Reporter:", reporter);
432+
433+
altPlayer.on("urn:mpeg:dash:event:callback:2015", function(callbackEvent, extra) {
434+
const eventType = callbackEvent.event.id;
435+
console.log(">>> Callback event received:", eventType, "mode:", extra?.mode, "at:", performance.now());
436+
437+
if (reporter) {
438+
reporter.recordEvent("ce", { cen: `callback-event-${eventType}` });
439+
} else {
440+
console.log(">>> Reporter is null");
441+
}
442+
});
443+
} else {
444+
console.log("AltPlayer not found")
445+
}
446+
});
447+
425448
}
426449

427450
document.addEventListener('DOMContentLoaded', function () {

src/dash/models/DashManifestModel.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1350,9 +1350,12 @@ function DashManifestModel() {
13501350
event.duration = currentMpdEvent.duration / eventStream.timescale;
13511351
}
13521352
if (currentMpdEvent.hasOwnProperty(DashConstants.ID)) {
1353-
event.id = parseInt(currentMpdEvent.id);
1353+
// Preserve original ID (can be string or number) and also store parsed integer
1354+
event.id = currentMpdEvent.id;
1355+
event.idInt = parseInt(currentMpdEvent.id);
13541356
} else {
13551357
event.id = null;
1358+
event.idInt = null;
13561359
}
13571360
if (currentMpdEvent.hasOwnProperty(DashConstants.STATUS)) {
13581361
event.status = currentMpdEvent.status;
@@ -1378,9 +1381,11 @@ function DashManifestModel() {
13781381
// to specifying a complete XML element(s) in the Event.
13791382
// It is useful when an event leans itself to a compact
13801383
// string representation'.
1384+
// Support both __cdata (legacy) and #cdata (current XML parser format)
13811385
event.messageData =
13821386
currentMpdEvent.messageData ||
13831387
currentMpdEvent.__cdata ||
1388+
currentMpdEvent['#cdata']?.nodeValue ||
13841389
currentMpdEvent.__text;
13851390
}
13861391

src/streaming/MediaManager.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ function MediaManager() {
148148
}
149149
}
150150

151-
function initializeAlternativePlayer(alternativeMpdUrl) {
151+
function createAlternativePlayer() {
152152
if (altPlayer) {
153153
altPlayer.off(Events.ERROR, onAlternativePlayerError, this);
154154
}
@@ -158,10 +158,16 @@ function MediaManager() {
158158
cacheInitSegments: true
159159
}
160160
});
161-
altPlayer.initialize(null, alternativeMpdUrl, false, NaN);
162-
altPlayer.preload();
163161
altPlayer.setAutoPlay(false);
164162
altPlayer.on(Events.ERROR, onAlternativePlayerError, this);
163+
return altPlayer;
164+
}
165+
166+
function loadAlternativeManifest(alternativeMpdUrl) {
167+
if (altPlayer) {
168+
altPlayer.initialize(null, alternativeMpdUrl, false, NaN);
169+
altPlayer.preload();
170+
}
165171
}
166172

167173
function onAlternativePlayerError(e) {
@@ -170,7 +176,7 @@ function MediaManager() {
170176
}
171177
}
172178

173-
function switchToAlternativeContent(playerId, alternativeMpdUrl, time = 0) {
179+
function switchToAlternativeContent(playerId, alternativeMpdUrl, time = 0, onReady = null) {
174180
if (isSwitching) {
175181
logger.debug('Switch already in progress - ignoring request');
176182
return
@@ -185,8 +191,23 @@ function MediaManager() {
185191
logger.info(`Using prebuffered content for player ${playerId}`);
186192
altPlayer = prebufferedContent.player;
187193
prebufferedPlayers.delete(playerId);
194+
195+
// Call onReady callback - for prebuffered, manifest is already loaded
196+
if (onReady && typeof onReady === 'function') {
197+
onReady(altPlayer);
198+
}
188199
} else {
189-
initializeAlternativePlayer(alternativeMpdUrl);
200+
// Create player first WITHOUT loading manifest
201+
createAlternativePlayer();
202+
203+
// Call onReady callback BEFORE loading manifest
204+
// This allows listeners to be registered before ON_RECEIVE events fire
205+
if (onReady && typeof onReady === 'function') {
206+
onReady(altPlayer);
207+
}
208+
209+
// Now load the manifest - this will trigger ON_RECEIVE events
210+
loadAlternativeManifest(alternativeMpdUrl);
190211
}
191212

192213
if (!altVideoElement) {

src/streaming/MediaPlayer.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,18 @@ function MediaPlayer() {
13721372
return dashMetrics;
13731373
}
13741374

1375+
/**
1376+
* Returns the CmcdReporter instance for external access.
1377+
* This allows custom event recording and data updates from outside the player.
1378+
*
1379+
* @returns {CmcdReporter|null} The CmcdReporter instance or null if not initialized
1380+
* @memberof module:MediaPlayer
1381+
* @instance
1382+
*/
1383+
function getCmcdReporter() {
1384+
return cmcdController ? cmcdController.getReporter() : null;
1385+
}
1386+
13751387
/*
13761388
---------------------------------------------------------------------------
13771389
@@ -2908,6 +2920,7 @@ function MediaPlayer() {
29082920
getCurrentSteeringResponseData,
29092921
getCurrentTextTrackIndex,
29102922
getCurrentTrackFor,
2923+
getCmcdReporter,
29112924
getDashAdapter,
29122925
getDashMetrics,
29132926
getDebug,

src/streaming/constants/Constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ export default {
427427
NO_JUMP_DEFAULT: 1,
428428
NO_JUMP_PRIORITY: 2
429429
},
430+
CONTENT_READY: 'alternativeContentReady',
430431
CONTENT_START: 'alternativeContentStart',
431432
CONTENT_END: 'alternativeContentEnd',
432433
EVENT_UPDATED: 'alternativeEventUpdated'

src/streaming/controllers/AlternativeMediaController.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,15 +196,27 @@ function AlternativeMediaController() {
196196
actualEventPresentationTime = playbackController.getTime();
197197
timeToSwitch = parsedEvent.startWithOffset ? actualEventPresentationTime - parsedEvent.presentationTime : 0;
198198
timeToSwitch = timeToSwitch + _getAnchor(parsedEvent.alternativeMPD.url);
199+
200+
// Callback to trigger CONTENT_READY when player is ready but before playback starts
201+
const onPlayerReady = (altPlayer) => {
202+
if (eventBus) {
203+
eventBus.trigger(Constants.ALTERNATIVE_MPD.CONTENT_READY, {
204+
event: parsedEvent,
205+
player: altPlayer
206+
});
207+
}
208+
};
209+
199210
mediaManager.switchToAlternativeContent(
200211
parsedEvent.id,
201212
parsedEvent.alternativeMPD.url,
202-
timeToSwitch
213+
timeToSwitch,
214+
onPlayerReady
203215
);
204-
216+
205217
// Trigger content start event
206218
if (eventBus){
207-
eventBus.trigger(Constants.ALTERNATIVE_MPD.CONTENT_START, {
219+
eventBus.trigger(Constants.ALTERNATIVE_MPD.CONTENT_START, {
208220
event: parsedEvent,
209221
player: mediaManager.getAlternativePlayer()
210222
});

src/streaming/controllers/CmcdController.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,15 @@ function CmcdController() {
580580
return cmcdModel.getCmcdParametersFromManifest();
581581
}
582582

583+
/**
584+
* Returns the CmcdReporter instance for external access.
585+
* This allows custom event recording and data updates from outside the player.
586+
* @returns {CmcdReporter|null} The CmcdReporter instance or null if not initialized
587+
*/
588+
function getReporter() {
589+
return cmcdReporter;
590+
}
591+
583592
function reset() {
584593
eventBus.off(MediaPlayerEvents.PLAYBACK_RATE_CHANGED, _onPlaybackRateChanged, this);
585594
eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this);
@@ -605,6 +614,7 @@ function CmcdController() {
605614
getCmcdRequestInterceptors,
606615
getCmcdResponseInterceptors,
607616
getCmcdParametersFromManifest,
617+
getReporter,
608618
initialize,
609619
isCmcdEnabled,
610620
reset,

src/streaming/controllers/EventController.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,18 @@ function EventController() {
185185
*/
186186
function _triggerEvents(events, presentationTimeThreshold, currentVideoTime) {
187187
try {
188+
// Skip event processing if currentVideoTime is null or undefined (playback hasn't started)
189+
if (currentVideoTime === null || currentVideoTime === undefined) {
190+
return;
191+
}
192+
188193
const callback = function (event, currentPeriodEvents) {
189194
if (event !== undefined) {
190195
const duration = !isNaN(event.duration) ? event.duration : 0;
191196
const isRetriggerable = _isRetriggerable(event);
192197
const hasNoJump = _hasNoJumpValue(event);
193198
const hasExecuteOnce = _hasExecuteOnceValue(event);
194-
199+
195200
// Check if event is ready to resolve (earliestResolutionTimeOffset feature)
196201
if (_checkEventReadyToResolve(event, currentVideoTime)) {
197202
_triggerEventReadyToResolve(event);
@@ -200,12 +205,20 @@ function EventController() {
200205
if (isRetriggerable && _canEventRetrigger(event, currentVideoTime, presentationTimeThreshold, hasExecuteOnce)) {
201206
event.triggeredStartEvent = false;
202207
}
203-
208+
204209
// Handle noJump events first - these ignore duration and trigger when skipping ahead
205210
if (hasNoJump && _shouldTriggerNoJumpEvent(event, currentVideoTime, currentPeriodEvents)) {
206211
event.triggeredNoJumpEvent = true;
207212
_startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_START);
208213
}
214+
// Special handling for presentationTime=0 events - fire ON_START immediately when playback starts
215+
// These events semantically mean "fire at the start of content" and should trigger as soon as playback begins
216+
else if (event.calculatedPresentationTime === 0 && currentVideoTime > 0) {
217+
_startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_START);
218+
if (hasNoJump) {
219+
event.triggeredNoJumpEvent = true;
220+
}
221+
}
209222
// Handle regular events - these check duration and timing
210223
else if (event.calculatedPresentationTime <= currentVideoTime && event.calculatedPresentationTime + presentationTimeThreshold + duration >= currentVideoTime) {
211224
_startEvent(event, MediaPlayerEvents.EVENT_MODE_ON_START);
@@ -267,7 +280,9 @@ function EventController() {
267280
let event = values[i];
268281
const currentTime = playbackController.getTime();
269282
const duration = !isNaN(event.duration) ? event.duration : 0;
270-
if (!_eventHasExpired(currentTime, duration, event.calculatedPresentationTime)) {
283+
const isExpired = _eventHasExpired(currentTime, duration, event.calculatedPresentationTime, false, true);
284+
285+
if (!isExpired) {
271286
let result = _addOrUpdateEvent(event, inlineEvents[periodId], true);
272287

273288
if (result === EVENT_HANDLED_STATES.ADDED) {
@@ -812,15 +827,21 @@ function EventController() {
812827
* @param {number} threshold
813828
* @param {number} calculatedPresentationTimeInSeconds
814829
* @param {boolean} isRetriggerable
830+
* @param {boolean} isInitialCheck - Whether this is the initial check during addInlineEvents
815831
* @return {boolean}
816832
* @private
817833
*/
818-
function _eventHasExpired(currentVideoTime, threshold, calculatedPresentationTimeInSeconds, isRetriggerable = false) {
834+
function _eventHasExpired(currentVideoTime, threshold, calculatedPresentationTimeInSeconds, isRetriggerable = false, isInitialCheck = false) {
819835
try {
820836
// Retriggerables events don't expire in the traditional sense
821837
if (isRetriggerable) {
822838
return false;
823839
}
840+
// Events at presentationTime=0 should not be considered expired during the initial check
841+
// because they semantically mean "fire at the start of content" and should always be processed
842+
if (isInitialCheck && calculatedPresentationTimeInSeconds === 0) {
843+
return false;
844+
}
824845
return currentVideoTime - threshold > calculatedPresentationTimeInSeconds;
825846
} catch (e) {
826847
logger.error(e);
@@ -876,6 +897,8 @@ function EventController() {
876897
logger.debug(`Starting callback event ${eventId} at ${currentVideoTime}`);
877898
const url = event.messageData instanceof Uint8Array ? Utils.uint8ArrayToString(event.messageData) : event.messageData;
878899
_sendCallbackRequest(url);
900+
// Also dispatch the callback event so external listeners can capture it (e.g., for CMCD reporting)
901+
eventBus.trigger(event.eventStream.schemeIdUri, { event }, { mode });
879902
} else {
880903
logger.debug(`Starting event ${eventId} from period ${event.eventStream.period.id} at ${currentVideoTime}`);
881904
eventBus.trigger(event.eventStream.schemeIdUri, { event }, { mode });

0 commit comments

Comments
 (0)