add ui-media-player to SpeedGrader submission comments

flag=speedgrader_studio_media_capture

test plan:
- have a course with at least 1 student and assignnment
- go to SpeedGrader with the FF on
- record or upload a media submission comment
- click the video to open the media player
- notice the media player is the new one

Change-Id: Ia17ca557130098b9bfaee4c31a4bd2b88c773c11
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/349020
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
Reviewed-by: Cameron Ray <cameron.ray@instructure.com>
QA-Review: Cameron Ray <cameron.ray@instructure.com>
Product-Review: Ravi Koll <ravi.koll@instructure.com>
This commit is contained in:
Derek Williams 2024-06-03 11:50:25 -04:00
parent abdc414ee5
commit 7bac8db0db
2 changed files with 158 additions and 14 deletions

View File

@ -17,12 +17,23 @@
import {getSourcesAndTracks} from '../mediaComment'
import $ from 'jquery'
import {enableFetchMocks} from 'jest-fetch-mock'
enableFetchMocks()
describe('getSourcesAndTracks', () => {
beforeAll(() => {
$.getJSON = jest.fn()
})
beforeEach(() => {
jest.resetModules()
})
afterAll(() => {
$.getJSON.mockRestore()
})
it('with no attachment id', () => {
getSourcesAndTracks(1)
expect($.getJSON).toHaveBeenCalledWith('/media_objects/1/info', expect.anything())
@ -32,4 +43,99 @@ describe('getSourcesAndTracks', () => {
getSourcesAndTracks(1, 4)
expect($.getJSON).toHaveBeenCalledWith('/media_attachments/4/info', expect.anything())
})
it('should return sources and tracks in the old format when studio_media_capture_enabled is false', async () => {
ENV.studio_media_capture_enabled = false
// Mock response
const mockResponse = {
media_sources: [
{
url: 'http://example.com/video.mp4',
content_type: 'video/mp4',
width: 640,
height: 360,
bitrate: 500000,
},
{
url: 'http://example.com/video_low.mp4',
content_type: 'video/mp4',
width: 320,
height: 180,
bitrate: 250000,
},
],
media_tracks: [{url: 'http://example.com/track.vtt', kind: 'subtitles', locale: 'en'}],
can_add_captions: true,
}
// Mock $.getJSON to return the mock response
jest.spyOn($, 'getJSON').mockImplementation((url, callback) => {
callback(mockResponse)
return $.Deferred().resolve(mockResponse).promise()
})
const id = '123'
const result = await getSourcesAndTracks(id)
expect(result.sources).toEqual([
"<source type='video&#x2F;mp4' src='http:&#x2F;&#x2F;example.com&#x2F;video_low.mp4' title='320x180 244 kbps' />",
"<source type='video&#x2F;mp4' src='http:&#x2F;&#x2F;example.com&#x2F;video.mp4' title='640x360 488 kbps' />",
])
expect(result.tracks).toEqual([
"<track kind='subtitles' label='English' src='http:&#x2F;&#x2F;example.com&#x2F;track.vtt' srclang='en' data-inherited-track='' />",
])
})
it('should return sources and tracks in the new format when studio_media_capture_enabled is true', async () => {
ENV.studio_media_capture_enabled = true
const mockResponse = {
media_sources: [
{
url: 'http://example.com/video.mp4',
content_type: 'video/mp4',
width: 640,
height: 360,
bitrate: 500000,
},
{
url: 'http://example.com/video_low.mp4',
content_type: 'video/mp4',
width: 320,
height: 180,
bitrate: 250000,
},
],
media_tracks: [{url: 'http://example.com/track.vtt', kind: 'subtitles', locale: 'en'}],
can_add_captions: true,
}
// Mock $.getJSON to return the mock response
jest.spyOn($, 'getJSON').mockImplementation((url, callback) => {
callback(mockResponse)
return $.Deferred().resolve(mockResponse).promise()
})
const id = '123'
const result = await getSourcesAndTracks(id)
expect(result.sources).toEqual([
{
src: 'http://example.com/video_low.mp4',
label: '320x180 244 kbps',
},
{
src: 'http://example.com/video.mp4',
label: '640x360 488 kbps',
},
])
expect(result.tracks).toEqual([
{
id: '123',
type: 'subtitles',
label: 'English',
src: '/track.vtt',
language: 'en',
},
])
})
})

View File

@ -25,6 +25,9 @@ import {map, values} from 'lodash'
import htmlEscape from '@instructure/html-escape'
import sanitizeUrl from '@canvas/util/sanitizeUrl'
import {contentMapping} from '@instructure/canvas-rce/src/common/mimeClass'
import React from 'react'
import ReactDOM from 'react-dom'
import {MediaPlayer} from '@instructure/ui-media-player'
const I18n = useI18nScope('jquery_media_comments')
@ -96,6 +99,7 @@ mejs.MepDefaults.features.splice(positionAfterSubtitleSelector, 0, 'sourcechoose
mejs.MepDefaults.features.splice(positionAfterSubtitleSelector, 0, 'speed')
export function getSourcesAndTracks(id, attachmentId) {
const studioMediaEnabled = ENV.studio_media_capture_enabled
const dfd = new $.Deferred()
const api = attachmentId ? 'media_attachments' : 'media_objects'
$.getJSON(`/${api}/${attachmentId || id}/info`, data => {
@ -106,24 +110,39 @@ export function getSourcesAndTracks(id, attachmentId) {
// mediaplayer plays the first source by default, which tends to be the highest
// resolution. sort so we play the lowest res. by default
.sort((a, b) => parseInt(a.bitrate, 10) - parseInt(b.bitrate, 10))
.map(
source =>
// xsslint safeString.function sanitizeUrl
`<source
type='${htmlEscape(source.content_type)}'
src='${sanitizeUrl(htmlEscape(source.url))}'
title='${htmlEscape(source.width)}x${htmlEscape(source.height)} ${htmlEscape(
Math.floor(source.bitrate / 1024)
)} kbps'
/>`
)
.map(source => {
if (studioMediaEnabled) {
return {
src: source.url,
label: `${htmlEscape(source.width)}x${htmlEscape(source.height)} ${htmlEscape(
Math.floor(source.bitrate / 1024)
)} kbps`,
}
}
// xsslint safeString.function sanitizeUrl
return `<source type='${htmlEscape(source.content_type)}' src='${sanitizeUrl(
htmlEscape(source.url)
)}' title='${htmlEscape(source.width)}x${htmlEscape(source.height)} ${htmlEscape(
Math.floor(source.bitrate / 1024)
)} kbps' />`
})
const tracks = map(data.media_tracks, track => {
const languageName = mejs.language.codes[track.locale] || track.locale
if (studioMediaEnabled) {
return {
id: attachmentId || id,
type: track.kind,
label: languageName,
src: getPathFromUrl(track.url),
language: track.locale,
}
}
return `<track kind='${htmlEscape(track.kind)}' label='${htmlEscape(
languageName
)}' src='${htmlEscape(track.url)}' srclang='${htmlEscape(track.locale)}'
data-inherited-track='${htmlEscape(track.inherited)}' />`
)}' src='${htmlEscape(track.url)}' srclang='${htmlEscape(
track.locale
)}' data-inherited-track='${htmlEscape(track.inherited)}' />`
})
const types = map(data.media_sources, source => source.content_type)
@ -132,6 +151,11 @@ export function getSourcesAndTracks(id, attachmentId) {
return dfd
}
function getPathFromUrl(url) {
const urlObj = new URL(url)
return urlObj.pathname + urlObj.search + urlObj.hash
}
function createMediaTag({sourcesAndTracks, mediaType, height, width, mediaPlayerOptions}) {
let tagType = mediaType === 'video' ? 'video' : 'audio'
const st_tags = sourcesAndTracks.sources.concat(sourcesAndTracks.tracks).join('')
@ -328,6 +352,14 @@ const mediaCommentActions = {
mediaCommentId: id,
}
const mediaPlayer = (
<MediaPlayer
tracks={sourcesAndTracks.tracks}
sources={sourcesAndTracks.sources}
captionPosition="bottom"
/>
)
const $mediaTag = createMediaTag({
sourcesAndTracks,
mediaPlayerOptions,
@ -335,7 +367,13 @@ const mediaCommentActions = {
height,
width,
})
$mediaTag.appendTo($dialog.html(''))
const studioMediaEnabled = ENV.studio_media_capture_enabled
if (studioMediaEnabled) {
ReactDOM.render(mediaPlayer, $dialog[0])
} else {
$mediaTag.appendTo($dialog.html(''))
}
$this.data({
mediaelementplayer: new mejs.MediaElementPlayer($mediaTag, mediaPlayerOptions),