Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

audio: new @uppy/audio plugin for recording with microphone #2976

Merged
merged 35 commits into from Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
893f921
add Audio plugin
arturi Jun 29, 2021
d13b3ba
Merge branch 'main' into audio-record
arturi Nov 12, 2021
9f2c578
add audio-oscilloscope to visualize the recording
arturi Nov 25, 2021
3f64c9e
refactor: rename everything to Audio, use oscilloscope, re-init when …
arturi Nov 25, 2021
1d1dcfc
tweak styles
arturi Nov 25, 2021
6e2a4eb
add @uppy/audio to the Uppy bundle
arturi Nov 25, 2021
4d81aef
update Readme and package.json
arturi Nov 25, 2021
12adb13
add docs, update locales, add website example
arturi Nov 25, 2021
52982ab
webcam plugin also shouldn’t show recording length counter on video p…
arturi Nov 25, 2021
b9d3f47
update package.json and yarn.lock
arturi Nov 25, 2021
48a5bd1
update types
arturi Nov 26, 2021
79649cd
update locale
arturi Nov 26, 2021
9c08dd5
fix locale issues
arturi Nov 26, 2021
a0de4e7
remove leftover webcam test
arturi Nov 26, 2021
f0fc9a4
Merge branch 'main' into audio-record
arturi Nov 26, 2021
f4ec431
Delete index.test-d.ts
arturi Nov 26, 2021
424e4f9
Revert "Delete index.test-d.ts"
arturi Nov 26, 2021
5cd6a34
fix lint and type tests
arturi Nov 26, 2021
c66adba
Update website/src/docs/audio.md
arturi Nov 29, 2021
a091dba
Update packages/@uppy/audio/src/audio-oscilloscope/index.js
arturi Nov 29, 2021
f7f28fe
Update packages/@uppy/audio/src/DiscardButton.js
arturi Nov 29, 2021
ebd123f
Update packages/@uppy/audio/src/audio-oscilloscope/index.js
arturi Nov 29, 2021
de2e024
Update packages/@uppy/audio/src/supportsMediaRecorder.js
arturi Nov 29, 2021
4d4d5ec
Update website/src/docs/audio.md
arturi Nov 29, 2021
677fea4
Update packages/@uppy/audio/types/index.d.ts
arturi Nov 30, 2021
deb2c07
Update packages/@uppy/audio/src/index.js
arturi Nov 30, 2021
b81cbe4
Update packages/@uppy/audio/src/index.js
arturi Nov 30, 2021
b5bf3a6
Update packages/@uppy/audio/src/index.js
arturi Nov 30, 2021
bb02c01
remove unused method
arturi Nov 30, 2021
9857ffd
remove unused commented declarations
arturi Dec 1, 2021
4a7be8c
make all methods private
arturi Dec 3, 2021
3d35b89
Merge branch 'main' into audio-record
arturi Dec 3, 2021
b44136d
convert class component to hooks
arturi Dec 6, 2021
a4d3d75
more private
arturi Dec 6, 2021
77a960d
fix lint
arturi Dec 6, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/dev/Dashboard.js
Expand Up @@ -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
Expand Down Expand Up @@ -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 })
Expand Down
21 changes: 21 additions & 0 deletions 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.
37 changes: 37 additions & 0 deletions packages/@uppy/audio/README.md
@@ -0,0 +1,37 @@
# @uppy/audio

<img src="https://uppy.io/images/logos/uppy-dog-head-arrow.svg" width="120" alt="Uppy logo: a superman puppy in a pink suit" align="right">

<a href="https://www.npmjs.com/package/@uppy/audio"><img src="https://img.shields.io/npm/v/@uppy/webcam.svg?style=flat-square"></a> <img src="https://github.com/transloadit/uppy/workflows/Tests/badge.svg" alt="CI status for Uppy tests"> <img src="https://github.com/transloadit/uppy/workflows/Companion/badge.svg" alt="CI status for Companion tests"> <img src="https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg" alt="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).
37 changes: 37 additions & 0 deletions 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"
}
}
22 changes: 22 additions & 0 deletions packages/@uppy/audio/src/AudioSourceSelect.js
@@ -0,0 +1,22 @@
const { h } = require('preact')

module.exports = ({ currentDeviceId, audioSources, onChangeSource }) => {
return (
<div className="uppy-Audio-videoSource">
<select
className="uppy-u-reset uppy-Audio-audioSource-select"
onChange={(event) => { onChangeSource(event.target.value) }}
>
{audioSources.map((audioSource) => (
<option
key={audioSource.deviceId}
value={audioSource.deviceId}
selected={audioSource.deviceId === currentDeviceId}
>
{audioSource.label}
</option>
))}
</select>
</div>
)
}
30 changes: 30 additions & 0 deletions packages/@uppy/audio/src/DiscardButton.js
@@ -0,0 +1,30 @@
const { h } = require('preact')

function DiscardButton ({ onDiscard, i18n }) {
return (
<button
className="uppy-u-reset uppy-c-btn uppy-Audio-button"
type="button"
title={i18n('discardRecordedFile')}
aria-label={i18n('discardRecordedFile')}
onClick={onDiscard}
data-uppy-super-focusable
>
<svg
width="13"
height="13"
viewBox="0 0 13 13"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className="uppy-c-icon"
>
<g fill="#FFF" fillRule="evenodd">
<path d="M.496 11.367L11.103.76l1.414 1.414L1.911 12.781z" />
<path d="M11.104 12.782L.497 2.175 1.911.76l10.607 10.606z" />
</g>
</svg>
</button>
)
}

module.exports = DiscardButton
12 changes: 12 additions & 0 deletions packages/@uppy/audio/src/PermissionsScreen.js
@@ -0,0 +1,12 @@
const { h } = require('preact')

module.exports = (props) => {
const { icon, hasAudio, i18n } = props
return (
<div className="uppy-Audio-permissons">
<div className="uppy-Audio-permissonsIcon">{icon()}</div>
<h1 className="uppy-Audio-title">{hasAudio ? i18n('allowAudioAccessTitle') : i18n('noAudioTitle')}</h1>
<p>{hasAudio ? i18n('allowAudioAccessDescription') : i18n('noAudioDescription')}</p>
</div>
)
}
35 changes: 35 additions & 0 deletions 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 (
<button
className="uppy-u-reset uppy-c-btn uppy-Audio-button"
type="button"
title={i18n('stopAudioRecording')}
aria-label={i18n('stopAudioRecording')}
onClick={onStopRecording}
data-uppy-super-focusable
>
<svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="100" height="100" viewBox="0 0 100 100">
<rect x="15" y="15" width="70" height="70" />
</svg>
</button>
)
}

return (
<button
className="uppy-u-reset uppy-c-btn uppy-Audio-button"
type="button"
title={i18n('startAudioRecording')}
aria-label={i18n('startAudioRecording')}
onClick={onStartRecording}
data-uppy-super-focusable
>
<svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="14px" height="20px" viewBox="0 0 14 20">
<path d="M7 14c2.21 0 4-1.71 4-3.818V3.818C11 1.71 9.21 0 7 0S3 1.71 3 3.818v6.364C3 12.29 4.79 14 7 14zm6.364-7h-.637a.643.643 0 0 0-.636.65V9.6c0 3.039-2.565 5.477-5.6 5.175-2.645-.264-4.582-2.692-4.582-5.407V7.65c0-.36-.285-.65-.636-.65H.636A.643.643 0 0 0 0 7.65v1.631c0 3.642 2.544 6.888 6.045 7.382v1.387H3.818a.643.643 0 0 0-.636.65v.65c0 .36.285.65.636.65h6.364c.351 0 .636-.29.636-.65v-.65c0-.36-.285-.65-.636-.65H7.955v-1.372C11.363 16.2 14 13.212 14 9.6V7.65c0-.36-.285-.65-.636-.65z" fill="#FFF" fill-rule="nonzero" />
</svg>
</button>
)
}
12 changes: 12 additions & 0 deletions 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 (
<span aria-label={i18n('recordingLength', { recording_length: formattedRecordingLengthSeconds })}>
{formattedRecordingLengthSeconds}
</span>
)
}
116 changes: 116 additions & 0 deletions 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 (
<div className="uppy-Audio-container">
<div className="uppy-Audio-audioContainer">
{hasRecordedAudio
? (
<audio
className="uppy-Audio-player"
controls
src={recordedAudio}
/>
) : (
<canvas
ref={canvasEl}
className="uppy-Audio-canvas"
/>
)}
</div>
<div className="uppy-Audio-footer">
<div className="uppy-Audio-audioSourceContainer">
{shouldShowAudioSourceDropdown
? AudioSourceSelect(props)
: null}
</div>
<div className="uppy-Audio-buttonContainer">
{shouldShowRecordButton && (
<RecordButton
recording={recording}
onStartRecording={onStartRecording}
onStopRecording={onStopRecording}
i18n={i18n}
/>
)}

{hasRecordedAudio && <SubmitButton onSubmit={onSubmit} i18n={i18n} />}

{hasRecordedAudio && <DiscardButton onDiscard={onDiscardRecordedAudio} i18n={i18n} />}
</div>

<div className="uppy-Audio-recordingLength">
{!hasRecordedAudio && (
<RecordingLength recordingLengthSeconds={recordingLengthSeconds} i18n={i18n} />
)}
</div>
</div>
</div>
)
}
28 changes: 28 additions & 0 deletions packages/@uppy/audio/src/SubmitButton.js
@@ -0,0 +1,28 @@
const { h } = require('preact')

function SubmitButton ({ onSubmit, i18n }) {
return (
<button
className="uppy-u-reset uppy-c-btn uppy-Audio-button uppy-Audio-button--submit"
type="button"
title={i18n('submitRecordedFile')}
aria-label={i18n('submitRecordedFile')}
onClick={onSubmit}
data-uppy-super-focusable
>
<svg
width="12"
height="9"
viewBox="0 0 12 9"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
className="uppy-c-icon"
>
<path fill="#fff" fillRule="nonzero" d="M10.66 0L12 1.31 4.136 9 0 4.956l1.34-1.31L4.136 6.38z" />
</svg>
</button>
)
}

module.exports = SubmitButton