add ability to save media objects to canvas-media
fixes COMMS-2314 refs COMMS-2293 Test Plan - create a media type assignment - as a student in a2 with notorious running locally go to record a video/upload a video file - notice it works and posts back to the containing dom Change-Id: I6c00c688b33deee017b7babb3729ff57d3dd2114 Reviewed-on: https://gerrit.instructure.com/205605 Tested-by: Jenkins Reviewed-by: Landon Gilbert-Bland <lbland@instructure.com> QA-Review: Steven Burnett <sburnett@instructure.com> Product-Review: Steven Burnett <sburnett@instructure.com>
This commit is contained in:
parent
c8261d1476
commit
e614f14517
|
@ -21,36 +21,57 @@ import Button from '@instructure/ui-buttons/lib/components/Button'
|
||||||
import closedCaptionLanguages from '../../../../shared/closedCaptionLanguages'
|
import closedCaptionLanguages from '../../../../shared/closedCaptionLanguages'
|
||||||
import I18n from 'i18n!assignments_2_text_entry'
|
import I18n from 'i18n!assignments_2_text_entry'
|
||||||
import {IconAttachMediaLine} from '@instructure/ui-icons'
|
import {IconAttachMediaLine} from '@instructure/ui-icons'
|
||||||
import React, {useState} from 'react'
|
import React from 'react'
|
||||||
import UploadMedia from '@instructure/canvas-media'
|
import UploadMedia from '@instructure/canvas-media'
|
||||||
import {UploadMediaStrings, MediaCaptureStrings} from '../../../../shared/UploadMediaTranslations'
|
import {UploadMediaStrings, MediaCaptureStrings} from '../../../../shared/UploadMediaTranslations'
|
||||||
import View from '@instructure/ui-layout/lib/components/View'
|
import View from '@instructure/ui-layout/lib/components/View'
|
||||||
|
|
||||||
export default function MediaAttempt() {
|
const languages = Object.keys(closedCaptionLanguages).map(key => {
|
||||||
const [mediaModalOpen, setMediaModalOpen] = useState(false)
|
return {id: key, label: closedCaptionLanguages[key]}
|
||||||
|
})
|
||||||
|
|
||||||
const languages = Object.keys(closedCaptionLanguages).map(key => {
|
export default class MediaAttempt extends React.Component {
|
||||||
return {id: key, label: closedCaptionLanguages[key]}
|
state = {
|
||||||
})
|
mediaModalOpen: false,
|
||||||
|
mediaObjectUrl: null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
onDismiss = mediaObject => {
|
||||||
<View as="div" borderWidth="small">
|
this.setState({mediaModalOpen: false, mediaObjectUrl: mediaObject.embedded_iframe_url})
|
||||||
<UploadMedia
|
}
|
||||||
uploadMediaTranslations={{UploadMediaStrings, MediaCaptureStrings}}
|
|
||||||
onDismiss={() => setMediaModalOpen(false)}
|
render() {
|
||||||
open={mediaModalOpen}
|
if (this.state.mediaObjectUrl) {
|
||||||
liveRegion={() => document.getElementById('flash_screenreader_holder')}
|
// TODO: figure out how the heck we want to style this thing.
|
||||||
languages={languages}
|
return <iframe title="meidathings" src={this.state.mediaObjectUrl} />
|
||||||
/>
|
}
|
||||||
<Billboard
|
|
||||||
heading={I18n.t('Add Media')}
|
return (
|
||||||
hero={<IconAttachMediaLine color="brand" />}
|
<View as="div" borderWidth="small">
|
||||||
message={
|
<UploadMedia
|
||||||
<Button size="small" variant="primary" onClick={() => setMediaModalOpen(true)}>
|
onDismiss={this.onDismiss}
|
||||||
{I18n.t('Record/Upload')}
|
contextId={this.props.assignment.env.courseId}
|
||||||
</Button>
|
contextType="course"
|
||||||
}
|
open={this.state.mediaModalOpen}
|
||||||
/>
|
tabs={{embed: false, record: true, upload: true}}
|
||||||
</View>
|
uploadMediaTranslations={{UploadMediaStrings, MediaCaptureStrings}}
|
||||||
)
|
liveRegion={() => document.getElementById('flash_screenreader_holder')}
|
||||||
|
languages={languages}
|
||||||
|
/>
|
||||||
|
<Billboard
|
||||||
|
heading={I18n.t('Add Media')}
|
||||||
|
hero={<IconAttachMediaLine color="brand" />}
|
||||||
|
message={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => this.setState({mediaModalOpen: true})}
|
||||||
|
>
|
||||||
|
{I18n.t('Record/Upload')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"@instructure/ui-themes": "^6.8.1",
|
"@instructure/ui-themes": "^6.8.1",
|
||||||
"@instructure/ui-toggle-details": "^6.8.1",
|
"@instructure/ui-toggle-details": "^6.8.1",
|
||||||
"@instructure/uid": "^6.8.1",
|
"@instructure/uid": "^6.8.1",
|
||||||
|
"axios": "^0.18.0",
|
||||||
"prop-types": "^15",
|
"prop-types": "^15",
|
||||||
"react": "^16",
|
"react": "^16",
|
||||||
"react-dom": "^16"
|
"react-dom": "^16"
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default function EmbedPanel({embedCode, setEmbedCode, label}) {
|
||||||
maxHeight="10rem"
|
maxHeight="10rem"
|
||||||
label={label}
|
label={label}
|
||||||
value={embedCode}
|
value={embedCode}
|
||||||
|
placeholder={label}
|
||||||
onChange={e => setEmbedCode(e.target.value)}
|
onChange={e => setEmbedCode(e.target.value)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,12 +20,14 @@ import React from 'react'
|
||||||
|
|
||||||
import {Alert} from '@instructure/ui-alerts'
|
import {Alert} from '@instructure/ui-alerts'
|
||||||
import {canUseMediaCapture, MediaCapture} from '@instructure/media-capture'
|
import {canUseMediaCapture, MediaCapture} from '@instructure/media-capture'
|
||||||
import {object, string} from 'prop-types'
|
import {func, object, string} from 'prop-types'
|
||||||
|
|
||||||
|
import saveMediaRecording from './saveMediaRecording'
|
||||||
|
|
||||||
export default class MediaRecorder extends React.Component {
|
export default class MediaRecorder extends React.Component {
|
||||||
// saveFile = (file) => {
|
saveFile = file => {
|
||||||
// this.props.contentProps.saveMediaRecording(file, this.props.editor, this.props.dismiss)
|
saveMediaRecording(file, this.props.contextId, this.props.contextType, this.props.dismiss)
|
||||||
// }
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -43,6 +45,9 @@ export default class MediaRecorder extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaRecorder.propTypes = {
|
MediaRecorder.propTypes = {
|
||||||
|
contextId: string,
|
||||||
|
contextType: string,
|
||||||
|
dismiss: func,
|
||||||
errorMessage: string.isRequired,
|
errorMessage: string.isRequired,
|
||||||
MediaCaptureStrings: object // eslint-disable-line react/forbid-prop-types
|
MediaCaptureStrings: object // eslint-disable-line react/forbid-prop-types
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const screenfull = {
|
||||||
|
on() {},
|
||||||
|
off() {}
|
||||||
|
}
|
||||||
|
export default screenfull
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {fireEvent, render} from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import EmbedPanel from '../EmbedPanel'
|
||||||
|
|
||||||
|
describe('EmbedPanel', () => {
|
||||||
|
it('renders with label', () => {
|
||||||
|
const {getByText} = render(
|
||||||
|
<EmbedPanel embedCode="" label="embed panel" setEmbedCode={() => {}} />
|
||||||
|
)
|
||||||
|
expect(getByText('embed panel')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with value', () => {
|
||||||
|
const {getByText} = render(
|
||||||
|
<EmbedPanel embedCode="the best value of the embed" label="" setEmbedCode={() => {}} />
|
||||||
|
)
|
||||||
|
expect(getByText('the best value of the embed')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('on change calls setEmbedCode', () => {
|
||||||
|
const handleChange = jest.fn()
|
||||||
|
const {getByPlaceholderText} = render(
|
||||||
|
<EmbedPanel
|
||||||
|
embedCode="the best value of the embed"
|
||||||
|
label="embed label"
|
||||||
|
setEmbedCode={handleChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const textArea = getByPlaceholderText('embed label')
|
||||||
|
fireEvent.change(textArea, {target: {value: 'TEST VALUE'}})
|
||||||
|
|
||||||
|
expect(handleChange).toHaveBeenCalledTimes(1)
|
||||||
|
expect(handleChange.mock.calls[0][0]).toBe('TEST VALUE')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import {render, fireEvent, waitForElement} from '@testing-library/react'
|
||||||
|
import ComputerPanel from '../ComputerPanel'
|
||||||
|
|
||||||
|
function makeTranslationProps() {
|
||||||
|
return {
|
||||||
|
UploadMediaStrings: {
|
||||||
|
CLEAR_FILE_TEXT: 'Clear File',
|
||||||
|
INVALID_FILE_TEXT: 'Invalid file type',
|
||||||
|
DRAG_DROP_CLICK_TO_BROWSE: 'drag and drop or clik to browse'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UploadFile: ComputerPanel', () => {
|
||||||
|
it('shows a failure message if the file is rejected', () => {
|
||||||
|
const notAnImageFile = new File(['foo'], 'foo.txt', {
|
||||||
|
type: 'text/plain'
|
||||||
|
})
|
||||||
|
const handleSetFile = jest.fn()
|
||||||
|
const handleSetHasUploadedFile = jest.fn()
|
||||||
|
const {getByLabelText, getByText} = render(
|
||||||
|
<ComputerPanel
|
||||||
|
theFile={null}
|
||||||
|
setFile={handleSetFile}
|
||||||
|
hasUploadedFile={false}
|
||||||
|
setHasUploadedFile={handleSetHasUploadedFile}
|
||||||
|
accept="image/*"
|
||||||
|
uploadMediaTranslations={makeTranslationProps()}
|
||||||
|
label="Upload File"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const dropZone = getByLabelText(/Upload File/, {selector: 'input'})
|
||||||
|
fireEvent.change(dropZone, {
|
||||||
|
target: {
|
||||||
|
files: [notAnImageFile]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(getByText('Invalid file type')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts file files', () => {
|
||||||
|
const aFile = new File(['foo'], 'foo.png', {
|
||||||
|
type: 'image/png'
|
||||||
|
})
|
||||||
|
const handleSetFile = jest.fn()
|
||||||
|
const handleSetHasUploadedFile = jest.fn()
|
||||||
|
const {getByLabelText, queryByText} = render(
|
||||||
|
<ComputerPanel
|
||||||
|
theFile={null}
|
||||||
|
setFile={handleSetFile}
|
||||||
|
hasUploadedFile={false}
|
||||||
|
setHasUploadedFile={handleSetHasUploadedFile}
|
||||||
|
accept="image/*"
|
||||||
|
uploadMediaTranslations={makeTranslationProps()}
|
||||||
|
label="Upload File"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const dropZone = getByLabelText(/Upload File/, {selector: 'input'})
|
||||||
|
fireEvent.change(dropZone, {
|
||||||
|
target: {
|
||||||
|
files: [aFile]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(queryByText('Invalid file type')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears error messages if a valid file is added', () => {
|
||||||
|
const notAnImageFile = new File(['foo'], 'foo.txt', {
|
||||||
|
type: 'text/plain'
|
||||||
|
})
|
||||||
|
const aFile = new File(['foo'], 'foo.png', {
|
||||||
|
type: 'image/png'
|
||||||
|
})
|
||||||
|
const handleSetFile = jest.fn()
|
||||||
|
const handleSetHasUploadedFile = jest.fn()
|
||||||
|
const {getByLabelText, getByText, queryByText} = render(
|
||||||
|
<ComputerPanel
|
||||||
|
theFile={null}
|
||||||
|
setFile={handleSetFile}
|
||||||
|
hasUploadedFile={false}
|
||||||
|
setHasUploadedFile={handleSetHasUploadedFile}
|
||||||
|
uploadMediaTranslations={makeTranslationProps()}
|
||||||
|
accept="image/*"
|
||||||
|
label="Upload File"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const dropZone = getByLabelText(/Upload File/, {selector: 'input'})
|
||||||
|
fireEvent.change(dropZone, {
|
||||||
|
target: {
|
||||||
|
files: [notAnImageFile]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(getByText('Invalid file type')).toBeVisible()
|
||||||
|
fireEvent.change(dropZone, {
|
||||||
|
target: {
|
||||||
|
files: [aFile]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(queryByText('Invalid file type')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Renders a video player preview if a file type is a video', async () => {
|
||||||
|
global.URL.createObjectURL = jest.fn(() => 'www.blah.com')
|
||||||
|
const handleSetFile = jest.fn()
|
||||||
|
const handleSetHasUploadedFile = jest.fn()
|
||||||
|
const aFile = new File(['foo'], 'foo.mp4', {
|
||||||
|
type: 'video/mp4'
|
||||||
|
})
|
||||||
|
const {getByText} = render(
|
||||||
|
<ComputerPanel
|
||||||
|
theFile={aFile}
|
||||||
|
setFile={handleSetFile}
|
||||||
|
uploadMediaTranslations={makeTranslationProps()}
|
||||||
|
hasUploadedFile
|
||||||
|
setHasUploadedFile={handleSetHasUploadedFile}
|
||||||
|
accept="mp4"
|
||||||
|
label="Upload File"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
const playButton = await waitForElement(() => getByText('Play'))
|
||||||
|
expect(playButton).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import K5Uploader from '@instructure/k5uploader'
|
||||||
|
import moxios from 'moxios'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import saveMediaRecording from '../saveMediaRecording'
|
||||||
|
|
||||||
|
function mediaServerSession() {
|
||||||
|
return {
|
||||||
|
ks: 'averylongstring',
|
||||||
|
subp_id: '0',
|
||||||
|
partner_id: '9',
|
||||||
|
uid: '1234_567',
|
||||||
|
serverTime: 1234,
|
||||||
|
kaltura_setting: {
|
||||||
|
uploadUrl: 'url.url.url',
|
||||||
|
entryUrl: 'url.url.url',
|
||||||
|
uiconfUrl: 'url.url.url',
|
||||||
|
partnerData: 'data from our partners'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('saveMediaRecording', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
moxios.install()
|
||||||
|
})
|
||||||
|
afterEach(() => {
|
||||||
|
moxios.uninstall()
|
||||||
|
})
|
||||||
|
it('fails if request for kaltura session fails', async done => {
|
||||||
|
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
|
||||||
|
status: 500,
|
||||||
|
response: {error: 'womp womp'}
|
||||||
|
})
|
||||||
|
const doneFunction = jest.fn()
|
||||||
|
sinon.stub(K5Uploader.prototype, 'loadUiConf').callsFake(() => 'mock')
|
||||||
|
await saveMediaRecording({}, '1', 'course', doneFunction)
|
||||||
|
expect(doneFunction).toHaveBeenCalledTimes(1)
|
||||||
|
expect(doneFunction.mock.calls[0][0].message).toBe('Request failed with status code 500')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns error if k5.filreError is dispatched', () => {
|
||||||
|
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
|
||||||
|
status: 200,
|
||||||
|
response: mediaServerSession()
|
||||||
|
})
|
||||||
|
const doneFunction = jest.fn()
|
||||||
|
return saveMediaRecording({}, '1', 'course', doneFunction).then(uploader => {
|
||||||
|
uploader.dispatchEvent('K5.fileError', {error: 'womp womp'}, uploader)
|
||||||
|
expect(doneFunction).toHaveBeenCalledTimes(1)
|
||||||
|
expect(doneFunction.mock.calls[0][0].error).toBe('womp womp')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('k5.ready calls uploaders uploadFile with file', () => {
|
||||||
|
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
|
||||||
|
status: 200,
|
||||||
|
response: mediaServerSession()
|
||||||
|
})
|
||||||
|
const doneFunction = jest.fn()
|
||||||
|
const uploadFileFunc = jest.fn()
|
||||||
|
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction).then(uploader => {
|
||||||
|
uploader.uploadFile = uploadFileFunc
|
||||||
|
uploader.dispatchEvent('K5.ready', uploader)
|
||||||
|
expect(uploadFileFunc).toHaveBeenCalledTimes(1)
|
||||||
|
expect(uploadFileFunc.mock.calls[0][0].file).toBe('thing')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('k5.complete calls done with canvasMediaObject data if succeeds', () => {
|
||||||
|
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
|
||||||
|
status: 200,
|
||||||
|
response: mediaServerSession()
|
||||||
|
})
|
||||||
|
moxios.stubRequest('/api/v1/media_objects', {
|
||||||
|
status: 200,
|
||||||
|
response: {data: 'media object data'}
|
||||||
|
})
|
||||||
|
const doneFunction2 = jest.fn()
|
||||||
|
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2).then(
|
||||||
|
async uploader => {
|
||||||
|
uploader.dispatchEvent('K5.complete', {stuff: 'datatatatatatatat'}, uploader)
|
||||||
|
await new Promise(setTimeout)
|
||||||
|
expect(doneFunction2).toHaveBeenCalledTimes(1)
|
||||||
|
expect(doneFunction2.mock.calls[0][0].data).toBe('media object data')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails if request to create media object fails', async () => {
|
||||||
|
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
|
||||||
|
status: 200,
|
||||||
|
response: mediaServerSession()
|
||||||
|
})
|
||||||
|
moxios.stubRequest('/api/v1/media_objects', {
|
||||||
|
status: 500,
|
||||||
|
response: {error: 'womp womp'}
|
||||||
|
})
|
||||||
|
const doneFunction2 = jest.fn()
|
||||||
|
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2).then(
|
||||||
|
async uploader => {
|
||||||
|
uploader.dispatchEvent('K5.complete', {stuff: 'datatatatatatatat'}, uploader)
|
||||||
|
await new Promise(setTimeout)
|
||||||
|
expect(doneFunction2).toHaveBeenCalledTimes(1)
|
||||||
|
expect(doneFunction2.mock.calls[0][0].message).toBe('Request failed with status code 500')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -25,6 +25,7 @@ import {Tabs} from '@instructure/ui-tabs'
|
||||||
import {View} from '@instructure/ui-layout'
|
import {View} from '@instructure/ui-layout'
|
||||||
|
|
||||||
import {ACCEPTED_FILE_TYPES} from './acceptedMediaFileTypes'
|
import {ACCEPTED_FILE_TYPES} from './acceptedMediaFileTypes'
|
||||||
|
import saveMediaRecording from './saveMediaRecording'
|
||||||
import translationShape from './translationShape'
|
import translationShape from './translationShape'
|
||||||
|
|
||||||
const ClosedCaptionPanel = React.lazy(() => import('./ClosedCaptionPanel'))
|
const ClosedCaptionPanel = React.lazy(() => import('./ClosedCaptionPanel'))
|
||||||
|
@ -39,24 +40,6 @@ export const PANELS = {
|
||||||
CLOSED_CAPTIONS: 3
|
CLOSED_CAPTIONS: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleSubmit = (editor, selectedPanel, uploadData, saveMediaRecording, onDismiss) => {
|
|
||||||
switch (selectedPanel) {
|
|
||||||
case PANELS.COMPUTER: {
|
|
||||||
const {theFile} = uploadData
|
|
||||||
saveMediaRecording(theFile, editor, onDismiss)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case PANELS.EMBED: {
|
|
||||||
const {embedCode} = uploadData
|
|
||||||
editor.insertContent(embedCode)
|
|
||||||
onDismiss()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error('Selected Panel is invalid') // Should never get here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UploadMedia extends React.Component {
|
export default class UploadMedia extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
languages: arrayOf(
|
languages: arrayOf(
|
||||||
|
@ -66,8 +49,15 @@ export default class UploadMedia extends React.Component {
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
liveRegion: func,
|
liveRegion: func,
|
||||||
|
contextId: string,
|
||||||
|
contextType: string,
|
||||||
onDismiss: func,
|
onDismiss: func,
|
||||||
open: bool,
|
open: bool,
|
||||||
|
tabs: shape({
|
||||||
|
embed: bool,
|
||||||
|
record: bool,
|
||||||
|
upload: bool
|
||||||
|
}),
|
||||||
uploadMediaTranslations: translationShape
|
uploadMediaTranslations: translationShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +68,26 @@ export default class UploadMedia extends React.Component {
|
||||||
theFile: null
|
theFile: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
switch (this.state.selectedPanel) {
|
||||||
|
case PANELS.COMPUTER: {
|
||||||
|
saveMediaRecording(
|
||||||
|
this.state.theFile,
|
||||||
|
this.props.contextId,
|
||||||
|
this.props.contextType,
|
||||||
|
this.props.onDismiss
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case PANELS.EMBED: {
|
||||||
|
this.props.onDismiss()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Selected Panel is invalid') // Should never get here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderFallbackSpinner = () => {
|
renderFallbackSpinner = () => {
|
||||||
const {LOADING_MEDIA} = this.props.uploadMediaTranslations.UploadMediaStrings
|
const {LOADING_MEDIA} = this.props.uploadMediaTranslations.UploadMediaStrings
|
||||||
return (
|
return (
|
||||||
|
@ -104,51 +114,56 @@ export default class UploadMedia extends React.Component {
|
||||||
maxWidth="large"
|
maxWidth="large"
|
||||||
onRequestTabChange={(_, {index}) => this.setState({selectedPanel: index})}
|
onRequestTabChange={(_, {index}) => this.setState({selectedPanel: index})}
|
||||||
>
|
>
|
||||||
<Tabs.Panel
|
{this.props.tabs.upload && (
|
||||||
isSelected={this.state.selectedPanel === PANELS.COMPUTER}
|
<Tabs.Panel
|
||||||
renderTitle={() => COMPUTER_PANEL_TITLE}
|
isSelected={this.state.selectedPanel === PANELS.COMPUTER}
|
||||||
>
|
renderTitle={() => COMPUTER_PANEL_TITLE}
|
||||||
<Suspense fallback={this.renderFallbackSpinner()}>
|
>
|
||||||
<ComputerPanel
|
<Suspense fallback={this.renderFallbackSpinner()}>
|
||||||
theFile={this.state.theFile}
|
<ComputerPanel
|
||||||
setFile={file => this.setState({theFile: file})}
|
theFile={this.state.theFile}
|
||||||
hasUploadedFile={this.state.hasUploadedFile}
|
setFile={file => this.setState({theFile: file})}
|
||||||
setHasUploadedFile={uploadFileState =>
|
hasUploadedFile={this.state.hasUploadedFile}
|
||||||
this.setState({hasUploadedFile: uploadFileState})
|
setHasUploadedFile={uploadFileState =>
|
||||||
}
|
this.setState({hasUploadedFile: uploadFileState})
|
||||||
label={DRAG_FILE_TEXT}
|
}
|
||||||
uploadMediaTranslations={this.props.uploadMediaTranslations}
|
label={DRAG_FILE_TEXT}
|
||||||
accept={ACCEPTED_FILE_TYPES}
|
uploadMediaTranslations={this.props.uploadMediaTranslations}
|
||||||
/>
|
accept={ACCEPTED_FILE_TYPES}
|
||||||
</Suspense>
|
/>
|
||||||
</Tabs.Panel>
|
</Suspense>
|
||||||
|
</Tabs.Panel>
|
||||||
<Tabs.Panel
|
)}
|
||||||
isSelected={this.state.selectedPanel === PANELS.RECORD}
|
{this.props.tabs.record && (
|
||||||
renderTitle={() => RECORD_PANEL_TITLE}
|
<Tabs.Panel
|
||||||
>
|
isSelected={this.state.selectedPanel === PANELS.RECORD}
|
||||||
<Suspense fallback={this.renderFallbackSpinner()}>
|
renderTitle={() => RECORD_PANEL_TITLE}
|
||||||
<MediaRecorder
|
>
|
||||||
MediaCaptureStrings={this.props.uploadMediaTranslations.MediaCaptureStrings}
|
<Suspense fallback={this.renderFallbackSpinner()}>
|
||||||
errorMessage={UPLOADING_ERROR}
|
<MediaRecorder
|
||||||
dismiss={this.props.onDismiss}
|
MediaCaptureStrings={this.props.uploadMediaTranslations.MediaCaptureStrings}
|
||||||
/>
|
contextType={this.props.contextType}
|
||||||
</Suspense>
|
contextId={this.props.contextId}
|
||||||
</Tabs.Panel>
|
errorMessage={UPLOADING_ERROR}
|
||||||
|
dismiss={this.props.onDismiss}
|
||||||
<Tabs.Panel
|
/>
|
||||||
isSelected={this.state.selectedPanel === PANELS.EMBED}
|
</Suspense>
|
||||||
renderTitle={() => EMBED_PANEL_TITLE}
|
</Tabs.Panel>
|
||||||
>
|
)}
|
||||||
<Suspense fallback={this.renderFallbackSpinner()}>
|
{this.props.tabs.embed && (
|
||||||
<EmbedPanel
|
<Tabs.Panel
|
||||||
label={EMBED_VIDEO_CODE_TEXT}
|
isSelected={this.state.selectedPanel === PANELS.EMBED}
|
||||||
embedCode={this.state.embedCode}
|
renderTitle={() => EMBED_PANEL_TITLE}
|
||||||
setEmbedCode={embedCode => this.setState({embedCode})}
|
>
|
||||||
/>
|
<Suspense fallback={this.renderFallbackSpinner()}>
|
||||||
</Suspense>
|
<EmbedPanel
|
||||||
</Tabs.Panel>
|
label={EMBED_VIDEO_CODE_TEXT}
|
||||||
|
embedCode={this.state.embedCode}
|
||||||
|
setEmbedCode={embedCode => this.setState({embedCode})}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Tabs.Panel>
|
||||||
|
)}
|
||||||
<Tabs.Panel
|
<Tabs.Panel
|
||||||
isSelected={this.state.selectedPanel === PANELS.CLOSED_CAPTIONS}
|
isSelected={this.state.selectedPanel === PANELS.CLOSED_CAPTIONS}
|
||||||
renderTitle={() => CLOSED_CAPTIONS_PANEL_TITLE}
|
renderTitle={() => CLOSED_CAPTIONS_PANEL_TITLE}
|
||||||
|
@ -178,7 +193,7 @@ export default class UploadMedia extends React.Component {
|
||||||
<Button
|
<Button
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleSubmit()
|
this.handleSubmit()
|
||||||
}}
|
}}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 - present Instructure, Inc.
|
||||||
|
*
|
||||||
|
* This file is part of Canvas.
|
||||||
|
*
|
||||||
|
* Canvas is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import K5Uploader from '@instructure/k5uploader'
|
||||||
|
|
||||||
|
export const VIDEO_SIZE_OPTIONS = {height: '432px', width: '768px'}
|
||||||
|
|
||||||
|
function generateUploadOptions(mediatypes, sessionData) {
|
||||||
|
const sessionDataCopy = JSON.parse(JSON.stringify(sessionData))
|
||||||
|
delete sessionDataCopy.kaltura_setting
|
||||||
|
return {
|
||||||
|
kaltura_session: sessionDataCopy,
|
||||||
|
allowedMediaTypes: mediatypes,
|
||||||
|
uploadUrl: sessionData.kaltura_setting.uploadUrl,
|
||||||
|
entryUrl: sessionData.kaltura_setting.entryUrl,
|
||||||
|
uiconfUrl: sessionData.kaltura_setting.uiconfUrl,
|
||||||
|
entryDefaults: {
|
||||||
|
partnerData: sessionData.kaltura_setting.partner_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUploaderReadyEventListeners(uploader, file) {
|
||||||
|
uploader.addEventListener('K5.ready', () => {
|
||||||
|
uploader.uploadFile(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUploaderFileErrorEventListeners(uploader, done) {
|
||||||
|
uploader.addEventListener('K5.fileError', error => {
|
||||||
|
done(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUploaderFileCompleteEventListeners(uploader, context, done) {
|
||||||
|
uploader.addEventListener('K5.complete', async mediaServerMediaObject => {
|
||||||
|
mediaServerMediaObject.contextCode = `${context.contextType}_${context.contextId}`
|
||||||
|
mediaServerMediaObject.type = `${context.contextType}_${context.contextId}`
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
id: mediaServerMediaObject.entryId,
|
||||||
|
type:
|
||||||
|
{2: 'image', 5: 'audio'}[mediaServerMediaObject.mediaType] ||
|
||||||
|
mediaServerMediaObject.type.includes('audio')
|
||||||
|
? 'audio'
|
||||||
|
: 'video',
|
||||||
|
context_code: mediaServerMediaObject.contextCode,
|
||||||
|
title: mediaServerMediaObject.title,
|
||||||
|
user_entered_title: mediaServerMediaObject.userTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvasMediaObject = await axios.post('/api/v1/media_objects', body)
|
||||||
|
done(canvasMediaObject.data)
|
||||||
|
} catch (e) {
|
||||||
|
done(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function saveMediaRecording(file, contextId, contextType, done) {
|
||||||
|
try {
|
||||||
|
const mediaServerSession = await axios.post(
|
||||||
|
'/api/v1/services/kaltura_session?include_upload_config=1'
|
||||||
|
)
|
||||||
|
const session = generateUploadOptions(
|
||||||
|
['video', 'audio', 'webm', 'video/webm', 'audio/webm'],
|
||||||
|
mediaServerSession.data
|
||||||
|
)
|
||||||
|
const k5UploaderSession = new K5Uploader(session)
|
||||||
|
addUploaderReadyEventListeners(k5UploaderSession, file)
|
||||||
|
addUploaderFileErrorEventListeners(k5UploaderSession, done)
|
||||||
|
addUploaderFileCompleteEventListeners(k5UploaderSession, {contextId, contextType}, done)
|
||||||
|
return k5UploaderSession
|
||||||
|
} catch (err) {
|
||||||
|
done(err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue