Skip to content

Commit

Permalink
Rewrite the event timing calculation and dispatching logic (#3471)
Browse files Browse the repository at this point in the history
* Rewrite the event timing calculation and dispatching logic

* Remove comment

* Fix EventController unit tests
  • Loading branch information
dsilhavy authored Dec 3, 2020
1 parent 87fae5b commit 7d4d208
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 146 deletions.
79 changes: 44 additions & 35 deletions src/dash/DashAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ function DashAdapter() {
}

if (config.errHandler) {
dashManifestModel.setConfig({errHandler: config.errHandler});
dashManifestModel.setConfig({ errHandler: config.errHandler });
}

if (config.BASE64) {
dashManifestModel.setConfig({BASE64: config.BASE64});
dashManifestModel.setConfig({ BASE64: config.BASE64 });
}
}

Expand Down Expand Up @@ -397,46 +397,55 @@ function DashAdapter() {
/**
*
* @param {object} eventBox
* @param {Array} eventStreams
* @param {number} startTime
* @param {object} eventStreams
* @param {number} mediaStartTime
* @param {object} voRepresentation
* @returns {null|Event}
* @memberOf module:DashAdapter
* @instance
* @ignore
*/
function getEvent(eventBox, eventStreams, startTime) {
if (!eventBox || !eventStreams) {
function getEvent(eventBox, eventStreams, mediaStartTime, voRepresentation) {
try {
if (!eventBox || !eventStreams || isNaN(mediaStartTime) || !voRepresentation) {
return null;
}
const event = new Event();
const schemeIdUri = eventBox.scheme_id_uri;
const value = eventBox.value;
const timescale = eventBox.timescale || 1;
const presentationTimeOffset = voRepresentation.presentationTimeOffset || 0;
const periodStart = voRepresentation.adaptation.period.start;
let presentationTimeDelta = eventBox.presentation_time_delta / timescale; // In case of version 1 events the presentation_time is parsed as presentation_time_delta
let calculatedPresentationTime;

if (eventBox.version === 0) {
calculatedPresentationTime = periodStart + mediaStartTime - presentationTimeOffset + presentationTimeDelta;
} else {
calculatedPresentationTime = periodStart - presentationTimeOffset + presentationTimeDelta;
}

const duration = eventBox.event_duration;
const id = eventBox.id;
const messageData = eventBox.message_data;

if (!eventStreams[schemeIdUri + '/' + value]) {
return null;
}

event.eventStream = eventStreams[schemeIdUri + '/' + value];
event.eventStream.value = value;
event.eventStream.timescale = timescale;
event.duration = duration;
event.id = id;
event.calculatedPresentationTime = calculatedPresentationTime;
event.messageData = messageData;
event.presentationTimeDelta = presentationTimeDelta;

return event;
} catch (e) {
return null;
}
const event = new Event();
const schemeIdUri = eventBox.scheme_id_uri;
const value = eventBox.value;
const timescale = eventBox.timescale;
let presentationTimeDelta;
let calculatedPresentationTime;
if (eventBox.version === 0) {
presentationTimeDelta = eventBox.presentation_time_delta;
calculatedPresentationTime = startTime * timescale + presentationTimeDelta;
} else {
presentationTimeDelta = 0;
calculatedPresentationTime = eventBox.presentation_time_delta;
}
const duration = eventBox.event_duration;
const id = eventBox.id;
const messageData = eventBox.message_data;

if (!eventStreams[schemeIdUri + '/' + value]) return null;

event.eventStream = eventStreams[schemeIdUri + '/' + value];
event.eventStream.value = value;
event.eventStream.timescale = timescale;
event.duration = duration;
event.id = id;
event.calculatedPresentationTime = calculatedPresentationTime;
event.messageData = messageData;
event.presentationTimeDelta = presentationTimeDelta;

return event;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/dash/DashHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ function DashHandler(config) {
request.type = HTTPRequest.MEDIA_SEGMENT_TYPE;
request.range = segment.mediaRange;
request.startTime = segment.presentationStartTime;
request.mediaStartTime = segment.mediaStartTime;
request.duration = segment.duration;
request.timescale = representation.timescale;
request.availabilityStartTime = segment.availabilityStartTime;
Expand Down
34 changes: 19 additions & 15 deletions src/dash/models/DashManifestModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -779,44 +779,48 @@ function DashManifestModel() {
eventStream.timescale = 1;

if (eventStreams[i].hasOwnProperty(Constants.SCHEME_ID_URI)) {
eventStream.schemeIdUri = eventStreams[i].schemeIdUri;
eventStream.schemeIdUri = eventStreams[i][Constants.SCHEME_ID_URI];
} else {
throw new Error('Invalid EventStream. SchemeIdUri has to be set');
}
if (eventStreams[i].hasOwnProperty(DashConstants.TIMESCALE)) {
eventStream.timescale = eventStreams[i].timescale;
eventStream.timescale = eventStreams[i][DashConstants.TIMESCALE];
}
if (eventStreams[i].hasOwnProperty(DashConstants.VALUE)) {
eventStream.value = eventStreams[i].value;
eventStream.value = eventStreams[i][DashConstants.VALUE];
}
if (eventStreams[i].hasOwnProperty(DashConstants.PRESENTATION_TIME_OFFSET)) {
eventStream.presentationTimeOffset = eventStreams[i][DashConstants.PRESENTATION_TIME_OFFSET];
}
for (j = 0; eventStreams[i].Event_asArray && j < eventStreams[i].Event_asArray.length; j++) {
const currentMpdEvent = eventStreams[i].Event_asArray[j];
const event = new Event();
event.presentationTime = 0;
event.eventStream = eventStream;

if (eventStreams[i].Event_asArray[j].hasOwnProperty(DashConstants.PRESENTATION_TIME)) {
event.presentationTime = eventStreams[i].Event_asArray[j].presentationTime;
const presentationTimeOffset = eventStream.presentationTimeOffset ? eventStream.presentationTimeOffset * eventStream.timescale : 0;
event.calculatedPresentationTime = event.presentationTime + (period.start * eventStream.timescale) + presentationTimeOffset;
if (currentMpdEvent.hasOwnProperty(DashConstants.PRESENTATION_TIME)) {
event.presentationTime = currentMpdEvent.presentationTime;
const presentationTimeOffset = eventStream.presentationTimeOffset ? eventStream.presentationTimeOffset / eventStream.timescale : 0;
event.calculatedPresentationTime = event.presentationTime / eventStream.timescale + period.start - presentationTimeOffset;
}
if (eventStreams[i].Event_asArray[j].hasOwnProperty(DashConstants.DURATION)) {
event.duration = eventStreams[i].Event_asArray[j].duration;
if (currentMpdEvent.hasOwnProperty(DashConstants.DURATION)) {
event.duration = currentMpdEvent.duration / eventStream.timescale;
}
if (eventStreams[i].Event_asArray[j].hasOwnProperty(DashConstants.ID)) {
event.id = eventStreams[i].Event_asArray[j].id;
if (currentMpdEvent.hasOwnProperty(DashConstants.ID)) {
event.id = currentMpdEvent.id;
}

if (eventStreams[i].Event_asArray[j].Signal && eventStreams[i].Event_asArray[j].Signal.Binary) {
if (currentMpdEvent.Signal && currentMpdEvent.Signal.Binary) {
// toString is used to manage both regular and namespaced tags
event.messageData = BASE64.decodeArray(eventStreams[i].Event_asArray[j].Signal.Binary.toString());
event.messageData = BASE64.decodeArray(currentMpdEvent.Signal.Binary.toString());
} else {
// From Cor.1: 'NOTE: this attribute is an alternative
// to specifying a complete XML element(s) in the Event.
// It is useful when an event leans itself to a compact
// string representation'.
event.messageData =
eventStreams[i].Event_asArray[j].messageData ||
eventStreams[i].Event_asArray[j].__text;
currentMpdEvent.messageData ||
currentMpdEvent.__text;
}

events.push(event);
Expand Down
3 changes: 2 additions & 1 deletion src/dash/vo/EventStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class EventStream {
this.timescale = 1;
this.value = '';
this.schemeIdUri = '';
this.presentationTimeOffset = 0;
}
}

export default EventStream;
export default EventStream;
44 changes: 28 additions & 16 deletions src/streaming/StreamProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,28 +538,40 @@ function StreamProcessor(config) {
}

function handleInbandEvents(data, request, mediaInbandEvents, trackInbandEvents) {
const fragmentStartTime = Math.max(!request || isNaN(request.startTime) ? 0 : request.startTime, 0);
const eventStreams = [];
const events = [];
try {
const eventStreams = {};
const events = [];

/* Extract the possible schemeIdUri : If a DASH client detects an event message box with a scheme that is not defined in MPD, the client is expected to ignore it */
const inbandEvents = mediaInbandEvents.concat(trackInbandEvents);
for (let i = 0, ln = inbandEvents.length; i < ln; i++) {
eventStreams[inbandEvents[i].schemeIdUri + '/' + inbandEvents[i].value] = inbandEvents[i];
}
/* Extract the possible schemeIdUri : If a DASH client detects an event message box with a scheme that is not defined in MPD, the client is expected to ignore it */
const inbandEvents = mediaInbandEvents.concat(trackInbandEvents);
for (let i = 0, ln = inbandEvents.length; i < ln; i++) {
eventStreams[inbandEvents[i].schemeIdUri + '/' + inbandEvents[i].value] = inbandEvents[i];
}

const isoFile = BoxParser(context).getInstance().parse(data);
const eventBoxes = isoFile.getBoxes('emsg');

if (!eventBoxes || eventBoxes.length === 0) {
return events;
}

const isoFile = BoxParser(context).getInstance().parse(data);
const eventBoxes = isoFile.getBoxes('emsg');
const sidx = isoFile.getBox('sidx');
const mediaAnchorTime = sidx && !isNaN(sidx.earliest_presentation_time) && !isNaN(sidx.timescale) ? sidx.earliest_presentation_time / sidx.timescale : request && !isNaN(request.mediaStartTime) ? request.mediaStartTime : 0;
const fragmentMediaStartTime = Math.max(mediaAnchorTime, 0);
const voRepresentation = representationController.getCurrentRepresentation();

for (let i = 0, ln = eventBoxes.length; i < ln; i++) {
const event = adapter.getEvent(eventBoxes[i], eventStreams, fragmentStartTime);
for (let i = 0, ln = eventBoxes.length; i < ln; i++) {
const event = adapter.getEvent(eventBoxes[i], eventStreams, fragmentMediaStartTime, voRepresentation);

if (event) {
events.push(event);
if (event) {
events.push(event);
}
}
}

return events;
return events;
} catch (e) {
return [];
}
}

function createBuffer(previousBuffers) {
Expand Down
49 changes: 26 additions & 23 deletions src/streaming/controllers/EventController.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@
import FactoryMaker from '../../core/FactoryMaker';
import Debug from '../../core/Debug';
import EventBus from '../../core/EventBus';
import Events from '../../core/events/Events';
import MediaPlayerEvents from '../../streaming/MediaPlayerEvents';
import XHRLoader from '../net/XHRLoader';
import {EVENT_MODE_ON_START, EVENT_MODE_ON_RECEIVE} from '../MediaPlayerEvents';

function EventController() {

Expand Down Expand Up @@ -120,9 +119,14 @@ function EventController() {
if (values) {
for (let i = 0; i < values.length; i++) {
let event = values[i];
inlineEvents[event.id] = event;
logger.debug('Add inline event with id ' + event.id);
_startEvent(event.id, event, values, EVENT_MODE_ON_RECEIVE);

// If we see the event for the first time we trigger it in onReceive mode
if (!inlineEvents[event.id]) {
_startEvent(event.id, event, values, MediaPlayerEvents.EVENT_MODE_ON_RECEIVE);
}

inlineEvents[event.id] = event;
}
}
logger.debug(`Added ${values.length} inline events`);
Expand All @@ -147,7 +151,7 @@ function EventController() {
}
inbandEvents[event.id] = event;
logger.debug('Add inband event with id ' + event.id);
_startEvent(event.id, event, values, EVENT_MODE_ON_RECEIVE);
_startEvent(event.id, event, values, MediaPlayerEvents.EVENT_MODE_ON_RECEIVE);
} else {
logger.debug('Repeated event with id ' + event.id);
}
Expand All @@ -161,22 +165,21 @@ function EventController() {
function _handleManifestReloadEvent(event) {
try {
if (event.eventStream.value == MPD_RELOAD_VALUE) {
const timescale = event.eventStream.timescale || 1;
const validUntil = event.calculatedPresentationTime / timescale;
const validUntil = event.calculatedPresentationTime;
let newDuration;
if (event.calculatedPresentationTime == 0xFFFFFFFF) {//0xFF... means remaining duration unknown
newDuration = NaN;
} else {
newDuration = (event.calculatedPresentationTime + event.duration) / timescale;
newDuration = event.calculatedPresentationTime + event.duration;
}
logger.info('Manifest validity changed: Valid until: ' + validUntil + '; remaining duration: ' + newDuration);
eventBus.trigger(Events.MANIFEST_VALIDITY_CHANGED, {
//logger.info('Manifest validity changed: Valid until: ' + validUntil + '; remaining duration: ' + newDuration);
eventBus.trigger(MediaPlayerEvents.MANIFEST_VALIDITY_CHANGED, {
id: event.id,
validUntil: validUntil,
newDuration: newDuration,
newManifestValidAfter: NaN //event.message_data - this is an arraybuffer with a timestring in it, but not used yet
}, {
mode: EVENT_MODE_ON_START
mode: MediaPlayerEvents.EVENT_MODE_ON_START
});
}
} catch (e) {
Expand All @@ -195,7 +198,7 @@ function EventController() {
for (let i = 0; i < eventIds.length; i++) {
let eventId = eventIds[i];
let event = activeEvents[eventId];
if (event !== null && (event.duration + event.calculatedPresentationTime) / event.eventStream.timescale < currentVideoTime) {
if (event !== null && event.duration + event.calculatedPresentationTime < currentVideoTime) {
logger.debug('Remove Event ' + eventId + ' at time ' + currentVideoTime);
event = null;
delete activeEvents[eventId];
Expand Down Expand Up @@ -251,11 +254,11 @@ function EventController() {
let event = events[eventId];

if (event !== undefined) {
const calculatedPresentationTimeInSeconds = event.calculatedPresentationTime / event.eventStream.timescale;

if (calculatedPresentationTimeInSeconds <= currentVideoTime && calculatedPresentationTimeInSeconds + presentationTimeThreshold >= currentVideoTime) {
_startEvent(eventId, event, events, EVENT_MODE_ON_START);
} else if (_eventHasExpired(currentVideoTime, presentationTimeThreshold, calculatedPresentationTimeInSeconds) || _eventIsInvalid(event)) {
const duration = !isNaN(event.duration) ? event.duration : 0;
// The event is either about to start or has already been started and we are within its duration
if ((event.calculatedPresentationTime <= currentVideoTime && event.calculatedPresentationTime + presentationTimeThreshold + duration >= currentVideoTime)) {
_startEvent(eventId, event, events, MediaPlayerEvents.EVENT_MODE_ON_START);
} else if (_eventHasExpired(currentVideoTime, duration + presentationTimeThreshold, event.calculatedPresentationTime) || _eventIsInvalid(event)) {
logger.debug(`Deleting event ${eventId} as it is expired or invalid`);
delete events[eventId];
}
Expand All @@ -266,9 +269,9 @@ function EventController() {
}
}

function _eventHasExpired(currentVideoTime, presentationTimeThreshold, calculatedPresentationTimeInSeconds) {
function _eventHasExpired(currentVideoTime, threshold, calculatedPresentationTimeInSeconds) {
try {
return currentVideoTime - presentationTimeThreshold > calculatedPresentationTimeInSeconds;
return currentVideoTime - threshold > calculatedPresentationTimeInSeconds;
} catch (e) {
return false;
}
Expand All @@ -278,7 +281,7 @@ function EventController() {
try {
const periodEndTime = event.eventStream.period.start + event.eventStream.period.duration;

return event.calculatedPresentationTime / 1000 > periodEndTime;
return event.calculatedPresentationTime > periodEndTime;
} catch (e) {
return false;
}
Expand All @@ -302,10 +305,10 @@ function EventController() {

eventIds.forEach((eventId) => {
const event = events[eventId];
const calculatedPresentationTimeInSeconds = event.calculatedPresentationTime / event.eventStream.timescale;
const calculatedPresentationTimeInSeconds = event.calculatedPresentationTime;

if (Math.abs(calculatedPresentationTimeInSeconds - currentTime) < REMAINING_EVENTS_THRESHOLD) {
_startEvent(eventId, event, events, EVENT_MODE_ON_START);
_startEvent(eventId, event, events, MediaPlayerEvents.EVENT_MODE_ON_START);
}
});
} catch (e) {
Expand All @@ -317,7 +320,7 @@ function EventController() {
try {
const currentVideoTime = playbackController.getTime();

if (mode === EVENT_MODE_ON_RECEIVE) {
if (mode === MediaPlayerEvents.EVENT_MODE_ON_RECEIVE) {
logger.debug(`Received event ${eventId}`);
eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode });
return;
Expand Down
1 change: 1 addition & 0 deletions src/streaming/vo/FragmentRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class FragmentRequest {
constructor(url) {
this.action = FragmentRequest.ACTION_DOWNLOAD;
this.startTime = NaN;
this.mediaStartTime = NaN;
this.mediaType = null;
this.mediaInfo = null;
this.type = null;
Expand Down
Loading

0 comments on commit 7d4d208

Please sign in to comment.