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', () => {