diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/TrayController.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/TrayController.jsx index b17a1800644..c25bdd22cfa 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/TrayController.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/TrayController.jsx @@ -101,6 +101,23 @@ export default class TrayController { }) } + requestSubtitlesFromIframe(cb) { + if (!bridge.canvasOrigin) return + + this._subtitleListener = new AbortController() + + window.addEventListener('message', (event) => { + if (event?.data?.subject === "media_tracks_response") { + cb(event?.data?.payload) + } + }, {signal: this._subtitleListener.signal}) + + this._audioContainer?.contentWindow?.postMessage( + {subject: 'media_tracks_request'}, + bridge.canvasOrigin + ) + } + _renderTray(trayProps) { const audioOptions = asAudioElement(this._audioContainer) || {} @@ -113,6 +130,7 @@ export default class TrayController { onExited={() => { bridge.focusActiveEditor(false) this._isOpen = false + this._subtitleListener?.abort() }} onSave={options => { this._applyAudioOptions(options) @@ -121,6 +139,7 @@ export default class TrayController { onDismiss={() => this._dismissTray()} open={this._shouldOpen} trayProps={trayProps} + requestSubtitlesFromIframe={(cb) => this.requestSubtitlesFromIframe(cb)} /> ) ReactDOM.render(element, this.container) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/AudioOptionsTray.test.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/AudioOptionsTray.test.jsx index f9e5db5de43..835d97a95ae 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/AudioOptionsTray.test.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/AudioOptionsTray.test.jsx @@ -34,9 +34,11 @@ describe('RCE "Audios" Plugin > AudioOptionsTray', () => { onRequestClose: jest.fn(), onSave: jest.fn(), open: true, + requestSubtitlesFromIframe: jest.fn(), audioOptions: { id: 'm-audio-id', titleText: 'Audio player', + tracks: [{locale: 'en', inherited: false}], }, trayProps: { host: 'localhost:3001', @@ -76,4 +78,17 @@ describe('RCE "Audios" Plugin > AudioOptionsTray', () => { tray.$doneButton.click() expect(props.onSave).toHaveBeenCalledTimes(1) }) + + describe('requestSubtitlesFromIframe', () => { + it('is not called when subtitles are present', () => { + renderComponent() + expect(props.requestSubtitlesFromIframe).not.toHaveBeenCalled() + }) + + it('is called when no subtitles present', () => { + props.audioOptions.tracks = null + renderComponent() + expect(props.requestSubtitlesFromIframe).toHaveBeenCalledTimes(1) + }) + }) }) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/TrayController.test.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/TrayController.test.jsx index 8354f65f4ef..7357a8ed3d6 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/TrayController.test.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/__tests__/TrayController.test.jsx @@ -150,4 +150,54 @@ describe('RCE "Audios" Plugin > AudioOptionsTray > TrayController', () => { await waitFor(() => expect(getTray()).toBeNull()) // the tray is closed after a transition }) }) + + describe.only('#requestSubtitlesFromIframe', () => { + let previousOrigin = '' + + beforeAll(() => { + previousOrigin = bridge.canvasOrigin + bridge.canvasOrigin = 'http://localhost' + }) + + afterAll(() => { + bridge.canvasOrigin = previousOrigin + }) + + it('posts message to iframe onload', () => { + const postMessageMock = jest.fn() + const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode()) + iframe.contentWindow.postMessage = postMessageMock; + trayController.showTrayForEditor(editors[0]) + expect(postMessageMock).toHaveBeenCalledTimes(1) + }) + + it('cleans up event listener on tray close', () => { + const postMessageMock = jest.fn() + const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode()) + iframe.contentWindow.postMessage = postMessageMock; + trayController.showTrayForEditor(editors[0]) + trayController.hideTrayForEditor(editors[0]) + trayController.showTrayForEditor(editors[0]) + expect(postMessageMock).toHaveBeenCalledTimes(2) + }) + + it('adds an event listener with a callback', () => { + const eventMock = jest.fn() + trayController.requestSubtitlesFromIframe(eventMock) + const msgEvent = new Event('message') + msgEvent.data = {subject: 'media_tracks_response', payload: [{locale: 'en'}]} + window.dispatchEvent(msgEvent) + expect(eventMock).toHaveBeenCalledTimes(1) + expect(eventMock).toHaveBeenCalledWith([{locale: 'en'}]) + }) + + it('event listener ignores events with wrong subject', () => { + const eventMock = jest.fn() + trayController.requestSubtitlesFromIframe(eventMock) + const msgEvent = new Event('message') + msgEvent.data = {subject: 'wrong_response', payload: [{locale: 'en'}]} + window.dispatchEvent(msgEvent) + expect(eventMock).toHaveBeenCalledTimes(0) + }) + }) }) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/index.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/index.jsx index 131851c1016..e31081b91de 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/index.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/AudioOptionsTray/index.jsx @@ -16,7 +16,7 @@ * with this program. If not, see . */ -import React, {useState} from 'react' +import React, {useState, useEffect} from 'react' import {arrayOf, bool, func, shape, string} from 'prop-types' import {Flex} from '@instructure/ui-flex' import {Tray} from '@instructure/ui-tray' @@ -40,9 +40,14 @@ export default function AudioOptionsTray({ onSave, trayProps, audioOptions, + requestSubtitlesFromIframe, }) { const [subtitles, setSubtitles] = useState(audioOptions.tracks || []) + useEffect(() => { + if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles) + }, []) + const handleSave = (e, contentProps) => { onSave({ media_object_id: audioOptions.id, @@ -129,6 +134,7 @@ AudioOptionsTray.propTypes = { onDismiss: func, onSave: func, open: bool.isRequired, + requestSubtitlesFromIframe: func, trayProps: shape({ host: string.isRequired, jwt: string.isRequired, @@ -149,4 +155,5 @@ AudioOptionsTray.defaultProps = { onExited: null, onDismiss: null, onSave: null, + requestSubtitlesFromIframe: () => {} } diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/TrayController.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/TrayController.jsx index 95b08db50e5..c70013b2106 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/TrayController.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/TrayController.jsx @@ -161,6 +161,22 @@ export default class TrayController { this._editor = null } + requestSubtitlesFromIframe(cb) { + if (!bridge.canvasOrigin) return + + this._subtitleListener = new AbortController() + window.addEventListener('message', (event) => { + if (event?.data?.subject === "media_tracks_response") { + cb(event?.data?.payload) + } + }, {signal: this._subtitleListener.signal}) + + this.$videoContainer?.contentWindow?.postMessage( + {subject: 'media_tracks_request'}, + bridge.canvasOrigin + ) + } + _renderTray(trayProps) { let vo = {} @@ -185,6 +201,7 @@ export default class TrayController { onExited={() => { bridge.focusActiveEditor(false) this._isOpen = false + this._subtitleListener?.abort() }} onSave={videoOptions => { this._applyVideoOptions(videoOptions) @@ -197,6 +214,7 @@ export default class TrayController { ? parseStudioOptions(this.$videoContainer) : null } + requestSubtitlesFromIframe={(cb) => this.requestSubtitlesFromIframe(cb)} /> ) ReactDOM.render(element, this.$container) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/TrayController.test.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/TrayController.test.jsx index 152aa25d6f0..af7796f5c13 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/TrayController.test.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/TrayController.test.jsx @@ -25,6 +25,7 @@ import VideoOptionsTrayDriver from './VideoOptionsTrayDriver' import * as contentSelection from '../../../shared/ContentSelection' import RCEGlobals from '../../../../RCEGlobals' import {createLiveRegion, removeLiveRegion} from '../../../../__tests__/liveRegionHelper' +import bridge from '../../../../../bridge' const mockVideoPlayers = [ { @@ -309,4 +310,54 @@ describe('RCE "Videos" Plugin > VideoOptionsTray > TrayController', () => { expect(updateMediaObject).toHaveBeenCalled() }) }) + + describe('#requestSubtitlesFromIframe', () => { + let previousOrigin = '' + + beforeAll(() => { + previousOrigin = bridge.canvasOrigin + bridge.canvasOrigin = 'http://localhost' + }) + + afterAll(() => { + bridge.canvasOrigin = previousOrigin + }) + + it('posts message to iframe onload', () => { + const postMessageMock = jest.fn() + const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode()) + iframe.contentWindow.postMessage = postMessageMock; + trayController.showTrayForEditor(editors[0]) + expect(postMessageMock).toHaveBeenCalledTimes(1) + }) + + it('cleans up event listener on tray close', () => { + const postMessageMock = jest.fn() + const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode()) + iframe.contentWindow.postMessage = postMessageMock; + trayController.showTrayForEditor(editors[0]) + trayController.hideTrayForEditor(editors[0]) + trayController.showTrayForEditor(editors[0]) + expect(postMessageMock).toHaveBeenCalledTimes(2) + }) + + it('adds an event listener with a callback', () => { + const eventMock = jest.fn() + trayController.requestSubtitlesFromIframe(eventMock) + const msgEvent = new Event('message') + msgEvent.data = {subject: 'media_tracks_response', payload: [{locale: 'en'}]} + window.dispatchEvent(msgEvent) + expect(eventMock).toHaveBeenCalledTimes(1) + expect(eventMock).toHaveBeenCalledWith([{locale: 'en'}]) + }) + + it('event listener ignores events with wrong subject', () => { + const eventMock = jest.fn() + trayController.requestSubtitlesFromIframe(eventMock) + const msgEvent = new Event('message') + msgEvent.data = {subject: 'wrong_response', payload: [{locale: 'en'}]} + window.dispatchEvent(msgEvent) + expect(eventMock).toHaveBeenCalledTimes(0) + }) + }) }) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/VideoOptionsTray.test.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/VideoOptionsTray.test.jsx index b0c6e424865..837169aacf3 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/VideoOptionsTray.test.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/__tests__/VideoOptionsTray.test.jsx @@ -37,6 +37,7 @@ describe('RCE "Videos" Plugin > VideoOptionsTray', () => { onRequestClose: jest.fn(), onSave: jest.fn(), open: true, + requestSubtitlesFromIframe: jest.fn(), videoOptions: { $element: null, appliedHeight: 180, @@ -202,6 +203,19 @@ describe('RCE "Videos" Plugin > VideoOptionsTray', () => { }) }) + describe('requestSubtitlesFromIframe', () => { + it('is not called when subtitles are present', () => { + renderComponent() + expect(props.requestSubtitlesFromIframe).not.toHaveBeenCalled() + }) + + it('is called when no subtitles present', () => { + props.videoOptions.tracks = null + renderComponent() + expect(props.requestSubtitlesFromIframe).toHaveBeenCalledTimes(1) + }) + }) + describe('when clicked', () => { beforeEach(() => { renderComponent() diff --git a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/index.jsx b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/index.jsx index 8001edd1649..a847a8dd85f 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/index.jsx +++ b/packages/canvas-rce/src/rce/plugins/instructure_record/VideoOptionsTray/index.jsx @@ -56,6 +56,7 @@ export default function VideoOptionsTray({ onSave, open, trayProps, + requestSubtitlesFromIframe = () => {}, onEntered = null, onExited = null, id = 'video-options-tray', @@ -96,6 +97,10 @@ export default function VideoOptionsTray({ } }, [videoOptions.attachmentId]) + useEffect(() => { + if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles) + }, []) + function handleTitleTextChange(event) { setTitleText(event.target.value) } @@ -374,4 +379,5 @@ VideoOptionsTray.propTypes = { }), id: string, studioOptions: parsedStudioOptionsPropType, + requestSubtitlesFromIframe: func } diff --git a/ui/features/media_player_iframe_content/index.jsx b/ui/features/media_player_iframe_content/index.jsx index 45b847ee115..4509c10f681 100644 --- a/ui/features/media_player_iframe_content/index.jsx +++ b/ui/features/media_player_iframe_content/index.jsx @@ -63,12 +63,27 @@ ready(() => { } } + const mediaTracks = media_object?.media_tracks?.map(track => { + return { + id: track.id, + src: track.url, + label: captionLanguageForLocale(track.locale), + type: track.kind, + language: track.locale, + inherited: track.inherited, + } + }) + window.addEventListener( 'message', event => { if (event?.data?.subject === 'reload_media' && media_id === event?.data?.media_object_id) { document.getElementsByTagName('video')[0].load() } + else if (event?.data?.subject === 'media_tracks_request') { + const tracks = mediaTracks?.map(t => ({locale: t.language, language: t.label, inherited: t.inherited})) + if (tracks) event.source.postMessage({subject: 'media_tracks_response', payload: tracks}, event.origin) + } }, false ) @@ -88,16 +103,6 @@ ready(() => { } } - const mediaTracks = media_object?.media_tracks?.map(track => { - return { - id: track.id, - src: track.url, - label: captionLanguageForLocale(track.locale), - type: track.kind, - language: track.locale, - inherited: track.inherited, - } - }) const aria_label = !media_object.title ? undefined : media_object.title ReactDOM.render(