Skip to content

Commit

Permalink
perf: Improve performance when parsing EMSG (#7557)
Browse files Browse the repository at this point in the history
With the change, we reuse MediaSource's MP4 parsing code to avoid
parsing everything twice.
It will also help with the implementation of
#7556 in the future.
  • Loading branch information
avelad authored Nov 12, 2024
1 parent 3f9dec2 commit cb66f47
Show file tree
Hide file tree
Showing 12 changed files with 477 additions and 472 deletions.
6 changes: 3 additions & 3 deletions demo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,6 @@ shakaDemo.Config = class {
.addNumberInput_('Update interval seconds',
'streaming.updateIntervalSeconds',
/* canBeDecimal= */ true)
.addBoolInput_('Dispatch all emsg boxes',
'streaming.dispatchAllEmsgBoxes')
.addBoolInput_('Observe media quality changes',
'streaming.observeQualityChanges')
.addNumberInput_('Max Variant Disabled Time',
Expand Down Expand Up @@ -645,7 +643,9 @@ shakaDemo.Config = class {
'Codec Switching Strategy',
'mediaSource.codecSwitchingStrategy',
strategyOptions,
strategyOptionsNames);
strategyOptionsNames)
.addBoolInput_('Dispatch all emsg boxes',
'mediaSource.dispatchAllEmsgBoxes');
}

/** @private */
Expand Down
12 changes: 6 additions & 6 deletions externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1557,7 +1557,6 @@ shaka.extern.LiveSyncConfiguration;
* minBytesForProgressEvents: number,
* preferNativeHls: boolean,
* updateIntervalSeconds: number,
* dispatchAllEmsgBoxes: boolean,
* observeQualityChanges: boolean,
* maxDisabledTime: number,
* segmentPrefetchLimit: number,
Expand Down Expand Up @@ -1739,10 +1738,6 @@ shaka.extern.LiveSyncConfiguration;
* The minimum number of seconds to see if the manifest has changes.
* <br>
* Defaults to <code>1</code>.
* @property {boolean} dispatchAllEmsgBoxes
* If true, all emsg boxes are parsed and dispatched.
* <br>
* Defaults to <code>false</code>.
* @property {boolean} observeQualityChanges
* If true, monitor media quality changes and emit
* <code>shaka.Player.MediaQualityChangedEvent</code>.
Expand Down Expand Up @@ -1850,7 +1845,8 @@ shaka.extern.StreamingConfiguration;
* addExtraFeaturesToSourceBuffer: function(string): string,
* forceTransmux: boolean,
* insertFakeEncryptionInInit: boolean,
* modifyCueCallback: shaka.extern.TextParser.ModifyCueCallback
* modifyCueCallback: shaka.extern.TextParser.ModifyCueCallback,
* dispatchAllEmsgBoxes: boolean
* }}
*
* @description
Expand Down Expand Up @@ -1889,6 +1885,10 @@ shaka.extern.StreamingConfiguration;
* A callback called for each cue after it is parsed, but right before it
* is appended to the presentation.
* Gives a chance for client-side editing of cue text, cue timing, etc.
* @property {boolean} dispatchAllEmsgBoxes
* If true, all emsg boxes are parsed and dispatched.
* <br>
* Defaults to <code>false</code>.
* @exportDoc
*/
shaka.extern.MediaSourceConfiguration;
Expand Down
159 changes: 148 additions & 11 deletions lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -859,15 +859,18 @@ shaka.media.MediaSourceEngine = class {
}

/**
* This method is only public for testing.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {!BufferSource} data
* @param {?shaka.media.SegmentReference} reference The segment reference
* we are appending, or null for init segments
* @param {!shaka.media.SegmentReference} reference The segment reference
* we are appending
* @param {shaka.extern.Stream} stream
* @param {!string} mimeType
* @return {{timestamp: ?number, metadata: !Array.<shaka.extern.ID3Metadata>}}
* @private
*/
getTimestampAndDispatchMetadata_(contentType, data, reference, mimeType) {
getTimestampAndDispatchMetadata(contentType, data, reference, stream,
mimeType) {
let timestamp = null;
let metadata = [];

Expand All @@ -894,16 +897,23 @@ shaka.media.MediaSourceEngine = class {
[id3Metadata], /* offset= */ 0, reference.endTime);
}
} else if (mimeType.includes('/mp4') &&
reference && reference.timestampOffset == 0 &&
reference &&
reference.initSegmentReference &&
reference.initSegmentReference.timescale) {
const timescale = reference.initSegmentReference.timescale;
if (!isNaN(timescale)) {
const hasEmsg = ((stream.emsgSchemeIdUris != null &&
stream.emsgSchemeIdUris.length > 0) ||
this.config_.dispatchAllEmsgBoxes);
const Mp4Parser = shaka.util.Mp4Parser;
let startTime = 0;
let parsedMedia = false;
new Mp4Parser()
.fullBox('prft', (box) => this.parsePrft_(timescale, box))
const parser = new Mp4Parser();
if (hasEmsg) {
parser.fullBox('emsg', (box) =>
this.parseEMSG_(reference, stream.emsgSchemeIdUris, box));
}
parser.fullBox('prft', (box) => this.parsePrft_(timescale, box))
.box('moof', Mp4Parser.children)
.box('traf', Mp4Parser.children)
.fullBox('tfdt', (box) => {
Expand All @@ -916,7 +926,7 @@ shaka.media.MediaSourceEngine = class {
parsedMedia = true;
box.parser.stop();
}).parse(data, /* partialOkay= */ true);
if (parsedMedia) {
if (parsedMedia && reference.timestampOffset == 0) {
timestamp = startTime;
}
}
Expand All @@ -937,6 +947,130 @@ shaka.media.MediaSourceEngine = class {
return {timestamp, metadata};
}


/**
* Parse the EMSG box from a MP4 container.
*
* @param {!shaka.media.SegmentReference} reference
* @param {?Array.<string>} emsgSchemeIdUris Array of emsg
* scheme_id_uri for which emsg boxes should be parsed.
* @param {!shaka.extern.ParsedBox} box
* @private
* https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format
* aligned(8) class DASHEventMessageBox
* extends FullBox(‘emsg’, version, flags = 0){
* if (version==0) {
* string scheme_id_uri;
* string value;
* unsigned int(32) timescale;
* unsigned int(32) presentation_time_delta;
* unsigned int(32) event_duration;
* unsigned int(32) id;
* } else if (version==1) {
* unsigned int(32) timescale;
* unsigned int(64) presentation_time;
* unsigned int(32) event_duration;
* unsigned int(32) id;
* string scheme_id_uri;
* string value;
* }
* unsigned int(8) message_data[];
*/
parseEMSG_(reference, emsgSchemeIdUris, box) {
let timescale;
let id;
let eventDuration;
let schemeId;
let startTime;
let presentationTimeDelta;
let value;

if (box.version === 0) {
schemeId = box.reader.readTerminatedString();
value = box.reader.readTerminatedString();
timescale = box.reader.readUint32();
presentationTimeDelta = box.reader.readUint32();
eventDuration = box.reader.readUint32();
id = box.reader.readUint32();
startTime = reference.startTime + (presentationTimeDelta / timescale);
} else {
timescale = box.reader.readUint32();
const pts = box.reader.readUint64();
startTime = (pts / timescale) + reference.timestampOffset;
presentationTimeDelta = startTime - reference.startTime;
eventDuration = box.reader.readUint32();
id = box.reader.readUint32();
schemeId = box.reader.readTerminatedString();
value = box.reader.readTerminatedString();
}
const messageData = box.reader.readBytes(
box.reader.getLength() - box.reader.getPosition());

// See DASH sec. 5.10.3.3.1
// 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.
if ((emsgSchemeIdUris && emsgSchemeIdUris.includes(schemeId)) ||
this.config_.dispatchAllEmsgBoxes) {
// See DASH sec. 5.10.4.1
// A special scheme in DASH used to signal manifest updates.
if (schemeId == 'urn:mpeg:dash:event:2012') {
this.playerInterface_.onManifestUpdate();
} else {
// All other schemes are dispatched as a general 'emsg' event.
const endTime = startTime + (eventDuration / timescale);
/** @type {shaka.extern.EmsgInfo} */
const emsg = {
startTime: startTime,
endTime: endTime,
schemeIdUri: schemeId,
value: value,
timescale: timescale,
presentationTimeDelta: presentationTimeDelta,
eventDuration: eventDuration,
id: id,
messageData: messageData,
};

// Dispatch an event to notify the application about the emsg box.
const eventName = shaka.util.FakeEvent.EventName.Emsg;
const data = (new Map()).set('detail', emsg);
const event = new shaka.util.FakeEvent(eventName, data);
// A user can call preventDefault() on a cancelable event.
event.cancelable = true;

this.playerInterface_.onEvent(event);

if (event.defaultPrevented) {
// If the caller uses preventDefault() on the 'emsg' event, don't
// process any further, and don't generate an ID3 'metadata' event
// for the same data.
return;
}

// Additionally, ID3 events generate a 'metadata' event. This is a
// pre-parsed version of the metadata blob already dispatched in the
// 'emsg' event.
if (schemeId == 'https://aomedia.org/emsg/ID3' ||
schemeId == 'https://developer.apple.com/streaming/emsg-id3') {
// See https://aomediacodec.github.io/id3-emsg/
const frames = shaka.util.Id3Utils.getID3Frames(messageData);
if (frames.length) {
/** @private {shaka.extern.ID3Metadata} */
const metadata = {
cueTime: startTime,
data: messageData,
frames: frames,
dts: startTime,
pts: startTime,
};
this.playerInterface_.onMetadata(
[metadata], /* offset= */ 0, endTime);
}
}
}
}
}

/**
* Parse PRFT box.
* @param {number} timescale
Expand Down Expand Up @@ -1040,8 +1174,8 @@ shaka.media.MediaSourceEngine = class {
mimeType = this.transmuxers_[contentType].getOriginalMimeType();
}
if (reference) {
const {timestamp, metadata} = this.getTimestampAndDispatchMetadata_(
contentType, data, reference, mimeType);
const {timestamp, metadata} = this.getTimestampAndDispatchMetadata(
contentType, data, reference, stream, mimeType);
if (timestamp != null) {
if (this.firstVideoTimestamp_ == null &&
contentType == ContentType.VIDEO) {
Expand Down Expand Up @@ -2342,7 +2476,8 @@ shaka.media.MediaSourceEngine.SourceBufferMode_ = {
* @typedef {{
* getKeySystem: function():?string,
* onMetadata: function(!Array<shaka.extern.ID3Metadata>, number, ?number),
* onEvent: function(!Event)
* onEvent: function(!Event),
* onManifestUpdate: function()
* }}
*
* @summary Player interface
Expand All @@ -2353,5 +2488,7 @@ shaka.media.MediaSourceEngine.SourceBufferMode_ = {
* Callback to use when metadata arrives.
* @property {function(!Event)} onEvent
* Called when an event occurs that should be sent to the app.
* @property {function()} onManifestUpdate
* Called when an embedded 'emsg' box should trigger a manifest update.
*/
shaka.media.MediaSourceEngine.PlayerInterface;
Loading

0 comments on commit cb66f47

Please sign in to comment.