From d202a8e94094139e6742b721f1c22c73cd1af131 Mon Sep 17 00:00:00 2001 From: Artur Paikin Date: Mon, 6 Dec 2021 17:06:51 +0000 Subject: [PATCH] audio: new @uppy/audio plugin for recording with microphone (#2976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add Audio plugin * add audio-oscilloscope to visualize the recording * refactor: rename everything to Audio, use oscilloscope, re-init when appropriate, improved preview screen * tweak styles * add @uppy/audio to the Uppy bundle * update Readme and package.json * add docs, update locales, add website example * webcam plugin also shouldn’t show recording length counter on video preview screen * update package.json and yarn.lock * update types * update locale * fix locale issues * remove leftover webcam test * Delete index.test-d.ts * Revert "Delete index.test-d.ts" This reverts commit f4ec431f6aee8cfb533eac6b2b00fecfc4286230. * fix lint and type tests * Update website/src/docs/audio.md Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/audio-oscilloscope/index.js Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/DiscardButton.js Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/audio-oscilloscope/index.js Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/supportsMediaRecorder.js Co-authored-by: Antoine du Hamel * Update website/src/docs/audio.md Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/types/index.d.ts Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/index.js Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/index.js Co-authored-by: Antoine du Hamel * Update packages/@uppy/audio/src/index.js Co-authored-by: Antoine du Hamel * remove unused method * remove unused commented declarations * make all methods private * convert class component to hooks * more private * fix lint Co-authored-by: Antoine du Hamel --- examples/dev/Dashboard.js | 5 + packages/@uppy/audio/LICENSE | 21 + packages/@uppy/audio/README.md | 37 ++ packages/@uppy/audio/package.json | 37 ++ packages/@uppy/audio/src/AudioSourceSelect.js | 22 ++ packages/@uppy/audio/src/DiscardButton.js | 30 ++ packages/@uppy/audio/src/PermissionsScreen.js | 12 + packages/@uppy/audio/src/RecordButton.js | 35 ++ packages/@uppy/audio/src/RecordingLength.js | 12 + packages/@uppy/audio/src/RecordingScreen.js | 116 ++++++ packages/@uppy/audio/src/SubmitButton.js | 28 ++ .../audio/src/audio-oscilloscope/LICENCE | 21 + .../audio/src/audio-oscilloscope/index.js | 84 ++++ packages/@uppy/audio/src/formatSeconds.js | 12 + .../@uppy/audio/src/formatSeconds.test.js | 11 + packages/@uppy/audio/src/index.js | 369 ++++++++++++++++++ packages/@uppy/audio/src/locale.js | 30 ++ packages/@uppy/audio/src/style.scss | 193 +++++++++ .../@uppy/audio/src/supportsMediaRecorder.js | 6 + .../audio/src/supportsMediaRecorder.test.js | 23 ++ packages/@uppy/audio/types/index.d.ts | 12 + packages/@uppy/audio/types/index.test-d.ts | 10 + packages/@uppy/core/src/_variables.scss | 1 + packages/@uppy/locales/src/en_US.js | 7 + packages/@uppy/webcam/src/CameraScreen.js | 10 +- packages/uppy/index.js | 1 + packages/uppy/index.mjs | 1 + packages/uppy/package.json | 1 + packages/uppy/src/style.scss | 1 + website/src/docs/audio.md | 102 +++++ website/src/examples/dashboard/app.es6 | 12 + website/src/examples/dashboard/app.html | 2 + website/src/examples/dashboard/index.ejs | 1 + yarn.lock | 12 + 34 files changed, 1272 insertions(+), 5 deletions(-) create mode 100644 packages/@uppy/audio/LICENSE create mode 100644 packages/@uppy/audio/README.md create mode 100644 packages/@uppy/audio/package.json create mode 100644 packages/@uppy/audio/src/AudioSourceSelect.js create mode 100644 packages/@uppy/audio/src/DiscardButton.js create mode 100644 packages/@uppy/audio/src/PermissionsScreen.js create mode 100644 packages/@uppy/audio/src/RecordButton.js create mode 100644 packages/@uppy/audio/src/RecordingLength.js create mode 100644 packages/@uppy/audio/src/RecordingScreen.js create mode 100644 packages/@uppy/audio/src/SubmitButton.js create mode 100644 packages/@uppy/audio/src/audio-oscilloscope/LICENCE create mode 100644 packages/@uppy/audio/src/audio-oscilloscope/index.js create mode 100644 packages/@uppy/audio/src/formatSeconds.js create mode 100644 packages/@uppy/audio/src/formatSeconds.test.js create mode 100644 packages/@uppy/audio/src/index.js create mode 100644 packages/@uppy/audio/src/locale.js create mode 100644 packages/@uppy/audio/src/style.scss create mode 100644 packages/@uppy/audio/src/supportsMediaRecorder.js create mode 100644 packages/@uppy/audio/src/supportsMediaRecorder.test.js create mode 100644 packages/@uppy/audio/types/index.d.ts create mode 100644 packages/@uppy/audio/types/index.test-d.ts create mode 100644 website/src/docs/audio.md diff --git a/examples/dev/Dashboard.js b/examples/dev/Dashboard.js index 0798c15e3b..884175dc0a 100644 --- a/examples/dev/Dashboard.js +++ b/examples/dev/Dashboard.js @@ -22,6 +22,7 @@ const Transloadit = require('@uppy/transloadit/src') const Form = require('@uppy/form/src') const ImageEditor = require('@uppy/image-editor/src') const DropTarget = require('@uppy/drop-target/src') +const Audio = require('@uppy/audio/src') /* eslint-enable import/no-extraneous-dependencies */ // DEV CONFIG: pick an uploader @@ -90,6 +91,10 @@ module.exports = () => { showVideoSourceDropdown: true, showRecordingLength: true, }) + .use(Audio, { + target: Dashboard, + showRecordingLength: true, + }) .use(ScreenCapture, { target: Dashboard }) .use(Form, { target: '#upload-form' }) .use(ImageEditor, { target: Dashboard }) diff --git a/packages/@uppy/audio/LICENSE b/packages/@uppy/audio/LICENSE new file mode 100644 index 0000000000..6b40b25cff --- /dev/null +++ b/packages/@uppy/audio/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@uppy/audio/README.md b/packages/@uppy/audio/README.md new file mode 100644 index 0000000000..81e9a4e5a3 --- /dev/null +++ b/packages/@uppy/audio/README.md @@ -0,0 +1,37 @@ +# @uppy/audio + +Uppy logo: a superman puppy in a pink suit + + CI status for Uppy tests CI status for Companion tests CI status for browser tests + +The Audio plugin for Uppy lets you record audio using a built-in or external microphone, or any other audio device, on desktop and mobile. + +Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service. + +## Example + +```js +import Uppy from '@uppy/core' +import Webcam from '@uppy/audio' + +const uppy = new Uppy() +uppy.use(Audio) +``` + +## Installation + +```bash +$ npm install @uppy/audio +``` + +We recommend installing from npm and then using a module bundler such as [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Rollup.js](http://rollupjs.org/). + +Alternatively, you can also use this plugin in a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object. See the [main Uppy documentation](https://uppy.io/docs/#Installation) for instructions. + +## Documentation + +Documentation for this plugin can be found on the [Uppy website](https://uppy.io/docs/webcam). + +## License + +[The MIT License](./LICENSE). diff --git a/packages/@uppy/audio/package.json b/packages/@uppy/audio/package.json new file mode 100644 index 0000000000..e7d070fb03 --- /dev/null +++ b/packages/@uppy/audio/package.json @@ -0,0 +1,37 @@ +{ + "name": "@uppy/audio", + "description": "Uppy plugin that records audio using the device’s microphone.", + "version": "0.1.0", + "license": "MIT", + "main": "lib/index.js", + "style": "dist/style.min.css", + "types": "types/index.d.ts", + "keywords": [ + "file uploader", + "uppy", + "uppy-plugin", + "audio", + "microphone", + "sound", + "record", + "mediarecorder" + ], + "homepage": "https://uppy.io", + "bugs": { + "url": "https://github.com/transloadit/uppy/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/transloadit/uppy.git" + }, + "dependencies": { + "@uppy/utils": "workspace:^", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "workspace:^" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@uppy/audio/src/AudioSourceSelect.js b/packages/@uppy/audio/src/AudioSourceSelect.js new file mode 100644 index 0000000000..5b420bc64f --- /dev/null +++ b/packages/@uppy/audio/src/AudioSourceSelect.js @@ -0,0 +1,22 @@ +const { h } = require('preact') + +module.exports = ({ currentDeviceId, audioSources, onChangeSource }) => { + return ( +
+ +
+ ) +} diff --git a/packages/@uppy/audio/src/DiscardButton.js b/packages/@uppy/audio/src/DiscardButton.js new file mode 100644 index 0000000000..e46d857029 --- /dev/null +++ b/packages/@uppy/audio/src/DiscardButton.js @@ -0,0 +1,30 @@ +const { h } = require('preact') + +function DiscardButton ({ onDiscard, i18n }) { + return ( + + ) +} + +module.exports = DiscardButton diff --git a/packages/@uppy/audio/src/PermissionsScreen.js b/packages/@uppy/audio/src/PermissionsScreen.js new file mode 100644 index 0000000000..88dae35a92 --- /dev/null +++ b/packages/@uppy/audio/src/PermissionsScreen.js @@ -0,0 +1,12 @@ +const { h } = require('preact') + +module.exports = (props) => { + const { icon, hasAudio, i18n } = props + return ( +
+
{icon()}
+

{hasAudio ? i18n('allowAudioAccessTitle') : i18n('noAudioTitle')}

+

{hasAudio ? i18n('allowAudioAccessDescription') : i18n('noAudioDescription')}

+
+ ) +} diff --git a/packages/@uppy/audio/src/RecordButton.js b/packages/@uppy/audio/src/RecordButton.js new file mode 100644 index 0000000000..34bb972679 --- /dev/null +++ b/packages/@uppy/audio/src/RecordButton.js @@ -0,0 +1,35 @@ +const { h } = require('preact') + +module.exports = function RecordButton ({ recording, onStartRecording, onStopRecording, i18n }) { + if (recording) { + return ( + + ) + } + + return ( + + ) +} diff --git a/packages/@uppy/audio/src/RecordingLength.js b/packages/@uppy/audio/src/RecordingLength.js new file mode 100644 index 0000000000..6df07558b7 --- /dev/null +++ b/packages/@uppy/audio/src/RecordingLength.js @@ -0,0 +1,12 @@ +const { h } = require('preact') +const formatSeconds = require('./formatSeconds') + +module.exports = function RecordingLength ({ recordingLengthSeconds, i18n }) { + const formattedRecordingLengthSeconds = formatSeconds(recordingLengthSeconds) + + return ( + + {formattedRecordingLengthSeconds} + + ) +} diff --git a/packages/@uppy/audio/src/RecordingScreen.js b/packages/@uppy/audio/src/RecordingScreen.js new file mode 100644 index 0000000000..93bea524bf --- /dev/null +++ b/packages/@uppy/audio/src/RecordingScreen.js @@ -0,0 +1,116 @@ +/* eslint-disable jsx-a11y/media-has-caption */ +const { h } = require('preact') +const { useEffect, useRef } = require('preact/hooks') +const RecordButton = require('./RecordButton') +const RecordingLength = require('./RecordingLength') +const AudioSourceSelect = require('./AudioSourceSelect') +const AudioOscilloscope = require('./audio-oscilloscope') +const SubmitButton = require('./SubmitButton') +const DiscardButton = require('./DiscardButton') + +module.exports = function RecordingScreen (props) { + const { + stream, + recordedAudio, + onStop, + recording, + supportsRecording, + audioSources, + showAudioSourceDropdown, + onSubmit, + i18n, + onStartRecording, + onStopRecording, + onDiscardRecordedAudio, + recordingLengthSeconds, + } = props + + const canvasEl = useRef(null) + const oscilloscope = useRef(null) + + // componentDidMount / componentDidUnmount + useEffect(() => { + return () => { + oscilloscope.current = null + onStop() + } + }, [onStop]) + + // componentDidUpdate + useEffect(() => { + if (!recordedAudio) { + oscilloscope.current = new AudioOscilloscope(canvasEl.current, { + canvas: { + width: 600, + height: 600, + }, + canvasContext: { + lineWidth: 2, + fillStyle: 'rgb(0,0,0)', + strokeStyle: 'green', + }, + }) + oscilloscope.current.draw() + + if (stream) { + const audioContext = new AudioContext() + const source = audioContext.createMediaStreamSource(stream) + oscilloscope.current.addSource(source) + } + } + }, [recordedAudio, stream]) + + const hasRecordedAudio = recordedAudio != null + const shouldShowRecordButton = !hasRecordedAudio && supportsRecording + const shouldShowAudioSourceDropdown = showAudioSourceDropdown + && !hasRecordedAudio + && audioSources + && audioSources.length > 1 + + return ( +
+
+ {hasRecordedAudio + ? ( +
+
+
+ {shouldShowAudioSourceDropdown + ? AudioSourceSelect(props) + : null} +
+
+ {shouldShowRecordButton && ( + + )} + + {hasRecordedAudio && } + + {hasRecordedAudio && } +
+ +
+ {!hasRecordedAudio && ( + + )} +
+
+
+ ) +} diff --git a/packages/@uppy/audio/src/SubmitButton.js b/packages/@uppy/audio/src/SubmitButton.js new file mode 100644 index 0000000000..8ce046783b --- /dev/null +++ b/packages/@uppy/audio/src/SubmitButton.js @@ -0,0 +1,28 @@ +const { h } = require('preact') + +function SubmitButton ({ onSubmit, i18n }) { + return ( + + ) +} + +module.exports = SubmitButton diff --git a/packages/@uppy/audio/src/audio-oscilloscope/LICENCE b/packages/@uppy/audio/src/audio-oscilloscope/LICENCE new file mode 100644 index 0000000000..eb0736f3a5 --- /dev/null +++ b/packages/@uppy/audio/src/audio-oscilloscope/LICENCE @@ -0,0 +1,21 @@ +MIT license + +Copyright (C) 2015 Miguel Mota + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@uppy/audio/src/audio-oscilloscope/index.js b/packages/@uppy/audio/src/audio-oscilloscope/index.js new file mode 100644 index 0000000000..c4a89dd346 --- /dev/null +++ b/packages/@uppy/audio/src/audio-oscilloscope/index.js @@ -0,0 +1,84 @@ +function isFunction (v) { + return typeof v === 'function' +} + +function result (v) { + return isFunction(v) ? v() : v +} + +/* Audio Oscilloscope + https://github.com/miguelmota/audio-oscilloscope +*/ +module.exports = class AudioOscilloscope { + constructor (canvas, options = {}) { + const canvasOptions = options.canvas || {} + const canvasContextOptions = options.canvasContext || {} + this.analyser = null + this.bufferLength = 0 + this.dataArray = [] + this.canvas = canvas + this.width = result(canvasOptions.width) || this.canvas.width + this.height = result(canvasOptions.height) || this.canvas.height + this.canvas.width = this.width + this.canvas.height = this.height + this.canvasContext = this.canvas.getContext('2d') + this.canvasContext.fillStyle = result(canvasContextOptions.fillStyle) || 'rgb(255, 255, 255)' + this.canvasContext.strokeStyle = result(canvasContextOptions.strokeStyle) || 'rgb(0, 0, 0)' + this.canvasContext.lineWidth = result(canvasContextOptions.lineWidth) || 1 + this.onDrawFrame = isFunction(options.onDrawFrame) ? options.onDrawFrame : () => {} + } + + addSource (streamSource) { + this.streamSource = streamSource + this.audioContext = this.streamSource.context + this.analyser = this.audioContext.createAnalyser() + this.analyser.fftSize = 2048 + this.bufferLength = this.analyser.frequencyBinCount + this.source = this.audioContext.createBufferSource() + this.dataArray = new Uint8Array(this.bufferLength) + this.analyser.getByteTimeDomainData(this.dataArray) + this.streamSource.connect(this.analyser) + } + + draw () { + const { analyser, dataArray, bufferLength } = this + const ctx = this.canvasContext + const w = this.width + const h = this.height + + if (analyser) { + analyser.getByteTimeDomainData(dataArray) + } + + ctx.fillRect(0, 0, w, h) + ctx.beginPath() + + const sliceWidth = (w * 1.0) / bufferLength + let x = 0 + + if (!bufferLength) { + ctx.moveTo(0, this.height / 2) + } + + for (let i = 0; i < bufferLength; i++) { + const v = dataArray[i] / 128.0 + const y = v * (h / 2) + + if (i === 0) { + ctx.moveTo(x, y) + } else { + ctx.lineTo(x, y) + } + + x += sliceWidth + } + + ctx.lineTo(w, h / 2) + ctx.stroke() + + this.onDrawFrame(this) + requestAnimationFrame(this.#draw) + } + + #draw = () => this.draw() +} diff --git a/packages/@uppy/audio/src/formatSeconds.js b/packages/@uppy/audio/src/formatSeconds.js new file mode 100644 index 0000000000..e88878f4b2 --- /dev/null +++ b/packages/@uppy/audio/src/formatSeconds.js @@ -0,0 +1,12 @@ +/** + * Takes an Integer value of seconds (e.g. 83) and converts it into a human-readable formatted string (e.g. '1:23'). + * + * @param {Integer} seconds + * @returns {string} the formatted seconds (e.g. '1:23' for 1 minute and 23 seconds) + * + */ +module.exports = function formatSeconds (seconds) { + return `${Math.floor( + seconds / 60, + )}:${String(seconds % 60).padStart(2, 0)}` +} diff --git a/packages/@uppy/audio/src/formatSeconds.test.js b/packages/@uppy/audio/src/formatSeconds.test.js new file mode 100644 index 0000000000..da21c08cd4 --- /dev/null +++ b/packages/@uppy/audio/src/formatSeconds.test.js @@ -0,0 +1,11 @@ +const formatSeconds = require('./formatSeconds') + +describe('formatSeconds', () => { + it('should return a value of \'0:43\' when an argument of 43 seconds is supplied', () => { + expect(formatSeconds(43)).toEqual('0:43') + }) + + it('should return a value of \'1:43\' when an argument of 103 seconds is supplied', () => { + expect(formatSeconds(103)).toEqual('1:43') + }) +}) diff --git a/packages/@uppy/audio/src/index.js b/packages/@uppy/audio/src/index.js new file mode 100644 index 0000000000..95307c3b8c --- /dev/null +++ b/packages/@uppy/audio/src/index.js @@ -0,0 +1,369 @@ +const { h } = require('preact') +const { UIPlugin } = require('@uppy/core') +const getFileTypeExtension = require('@uppy/utils/lib/getFileTypeExtension') +const supportsMediaRecorder = require('./supportsMediaRecorder') +const RecordingScreen = require('./RecordingScreen') +const PermissionsScreen = require('./PermissionsScreen') +const locale = require('./locale.js') + +/** + * Audio recording plugin + */ +module.exports = class Audio extends UIPlugin { + static VERSION = require('../package.json').version + + #stream = null + + #audioActive = false + + #recordingChunks = null + + #recorder = null + + #capturedMediaFile = null + + #mediaDevices = null + + #supportsUserMedia = null + + constructor (uppy, opts) { + super(uppy, opts) + this.#mediaDevices = navigator.mediaDevices + this.#supportsUserMedia = this.#mediaDevices != null + this.id = this.opts.id || 'Audio' + this.type = 'acquirer' + this.icon = () => ( + + ) + + this.defaultLocale = locale + + this.opts = { ...opts } + + this.i18nInit() + this.title = this.i18n('pluginNameAudio') + + this.setPluginState({ + hasAudio: false, + audioReady: false, + cameraError: null, + recordingLengthSeconds: 0, + audioSources: [], + currentDeviceId: null, + }) + } + + #hasAudioCheck () { + if (!this.#mediaDevices) { + return Promise.resolve(false) + } + + return this.#mediaDevices.enumerateDevices().then(devices => { + return devices.some(device => device.kind === 'audioinput') + }) + } + + // eslint-disable-next-line consistent-return + #start = (options = null) => { + if (!this.#supportsUserMedia) { + return Promise.reject(new Error('Microphone access not supported')) + } + + this.#audioActive = true + + this.#hasAudioCheck().then(hasAudio => { + this.setPluginState({ + hasAudio, + }) + + // ask user for access to their camera + return this.#mediaDevices.getUserMedia({ audio: true }) + .then((stream) => { + this.#stream = stream + + let currentDeviceId = null + const tracks = stream.getAudioTracks() + + if (!options || !options.deviceId) { + currentDeviceId = tracks[0].getSettings().deviceId + } else { + tracks.forEach((track) => { + if (track.getSettings().deviceId === options.deviceId) { + currentDeviceId = track.getSettings().deviceId + } + }) + } + + // Update the sources now, so we can access the names. + this.#updateSources() + + this.setPluginState({ + currentDeviceId, + audioReady: true, + }) + }) + .catch((err) => { + this.setPluginState({ + audioReady: false, + cameraError: err, + }) + this.uppy.info(err.message, 'error') + }) + }) + } + + #startRecording = () => { + // only used if supportsMediaRecorder() returned true + // eslint-disable-next-line compat/compat + this.#recorder = new MediaRecorder(this.#stream) + this.#recordingChunks = [] + let stoppingBecauseOfMaxSize = false + this.#recorder.addEventListener('dataavailable', (event) => { + this.#recordingChunks.push(event.data) + + const { restrictions } = this.uppy.opts + if (this.#recordingChunks.length > 1 + && restrictions.maxFileSize != null + && !stoppingBecauseOfMaxSize) { + const totalSize = this.#recordingChunks.reduce((acc, chunk) => acc + chunk.size, 0) + // Exclude the initial chunk from the average size calculation because it is likely to be a very small outlier + const averageChunkSize = (totalSize - this.#recordingChunks[0].size) / (this.#recordingChunks.length - 1) + const expectedEndChunkSize = averageChunkSize * 3 + const maxSize = Math.max(0, restrictions.maxFileSize - expectedEndChunkSize) + + if (totalSize > maxSize) { + stoppingBecauseOfMaxSize = true + this.uppy.info(this.i18n('recordingStoppedMaxSize'), 'warning', 4000) + this.#stopRecording() + } + } + }) + + // use a "time slice" of 500ms: ondataavailable will be called each 500ms + // smaller time slices mean we can more accurately check the max file size restriction + this.#recorder.start(500) + + // Start the recordingLengthTimer if we are showing the recording length. + this.recordingLengthTimer = setInterval(() => { + const currentRecordingLength = this.getPluginState().recordingLengthSeconds + this.setPluginState({ recordingLengthSeconds: currentRecordingLength + 1 }) + }, 1000) + + this.setPluginState({ + isRecording: true, + }) + } + + #stopRecording = () => { + const stopped = new Promise((resolve) => { + this.#recorder.addEventListener('stop', () => { + resolve() + }) + this.#recorder.stop() + + if (this.opts.showRecordingLength) { + // Stop the recordingLengthTimer if we are showing the recording length. + clearInterval(this.recordingLengthTimer) + this.setPluginState({ recordingLengthSeconds: 0 }) + } + }) + + return stopped.then(() => { + this.setPluginState({ + isRecording: false, + }) + return this.#getAudio() + }).then((file) => { + try { + this.#capturedMediaFile = file + // create object url for capture result preview + this.setPluginState({ + recordedAudio: URL.createObjectURL(file.data), + }) + } catch (err) { + // Logging the error, exept restrictions, which is handled in Core + if (!err.isRestriction) { + this.uppy.log(err) + } + } + }).then(() => { + this.#recordingChunks = null + this.#recorder = null + }, (error) => { + this.#recordingChunks = null + this.#recorder = null + throw error + }) + } + + #discardRecordedAudio = () => { + this.setPluginState({ recordedAudio: null }) + this.#capturedMediaFile = null + } + + #submit = () => { + try { + if (this.#capturedMediaFile) { + this.uppy.addFile(this.#capturedMediaFile) + } + } catch (err) { + // Logging the error, exept restrictions, which is handled in Core + if (!err.isRestriction) { + this.uppy.log(err, 'error') + } + } + } + + #stop = async () => { + if (this.#stream) { + const audioTracks = this.#stream.getAudioTracks() + audioTracks.forEach((track) => track.stop()) + } + + if (this.#recorder) { + await new Promise((resolve) => { + this.#recorder.addEventListener('stop', resolve, { once: true }) + this.#recorder.stop() + + if (this.opts.showRecordingLength) { + clearInterval(this.recordingLengthTimer) + } + }) + } + + this.#recordingChunks = null + this.#recorder = null + this.#audioActive = false + this.#stream = null + + this.setPluginState({ + recordedAudio: null, + isRecording: false, + recordingLengthSeconds: 0, + }) + } + + #getAudio () { + // Sometimes in iOS Safari, Blobs (especially the first Blob in the recordingChunks Array) + // have empty 'type' attributes (e.g. '') so we need to find a Blob that has a defined 'type' + // attribute in order to determine the correct MIME type. + const mimeType = this.#recordingChunks.find(blob => blob.type?.length > 0).type + + const fileExtension = getFileTypeExtension(mimeType) + + if (!fileExtension) { + return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`)) + } + + const name = `audio-${Date.now()}.${fileExtension}` + const blob = new Blob(this.#recordingChunks, { type: mimeType }) + const file = { + source: this.id, + name, + data: new Blob([blob], { type: mimeType }), + type: mimeType, + } + + return Promise.resolve(file) + } + + #changeSource = (deviceId) => { + this.#stop() + this.#start({ deviceId }) + } + + #updateSources = () => { + this.#mediaDevices.enumerateDevices().then(devices => { + this.setPluginState({ + audioSources: devices.filter((device) => device.kind === 'audioinput'), + }) + }) + } + + render () { + if (!this.#audioActive) { + this.#start() + } + + const audioState = this.getPluginState() + + if (!audioState.audioReady || !audioState.hasAudio) { + return ( + + ) + } + + return ( + + ) + } + + install () { + this.setPluginState({ + audioReady: false, + recordingLengthSeconds: 0, + }) + + const { target } = this.opts + if (target) { + this.mount(target, this) + } + + if (this.#mediaDevices) { + this.#updateSources() + + this.#mediaDevices.ondevicechange = () => { + this.#updateSources() + + if (this.#stream) { + let restartStream = true + + const { audioSources, currentDeviceId } = this.getPluginState() + + audioSources.forEach((audioSource) => { + if (currentDeviceId === audioSource.deviceId) { + restartStream = false + } + }) + + if (restartStream) { + this.#stop() + this.#start() + } + } + } + } + } + + uninstall () { + if (this.#stream) { + this.#stop() + } + + this.unmount() + } +} diff --git a/packages/@uppy/audio/src/locale.js b/packages/@uppy/audio/src/locale.js new file mode 100644 index 0000000000..6bde98f694 --- /dev/null +++ b/packages/@uppy/audio/src/locale.js @@ -0,0 +1,30 @@ +module.exports = { + strings: { + pluginNameAudio: 'Audio', + // Used as the label for the button that starts an audio recording. + // This is not visibly rendered but is picked up by screen readers. + startAudioRecording: 'Begin audio recording', + // Used as the label for the button that stops an audio recording. + // This is not visibly rendered but is picked up by screen readers. + stopAudioRecording: 'Stop audio recording', + // Title on the “allow access” screen + allowAudioAccessTitle: 'Please allow access to your microphone', + // Description on the “allow access” screen + allowAudioAccessDescription: 'In order to record audio, please allow microphone access for this site.', + // Title on the “device not available” screen + noAudioTitle: 'Microphone Not Available', + // Description on the “device not available” screen + noAudioDescription: 'In order to record audio, please connect a microphone or another audio input device', + // Message about file size will be shown in an Informer bubble + recordingStoppedMaxSize: 'Recording stopped because the file size is about to exceed the limit', + // Used as the label for the counter that shows recording length (`1:25`). + // This is not visibly rendered but is picked up by screen readers. + recordingLength: 'Recording length %{recording_length}', + // Used as the label for the submit checkmark button. + // This is not visibly rendered but is picked up by screen readers. + submitRecordedFile: 'Submit recorded file', + // Used as the label for the discard cross button. + // This is not visibly rendered but is picked up by screen readers. + discardRecordedFile: 'Discard recorded file', + }, +} diff --git a/packages/@uppy/audio/src/style.scss b/packages/@uppy/audio/src/style.scss new file mode 100644 index 0000000000..e5f9ed6a64 --- /dev/null +++ b/packages/@uppy/audio/src/style.scss @@ -0,0 +1,193 @@ +@import '@uppy/core/src/_utils.scss'; +@import '@uppy/core/src/_variables.scss'; + +.uppy-Audio-container { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.uppy-Audio-audioContainer { + display: flex; + width: 100%; + height: 100%; + background-color: $gray-300; + position: relative; + justify-content: center; + align-items: center; +} + +.uppy-Audio-player { + width: 85%; + border-radius: 12px; +} + +.uppy-Audio-canvas { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.uppy-Audio-footer { + width: 100%; + // min-height: 75px; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 20px 20px; +} + +.uppy-Audio-audioSourceContainer { + width: 100%; + flex-grow: 0; +} + +.uppy-size--lg .uppy-Audio-audioSourceContainer { + width: 33%; + margin: 0; // vertical alignment handled by the flexbox wrapper +} + +.uppy-Audio-audioSource-select { + display: block; + font-size: 16px; + line-height: 1.2; + padding: .4em 1em .3em .4em; + width: 100%; + max-width: 90%; + border: 1px solid $gray-600; + background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23757575%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); + background-repeat: no-repeat; + background-position: right .4em top 50%, 0 0; + background-size: .65em auto, 100%; + margin: auto; + margin-bottom: 10px; + white-space: nowrap; + text-overflow: ellipsis; + + .uppy-size--lg & { + font-size: 14px; + margin-bottom: 0; + } +} + + .uppy-Audio-audioSource-select::-ms-expand { + display: none; + } + +.uppy-Audio-buttonContainer { + width: 50%; + margin-left: 25%; + text-align: center; + flex: 1; +} + +.uppy-size--lg .uppy-Audio-buttonContainer { + width: 34%; + margin-left: 0; +} + +.uppy-Audio-recordingLength { + width: 25%; + flex-grow: 0; + color: $gray-600; + font-family: $font-family-mono; + text-align: right; +} + +.uppy-size--lg .uppy-Audio-recordingLength { + width: 33%; +} + +.uppy-Audio-button { + @include blue-border-focus; + width: 45px; + height: 45px; + border-radius: 50%; + background-color: $red; + color: $white; + cursor: pointer; + transition: all 0.3s; + + &:hover { + background-color: darken($red, 5%); + } + + [data-uppy-theme="dark"] & { + @include blue-border-focus--dark; + } +} + +.uppy-Audio-button--submit { + background-color: $blue; + margin: 0 12px; + + &:hover { + background-color: darken($blue, 5%); + } +} + +.uppy-Audio-button svg { + width: 26px; + height: 26px; + max-width: 100%; + max-height: 100%; + display: inline-block; + vertical-align: text-top; + overflow: hidden; + fill: currentColor; +} + +.uppy-size--md .uppy-Audio-button { + width: 60px; + height: 60px; +} + +.uppy-Audio-permissons { + padding: 15px; + display: flex; + align-items: center; + justify-content: center; + flex-flow: column wrap; + height: 100%; + flex: 1; +} + +.uppy-Audio-permissons p { + max-width: 450px; + line-height: 1.3; + text-align: center; + line-height: 1.45; + color: $gray-500; + margin: 0; +} + +.uppy-Audio-permissonsIcon svg { + width: 100px; + height: 75px; + color: $gray-400; + margin-bottom: 30px; +} + +.uppy-Audio-title { + font-size: 22px; + line-height: 1.35; + font-weight: 400; + margin: 0; + margin-bottom: 5px; + padding: 0 15px; + max-width: 500px; + text-align: center; + color: $gray-800; + + [data-uppy-theme="dark"] & { + color: $gray-200; + } +} diff --git a/packages/@uppy/audio/src/supportsMediaRecorder.js b/packages/@uppy/audio/src/supportsMediaRecorder.js new file mode 100644 index 0000000000..ee1eec0cf1 --- /dev/null +++ b/packages/@uppy/audio/src/supportsMediaRecorder.js @@ -0,0 +1,6 @@ +module.exports = function supportsMediaRecorder () { + /* eslint-disable compat/compat */ + return typeof MediaRecorder === 'function' + && typeof MediaRecorder.prototype?.start === 'function' + /* eslint-enable compat/compat */ +} diff --git a/packages/@uppy/audio/src/supportsMediaRecorder.test.js b/packages/@uppy/audio/src/supportsMediaRecorder.test.js new file mode 100644 index 0000000000..1f9699cc59 --- /dev/null +++ b/packages/@uppy/audio/src/supportsMediaRecorder.test.js @@ -0,0 +1,23 @@ +const supportsMediaRecorder = require('./supportsMediaRecorder') + +describe('supportsMediaRecorder', () => { + it('should return true if MediaRecorder is supported', () => { + global.MediaRecorder = class MediaRecorder { + start () {} // eslint-disable-line + } + expect(supportsMediaRecorder()).toEqual(true) + }) + + it('should return false if MediaRecorder is not supported', () => { + global.MediaRecorder = undefined + expect(supportsMediaRecorder()).toEqual(false) + + global.MediaRecorder = class MediaRecorder {} + expect(supportsMediaRecorder()).toEqual(false) + + global.MediaRecorder = class MediaRecorder { + foo () {} // eslint-disable-line + } + expect(supportsMediaRecorder()).toEqual(false) + }) +}) diff --git a/packages/@uppy/audio/types/index.d.ts b/packages/@uppy/audio/types/index.d.ts new file mode 100644 index 0000000000..042e6b99bd --- /dev/null +++ b/packages/@uppy/audio/types/index.d.ts @@ -0,0 +1,12 @@ +import type { PluginOptions, UIPlugin, PluginTarget } from '@uppy/core' +import type AudioLocale from './generatedLocale' + +export interface AudioOptions extends PluginOptions { + target?: PluginTarget + showVideoSourceDropdown?: boolean + locale?: AudioLocale +} + +declare class Audio extends UIPlugin {} + +export default Audio diff --git a/packages/@uppy/audio/types/index.test-d.ts b/packages/@uppy/audio/types/index.test-d.ts new file mode 100644 index 0000000000..cf9da1c108 --- /dev/null +++ b/packages/@uppy/audio/types/index.test-d.ts @@ -0,0 +1,10 @@ +import Uppy from '@uppy/core' +import Audio from '..' + +{ + const uppy = new Uppy() + + uppy.use(Audio, { + target: 'body', + }) +} diff --git a/packages/@uppy/core/src/_variables.scss b/packages/@uppy/core/src/_variables.scss index 0a317df604..9e7a972def 100644 --- a/packages/@uppy/core/src/_variables.scss +++ b/packages/@uppy/core/src/_variables.scss @@ -1,5 +1,6 @@ // Fonts $font-family-base: -apple-system, blinkmacsystemfont, 'Segoe UI', helvetica, arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !default; +$font-family-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default; // Colors diff --git a/packages/@uppy/locales/src/en_US.js b/packages/@uppy/locales/src/en_US.js index 751b6b276a..9c576f7f46 100644 --- a/packages/@uppy/locales/src/en_US.js +++ b/packages/@uppy/locales/src/en_US.js @@ -11,6 +11,8 @@ en_US.strings = { allFilesFromFolderNamed: 'All files from folder %{name}', allowAccessDescription: 'In order to take pictures or record video with your camera, please allow camera access for this site.', allowAccessTitle: 'Please allow access to your camera', + allowAudioAccessDescription: 'In order to record audio, please allow microphone access for this site.', + allowAudioAccessTitle: 'Please allow access to your microphone', aspectRatioLandscape: 'Crop landscape (16:9)', aspectRatioPortrait: 'Crop portrait (9:16)', aspectRatioSquare: 'Crop square', @@ -86,6 +88,8 @@ en_US.strings = { '1': 'Missing required meta fields: %{fields}.', }, myDevice: 'My Device', + noAudioDescription: 'In order to record audio, please connect a microphone or another audio input device', + noAudioTitle: 'Microphone Not Available', noCameraDescription: 'In order to take pictures or record video, please connect a camera device', noCameraTitle: 'Camera Not Available', noDuplicates: 'Cannot add the duplicate file \'%{fileName}\', it already exists', @@ -96,6 +100,7 @@ en_US.strings = { pause: 'Pause', paused: 'Paused', pauseUpload: 'Pause upload', + pluginNameAudio: 'Audio', pluginNameBox: 'Box', pluginNameCamera: 'Camera', pluginNameDropbox: 'Dropbox', @@ -136,8 +141,10 @@ en_US.strings = { sessionRestored: 'Session restored', signInWithGoogle: 'Sign in with Google', smile: 'Smile!', + startAudioRecording: 'Begin audio recording', startCapturing: 'Begin screen capturing', startRecording: 'Begin video recording', + stopAudioRecording: 'Stop audio recording', stopCapturing: 'Stop screen capturing', stopRecording: 'Stop video recording', streamActive: 'Stream active', diff --git a/packages/@uppy/webcam/src/CameraScreen.js b/packages/@uppy/webcam/src/CameraScreen.js index ab47d9f283..55a81af39f 100644 --- a/packages/@uppy/webcam/src/CameraScreen.js +++ b/packages/@uppy/webcam/src/CameraScreen.js @@ -49,7 +49,7 @@ class CameraScreen extends Component { || isModeAvailable(modes, 'video-audio') ) const shouldShowSnapshotButton = !hasRecordedVideo && isModeAvailable(modes, 'picture') - const shouldShowRecordingLength = supportsRecording && showRecordingLength + const shouldShowRecordingLength = supportsRecording && showRecordingLength && !hasRecordedVideo const shouldShowVideoSourceDropdown = showVideoSourceDropdown && videoSources && videoSources.length > 1 const videoProps = { @@ -105,11 +105,11 @@ class CameraScreen extends Component { {hasRecordedVideo && } - {shouldShowRecordingLength && ( -
+
+ {shouldShowRecordingLength && ( -
- )} + )} +
) diff --git a/packages/uppy/index.js b/packages/uppy/index.js index 45579260e1..ca74319ae9 100644 --- a/packages/uppy/index.js +++ b/packages/uppy/index.js @@ -35,6 +35,7 @@ exports.Unsplash = require('@uppy/unsplash') exports.Url = require('@uppy/url') exports.Webcam = require('@uppy/webcam') exports.ScreenCapture = require('@uppy/screen-capture') +exports.Audio = require('@uppy/audio') // Uploaders exports.AwsS3 = require('@uppy/aws-s3') diff --git a/packages/uppy/index.mjs b/packages/uppy/index.mjs index be01efd7ea..a42fb39bbe 100644 --- a/packages/uppy/index.mjs +++ b/packages/uppy/index.mjs @@ -31,6 +31,7 @@ export { default as Unsplash } from '@uppy/unsplash' export { default as Url } from '@uppy/url' export { default as Webcam } from '@uppy/webcam' export { default as ScreenCapture } from '@uppy/screen-capture' +export { default as Audio } from '@uppy/audio' // Uploaders export { default as AwsS3 } from '@uppy/aws-s3' diff --git a/packages/uppy/package.json b/packages/uppy/package.json index f95f72856b..44a0a81bb8 100644 --- a/packages/uppy/package.json +++ b/packages/uppy/package.json @@ -31,6 +31,7 @@ "url": "git+https://github.com/transloadit/uppy.git" }, "dependencies": { + "@uppy/audio": "workspace:^", "@uppy/aws-s3": "workspace:^", "@uppy/aws-s3-multipart": "workspace:^", "@uppy/box": "workspace:^", diff --git a/packages/uppy/src/style.scss b/packages/uppy/src/style.scss index 8f1c191eda..9e716c7224 100644 --- a/packages/uppy/src/style.scss +++ b/packages/uppy/src/style.scss @@ -8,6 +8,7 @@ @import '@uppy/status-bar/src/style.scss'; @import '@uppy/url/src/style.scss'; @import '@uppy/webcam/src/style.scss'; +@import '@uppy/audio/src/style.scss'; @import '@uppy/screen-capture/src/style.scss'; @import '@uppy/image-editor/src/style.scss'; @import '@uppy/drop-target/src/style.scss'; diff --git a/website/src/docs/audio.md b/website/src/docs/audio.md new file mode 100644 index 0000000000..616210fe40 --- /dev/null +++ b/website/src/docs/audio.md @@ -0,0 +1,102 @@ +--- +type: docs +order: 3 +title: "Audio" +module: "@uppy/audio" +permalink: docs/audio/ +category: "Sources" +tagline: "upload audio recordings" +--- + +The `@uppy/audio` plugin lets you record audio using a built-in or external microphone, or any other audio device, on desktop and mobile. + +```js +import Audio from '@uppy/audio' + +uppy.use(Audio, { + // Options +}) +``` + +Try it live + +## Installation + +This plugin is published as the `@uppy/audio` package. + +Install from NPM: + +```shell +npm install @uppy/audio +``` + +In the [CDN package](/docs/#With-a-script-tag), the plugin class is available on the `Uppy` global object: + +```js +const { Audio } = Uppy +``` + +## CSS + +The `@uppy/audio` plugin requires the following CSS for styling: + +```js +import '@uppy/core/dist/style.css' +import '@uppy/audio/dist/style.css' +``` + +Import general Core styles from `@uppy/core/dist/style.css` first, then add the Webcam styles from `@uppy/audio/dist/style.css`. A minified version is also available as `style.min.css` at the same path. The way to do import depends on your build system. + +## Options + +The `@uppy/audio` plugin has the following configurable options: + +### `id: 'Audio'` + +A unique identifier for this plugin. It defaults to `'Audio'`. + +### `target: null` + +DOM element, CSS selector, or plugin to mount Audio into. + +### `showAudioSourceDropdown: false` + +Configures whether to show a dropdown which enables to choose the audio device to use. The default is `false`. + +### `locale: {}` + + + +```js +module.exports = { + strings: { + pluginNameAudio: 'Audio', + // Used as the label for the button that starts an audio recording. + // This is not visibly rendered but is picked up by screen readers. + startAudioRecording: 'Begin audio recording', + // Used as the label for the button that stops an audio recording. + // This is not visibly rendered but is picked up by screen readers. + stopAudioRecording: 'Stop audio recording', + // Title on the “allow access” screen + allowAudioAccessTitle: 'Please allow access to your microphone', + // Description on the “allow access” screen + allowAudioAccessDescription: 'In order to record audio, please allow microphone access for this site.', + // Title on the “device not available” screen + noAudioTitle: 'Microphone Not Available', + // Description on the “device not available” screen + noAudioDescription: 'In order to record audio, please connect a microphone or another audio input device', + // Message about file size will be shown in an Informer bubble + recordingStoppedMaxSize: 'Recording stopped because the file size is about to exceed the limit', + // Used as the label for the counter that shows recording length (`1:25`). + // This is not visibly rendered but is picked up by screen readers. + recordingLength: 'Recording length %{recording_length}', + // Used as the label for the submit checkmark button. + // This is not visibly rendered but is picked up by screen readers. + submitRecordedFile: 'Submit recorded file', + // Used as the label for the discard cross button. + // This is not visibly rendered but is picked up by screen readers. + discardRecordedFile: 'Discard recorded file', + }, +} + +``` diff --git a/website/src/examples/dashboard/app.es6 b/website/src/examples/dashboard/app.es6 index 3a48910fdd..3c6d4bef0e 100644 --- a/website/src/examples/dashboard/app.es6 +++ b/website/src/examples/dashboard/app.es6 @@ -10,6 +10,7 @@ const Zoom = require('@uppy/zoom') const ImageEditor = require('@uppy/image-editor') const Url = require('@uppy/url') const Webcam = require('@uppy/webcam') +const Audio = require('@uppy/audio') const ScreenCapture = require('@uppy/screen-capture') const Tus = require('@uppy/tus') const DropTarget = require('@uppy/drop-target') @@ -166,6 +167,17 @@ function uppySetOptions () { window.uppy.removePlugin(webcamInstance) } + const audioInstance = window.uppy.getPlugin('Audio') + if (opts.Audio && !audioInstance) { + window.uppy.use(Audio, { + target: Dashboard, + showAudioSourceDropdown: true, + }) + } + if (!opts.Audio && audioInstance) { + window.uppy.removePlugin(audioInstance) + } + const screenCaptureInstance = window.uppy.getPlugin('ScreenCapture') if (opts.ScreenCapture && !screenCaptureInstance) { window.uppy.use(ScreenCapture, { target: Dashboard }) diff --git a/website/src/examples/dashboard/app.html b/website/src/examples/dashboard/app.html index d8accf3a91..8a3bd9a456 100644 --- a/website/src/examples/dashboard/app.html +++ b/website/src/examples/dashboard/app.html @@ -13,6 +13,7 @@
  • +
  • @@ -44,6 +45,7 @@ var optionInputs = { DashboardInline: document.querySelector('#opts-DashboardInline'), Webcam: document.querySelector('#opts-Webcam'), + Audio: document.querySelector('#opts-Audio'), ScreenCapture: document.querySelector('#opts-ScreenCapture'), GoogleDrive: document.querySelector('#opts-GoogleDrive'), Dropbox: document.querySelector('#opts-Dropbox'), diff --git a/website/src/examples/dashboard/index.ejs b/website/src/examples/dashboard/index.ejs index 0e083bb50b..1f1f48cc89 100644 --- a/website/src/examples/dashboard/index.ejs +++ b/website/src/examples/dashboard/index.ejs @@ -69,6 +69,7 @@ const uppy = new Uppy({ .use(Facebook, { target: Dashboard, companionUrl: 'https://companion.uppy.io' }) .use(OneDrive, { target: Dashboard, companionUrl: 'https://companion.uppy.io' }) .use(Webcam, { target: Dashboard }) +.use(Audio, { target: Dashboard }) .use(ScreenCapture, { target: Dashboard }) .use(ImageEditor, { target: Dashboard }) .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' }) diff --git a/yarn.lock b/yarn.lock index 48c3a20d2d..3e099abe5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8572,6 +8572,17 @@ __metadata: languageName: unknown linkType: soft +"@uppy/audio@workspace:^, @uppy/audio@workspace:packages/@uppy/audio": + version: 0.0.0-use.local + resolution: "@uppy/audio@workspace:packages/@uppy/audio" + dependencies: + "@uppy/utils": "workspace:^" + preact: ^10.5.13 + peerDependencies: + "@uppy/core": "workspace:^" + languageName: unknown + linkType: soft + "@uppy/aws-s3-multipart@workspace:^, @uppy/aws-s3-multipart@workspace:packages/@uppy/aws-s3-multipart": version: 0.0.0-use.local resolution: "@uppy/aws-s3-multipart@workspace:packages/@uppy/aws-s3-multipart" @@ -42405,6 +42416,7 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis: version: 0.0.0-use.local resolution: "uppy@workspace:packages/uppy" dependencies: + "@uppy/audio": "workspace:^" "@uppy/aws-s3": "workspace:^" "@uppy/aws-s3-multipart": "workspace:^" "@uppy/box": "workspace:^"