diff --git a/externs/shaka/player.js b/externs/shaka/player.js
index 053afb99be..60db46f18c 100644
--- a/externs/shaka/player.js
+++ b/externs/shaka/player.js
@@ -1328,7 +1328,8 @@ shaka.extern.StreamingConfiguration;
* codecSwitchingStrategy: shaka.config.CodecSwitchingStrategy,
* sourceBufferExtraFeatures: string,
* forceTransmux: boolean,
-* insertFakeEncryptionInInit: boolean
+ * insertFakeEncryptionInInit: boolean,
+ * modifyCueCallback: shaka.extern.TextParser.ModifyCueCallback
* }}
*
* @description
@@ -1358,6 +1359,10 @@ shaka.extern.StreamingConfiguration;
* time.
*
* This value defaults to true
.
+ * @property {shaka.extern.TextParser.ModifyCueCallback} modifyCueCallback
+ * 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.
* @exportDoc
*/
shaka.extern.MediaSourceConfiguration;
diff --git a/externs/shaka/text.js b/externs/shaka/text.js
index cf42d110e1..2f34b7cabf 100644
--- a/externs/shaka/text.js
+++ b/externs/shaka/text.js
@@ -85,6 +85,16 @@ shaka.extern.TextParser = class {
shaka.extern.TextParser.TimeContext;
+/**
+ * A callback used for editing cues before appending.
+ * Provides the cue, and the URI of the captions file the cue was parsed from.
+ * You can edit the cue object passed in.
+ * @typedef {function(!shaka.text.Cue, ?string)}
+ * @exportDoc
+ */
+shaka.extern.TextParser.ModifyCueCallback;
+
+
/**
* @typedef {function():!shaka.extern.TextParser}
* @exportDoc
diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js
index ee32284f93..6b60338963 100644
--- a/lib/media/media_source_engine.js
+++ b/lib/media/media_source_engine.js
@@ -550,6 +550,9 @@ shaka.media.MediaSourceEngine = class {
*/
configure(config) {
this.config_ = config;
+ if (this.textEngine_) {
+ this.textEngine_.setModifyCueCallback(config.modifyCueCallback);
+ }
}
/**
@@ -571,6 +574,9 @@ shaka.media.MediaSourceEngine = class {
reinitText(mimeType, sequenceMode, external) {
if (!this.textEngine_) {
this.textEngine_ = new shaka.text.TextEngine(this.textDisplayer_);
+ if (this.textEngine_) {
+ this.textEngine_.setModifyCueCallback(this.config_.modifyCueCallback);
+ }
}
this.textEngine_.initParser(mimeType, sequenceMode,
external || this.segmentRelativeVttTiming_, this.manifestType_);
diff --git a/lib/text/text_engine.js b/lib/text/text_engine.js
index a438e75d8d..97e85f154b 100644
--- a/lib/text/text_engine.js
+++ b/lib/text/text_engine.js
@@ -51,6 +51,9 @@ shaka.text.TextEngine = class {
/** @private {string} */
this.selectedClosedCaptionId_ = '';
+ /** @private {shaka.extern.TextParser.ModifyCueCallback} */
+ this.modifyCueCallback_ = (cue, uri) => {};
+
/**
* The closed captions map stores the CEA closed captions by closed captions
* id and start and end time.
@@ -162,6 +165,11 @@ shaka.text.TextEngine = class {
this.segmentRelativeVttTiming_ = segmentRelativeVttTiming;
}
+ /** @param {shaka.extern.TextParser.ModifyCueCallback} modifyCueCallback */
+ setModifyCueCallback(modifyCueCallback) {
+ this.modifyCueCallback_ = modifyCueCallback;
+ }
+
/**
* @param {BufferSource} buffer
* @param {?number} startTime relative to the start of the presentation
@@ -200,6 +208,9 @@ shaka.text.TextEngine = class {
// Parse the buffer and add the new cues.
const allCues = this.parser_.parseMedia(
shaka.util.BufferUtils.toUint8(buffer), time, uri);
+ for (const cue of allCues) {
+ this.modifyCueCallback_(cue, uri || null);
+ }
const cuesToAppend = allCues.filter((cue) => {
return cue.startTime >= this.appendWindowStart_ &&
cue.startTime < this.appendWindowEnd_;
diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js
index 512494f697..fa2ec63f2e 100644
--- a/lib/util/player_configuration.js
+++ b/lib/util/player_configuration.js
@@ -340,6 +340,11 @@ shaka.util.PlayerConfiguration = class {
sourceBufferExtraFeatures: '',
forceTransmux: false,
insertFakeEncryptionInInit: true,
+ modifyCueCallback: (cue, uri) => {
+ return shaka.util.ConfigUtils.referenceParametersAndReturn(
+ [cue, uri],
+ undefined);
+ },
};
const ads = {
diff --git a/test/demo/demo_unit.js b/test/demo/demo_unit.js
index 6bc297e4e3..af64c3d088 100644
--- a/test/demo/demo_unit.js
+++ b/test/demo/demo_unit.js
@@ -81,7 +81,8 @@ describe('Demo', () => {
.add('drm.keySystemsMapping')
.add('manifest.raiseFatalErrorOnManifestUpdateRequestFailure')
.add('drm.persistentSessionOnlinePlayback')
- .add('drm.persistentSessionsMetadata');
+ .add('drm.persistentSessionsMetadata')
+ .add('mediaSource.modifyCueCallback');
/**
* @param {!Object} section
diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js
index c73645f616..81a6d8faf8 100644
--- a/test/media/media_source_engine_unit.js
+++ b/test/media/media_source_engine_unit.js
@@ -1496,7 +1496,7 @@ describe('MediaSourceEngine', () => {
mockTextEngine = jasmine.createSpyObj('TextEngine', [
'initParser', 'destroy', 'appendBuffer', 'remove', 'setTimestampOffset',
'setAppendWindow', 'bufferStart', 'bufferEnd', 'bufferedAheadOf',
- 'storeAndAppendClosedCaptions',
+ 'storeAndAppendClosedCaptions', 'setModifyCueCallback',
]);
const resolve = () => Promise.resolve();
diff --git a/test/text/text_engine_unit.js b/test/text/text_engine_unit.js
index 59f9f06103..2231279abd 100644
--- a/test/text/text_engine_unit.js
+++ b/test/text/text_engine_unit.js
@@ -135,6 +135,18 @@ describe('TextEngine', () => {
textEngine.destroy();
await p;
});
+
+ it('calls modifyCueCallback', async () => {
+ const cue1 = createFakeCue(0, 1);
+ const cue2 = createFakeCue(1, 2);
+ const modifyCueCallback = jasmine.createSpy('modifyCueCallback');
+ textEngine.setModifyCueCallback(
+ shaka.test.Util.spyFunc(modifyCueCallback));
+ mockParseMedia.and.returnValue([cue1, cue2]);
+ await textEngine.appendBuffer(dummyData, 0, 3, 'uri');
+ expect(modifyCueCallback).toHaveBeenCalledWith(cue1, 'uri');
+ expect(modifyCueCallback).toHaveBeenCalledWith(cue2, 'uri');
+ });
});
describe('storeAndAppendClosedCaptions', () => {