diff --git a/src/lib/viewers/media/DashViewer.js b/src/lib/viewers/media/DashViewer.js index b19dddd43..a32a256bd 100644 --- a/src/lib/viewers/media/DashViewer.js +++ b/src/lib/viewers/media/DashViewer.js @@ -451,57 +451,77 @@ class DashViewer extends VideoBaseViewer { this.textTracks.map((track) => getLanguageName(track.language) || track.language), getLanguageName(this.options.location.locale.substring(0, 2)) ); - return; } - - // Attempt to load auto-generated captions - // @TODO 07-07-18: Support both auto-generated captions and subtitles from videos - this.loadAutoGeneratedCaptions(); } /** - * Loads auto-generated captions + * Loads auto-generated captions from skills using a shaka SimpleTextDisplayer + * @TODO 07-07-18: Support both auto-generated captions and subtitles from videos * + * @public + * @param {Object} transcriptCard - transcript card from Box Skills * @return {void} */ - loadAutoGeneratedCaptions() { - const textCues = []; - - // Convert Box Skill transcript cards to Shaka Text Cues - const skillsCards = getProp(this.options, 'file.metadata.global.boxSkillsCards.cards'); - // No skills are available on the file - if (!skillsCards) { + loadAutoGeneratedCaptions(transcriptCard) { + // Avoid regenerating captions if the object has not changed + if (this.transcript === transcriptCard) { return; } - const transcriptCard = skillsCards.find((card) => card.skill_card_type === 'transcript'); - // No transcript can be found in the skills data - if (!transcriptCard) { + this.transcript = transcriptCard; + const textCues = this.createTextCues(transcriptCard); + + // Don't do anything if there are no cues + if (!textCues.length) { return; } - const entries = transcriptCard.entries || []; - entries.forEach((entry) => { - // Set defaults if transcript data is malformed (start/end: 0s, text: '') - const { appears = [{}], text = '' } = entry; - const { start = 0, end = 0 } = Array.isArray(appears) && appears.length > 0 ? appears[0] : {}; - textCues.push(new shaka.text.Cue(start, end, text)); - }); - - if (textCues.length > 0) { - this.autoCaptionDisplayer = new shaka.text.SimpleTextDisplayer(this.mediaEl); - this.autoCaptionDisplayer.append(textCues); - this.player.configure({ textDisplayFactory: this.autoCaptionDisplayer }); + // We know we are editing the transcript if we already have created an autoCaptionDisplayer + if (this.autoCaptionDisplayer) { + const areAutoCaptionsVisible = this.autoCaptionDisplayer.isTextVisible(); + this.autoCaptionDisplayer.destroy(); + this.setupAutoCaptionDisplayer(textCues); + this.autoCaptionDisplayer.setTextVisibility(areAutoCaptionsVisible); + } else { + this.setupAutoCaptionDisplayer(textCues); + // Update the subtitles/caption button to reflect auto-translation + this.mediaControls.setLabel(this.mediaControls.subtitlesButtonEl, __('media_auto_generated_captions')); this.mediaControls.initSubtitles( [__('auto_generated')], getLanguageName(this.options.location.locale.substring(0, 2)) ); - - // Update the subtitles/caption button to reflect auto-translation - this.mediaControls.setLabel(this.mediaControls.subtitlesButtonEl, __('media_auto_generated_captions')); } } + /** + * Turns a Box Skills transcript card into an array of shaka text cues + * + * @param {Object} transcriptCard - transcript card from Box Skills + * @return {Array} Array of text cues + */ + createTextCues(transcriptCard) { + const entries = getProp(transcriptCard, 'entries', []); + return entries.map((entry) => { + // Set defaults if transcript data is malformed (start/end: 0s, text: '') + const { appears = [{}], text = '' } = entry; + const { start = 0, end = 0 } = Array.isArray(appears) && appears.length > 0 ? appears[0] : {}; + return new shaka.text.Cue(start, end, text); + }); + } + + /** + * Sets up the autoCaption displayer using a shaka SimpleTextDisplayer + * + * @public + * @param {Array} textCues - Array of text cues which map text to a timestamp + * @return {void} + */ + setupAutoCaptionDisplayer(textCues) { + this.autoCaptionDisplayer = new shaka.text.SimpleTextDisplayer(this.mediaEl); + this.autoCaptionDisplayer.append(textCues); + this.player.configure({ textDisplayFactory: this.autoCaptionDisplayer }); + } + /** * Loads alternate audio streams * diff --git a/src/lib/viewers/media/__tests__/DashViewer-test.js b/src/lib/viewers/media/__tests__/DashViewer-test.js index b238f9f98..81cbdd012 100644 --- a/src/lib/viewers/media/__tests__/DashViewer-test.js +++ b/src/lib/viewers/media/__tests__/DashViewer-test.js @@ -794,100 +794,94 @@ describe('lib/viewers/media/DashViewer', () => { expect(dash.textTracks).to.deep.equal([russian, foo, und, empty, doesntmatter, zero]); }); + }); - it('should attempt to load auto-generated subtitles if no there are no subtitles from the video', () => { - sandbox.stub(dash, 'loadAutoGeneratedCaptions'); + describe('loadAutoGeneratedCaptions', () => { + beforeEach(() => { + dash.autoCaptionDisplayer = { + append: () => {}, + setTextVisibility: () => {}, + isTextVisible: () => {}, + destroy: () => {} + }; + dash.createTextCues = sandbox.stub(); + dash.setupAutoCaptionDisplayer = sandbox.stub(); + stubs.mockPlayer = sandbox.mock(dash.player); + stubs.mockDisplayer = sandbox.mock(dash.autoCaptionDisplayer); + }); - const subs = []; - stubs.mockPlayer.expects('getTextTracks').returns(subs); - stubs.mockControls.expects('initSubtitles').never(); + const transcript = { + appears: [ + { + start: 0, + end: 1 + } + ], + text: 'sometext' + }; - dash.loadSubtitles(); + const cues = [{ 1: 'foo' }, { 2: 'bar' }]; - expect(dash.loadAutoGeneratedCaptions).to.be.called; + it('should do nothing if the transcript has not changed', () => { + dash.transcript = transcript; + dash.loadAutoGeneratedCaptions(transcript); + expect(dash.createTextCues).to.not.be.called; }); - }); - describe('loadAutoGeneratedCaptions()', () => { - it('should convert to Shaka Cues, initialize subtitles, set button label & return true if there are auto-generated subtitles', () => { - dash.options = { - file: { - metadata: { - global: { - boxSkillsCards: { - cards: [ - { - skill_card_type: 'transcript', - entries: [ - { - appears: [ - { - start: 0, - end: 1 - } - ], - text: 'sometext' - } - ] - } - ] - } - } - } - }, - location: { - locale: 'en-US' - } - }; - const appendStub = sandbox.stub(); - sandbox.stub(shaka.text, 'SimpleTextDisplayer').returns({ - append: appendStub - }); + it('should do nothing if no text cues are found', () => { + dash.createTextCues.returns([]); + dash.setupAutoCaptionDisplayer = sandbox.stub(); - stubs.mockPlayer.expects('configure').withArgs({ - textDisplayFactory: sandbox.match.any - }); + dash.loadAutoGeneratedCaptions(transcript); + expect(dash.setupAutoCaptionDisplayer).to.not.be.called; + }); + + it('should destroy and reset an existing autoCaptionDisplayer', () => { + stubs.mockDisplayer.expects('destroy'); + stubs.mockDisplayer.expects('setTextVisibility'); + dash.createTextCues.returns(cues); + dash.loadAutoGeneratedCaptions(transcript); + + expect(dash.setupAutoCaptionDisplayer).to.be.calledWith(cues); + }); + + it('should setup a new autoCaptionDisplayer if setting up for first time', () => { + dash.autoCaptionDisplayer = null; + dash.createTextCues.returns(cues); stubs.mockControls.expects('initSubtitles').withArgs(['Auto-Generated'], 'English'); stubs.mockControls.expects('setLabel').withArgs(sandbox.match.any, 'Auto-Generated Captions'); - dash.loadAutoGeneratedCaptions(); - - expect(appendStub).to.be.called; + dash.loadAutoGeneratedCaptions(transcript); + expect(dash.setupAutoCaptionDisplayer).to.be.calledWith(cues); }); + }); - it('should not set a custom text displayer if there are no transcript cards', () => { - stubs.mockPlayer.expects('configure').never(); - stubs.mockControls.expects('initSubtitles').never(); - stubs.mockControls.expects('setLabel').never(); + describe('createTextCues()', () => { + it('should correctly map cues', () => { + const transcript = { entries: [{ appears: [{ start: 1, end: 2 }], text: 'foo' }] }; + const result = dash.createTextCues(transcript)[0]; + expect(result.startTime).to.equal(1); + expect(result.endTime).to.equal(2); - dash.loadAutoGeneratedCaptions(); + expect(result.payload).to.equal('foo'); }); + }); - it('should convert to Shaka Cues, initialize subtitles, set button label & return true if there are auto-generated subtitles', () => { - dash.options = { - file: { - metadata: { - global: { - boxSkillsCards: { - cards: [ - { - skill_card_type: 'not a transcript', - entries: [] - } - ] - } - } - } - }, - location: { - locale: 'en-US' - } - }; - stubs.mockPlayer.expects('configure').never(); - stubs.mockControls.expects('initSubtitles').never(); - stubs.mockControls.expects('setLabel').never(); + describe('setupAutoCaptionDisplayer()', () => { + beforeEach(() => { + stubs.appendStub = sandbox.stub(); + sandbox.stub(shaka.text, 'SimpleTextDisplayer').returns({ + append: stubs.appendStub + }); + }); + + it('should setup a simpleTextDisplayer and configure the player', () => { + stubs.mockPlayer.expects('configure').withArgs({ + textDisplayFactory: sandbox.match.any + }); - dash.loadAutoGeneratedCaptions(); + dash.setupAutoCaptionDisplayer('foo'); + expect(stubs.appendStub).to.be.called; }); });