Show Media Captions in New Quizzes

closes LF-804
flag=none

Test Plan:
 - Add Video to a New Quiz
 - Add captions
 - Refresh the New Quiz
 - Open Video tray
 * Verify captions appear
 - Repeat with Audio

Change-Id: I1a3f0df0bfcaec7b3f456d7dcfea78017ab1a549
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/334124
Reviewed-by: Eric Saupe <eric.saupe@instructure.com>
QA-Review: Eric Saupe <eric.saupe@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Jacob DeWar <jacob.dewar@instructure.com>
This commit is contained in:
Jacob DeWar 2023-11-27 12:22:17 -05:00
parent ccbfef7d8d
commit 92353a1edf
9 changed files with 196 additions and 11 deletions

View File

@ -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)

View File

@ -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)
})
})
})

View File

@ -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)
})
})
})

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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: () => {}
}

View File

@ -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)

View File

@ -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)
})
})
})

View File

@ -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()

View File

@ -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
}

View File

@ -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(