diff --git a/examples/dev/Dashboard.js b/examples/dev/Dashboard.js index 579b3e7f0..5cc6e4e74 100644 --- a/examples/dev/Dashboard.js +++ b/examples/dev/Dashboard.js @@ -70,7 +70,11 @@ module.exports = () => { .use(OneDrive, { target: Dashboard, companionUrl: COMPANION_URL }) .use(Zoom, { target: Dashboard, companionUrl: COMPANION_URL }) .use(Url, { target: Dashboard, companionUrl: COMPANION_URL }) - .use(Webcam, { target: Dashboard }) + .use(Webcam, { + target: Dashboard, + showVideoSourceDropdown: true, + showRecordingLength: true + }) .use(ScreenCapture, { target: Dashboard }) .use(Form, { target: '#upload-form' }) .use(ImageEditor, { target: Dashboard }) diff --git a/packages/@uppy/webcam/src/CameraScreen.js b/packages/@uppy/webcam/src/CameraScreen.js index 7bcbf055f..a453df926 100644 --- a/packages/@uppy/webcam/src/CameraScreen.js +++ b/packages/@uppy/webcam/src/CameraScreen.js @@ -2,6 +2,7 @@ const { h, Component } = require('preact') const SnapshotButton = require('./SnapshotButton') const RecordButton = require('./RecordButton') const RecordingLength = require('./RecordingLength') +const VideoSourceSelect = require('./VideoSourceSelect') function isModeAvailable (modes, mode) { return modes.indexOf(mode) !== -1 @@ -24,18 +25,25 @@ class CameraScreen extends Component { ) const shouldShowSnapshotButton = isModeAvailable(this.props.modes, 'picture') const shouldShowRecordingLength = this.props.supportsRecording && this.props.showRecordingLength + const shouldShowVideoSourceDropdown = this.props.showVideoSourceDropdown && this.props.videoSources && this.props.videoSources.length > 1 return (
-
- {shouldShowRecordingLength ? RecordingLength(this.props) : null} - {' '} - {shouldShowSnapshotButton ? SnapshotButton(this.props) : null} - {' '} - {shouldShowRecordButton ? RecordButton(this.props) : null} +
) diff --git a/packages/@uppy/webcam/src/RecordingLength.js b/packages/@uppy/webcam/src/RecordingLength.js index 8fe6465d8..6df07558b 100644 --- a/packages/@uppy/webcam/src/RecordingLength.js +++ b/packages/@uppy/webcam/src/RecordingLength.js @@ -5,8 +5,8 @@ module.exports = function RecordingLength ({ recordingLengthSeconds, i18n }) { const formattedRecordingLengthSeconds = formatSeconds(recordingLengthSeconds) return ( -
+ {formattedRecordingLengthSeconds} -
+ ) } diff --git a/packages/@uppy/webcam/src/VideoSourceSelect.js b/packages/@uppy/webcam/src/VideoSourceSelect.js new file mode 100644 index 000000000..1644a8aa3 --- /dev/null +++ b/packages/@uppy/webcam/src/VideoSourceSelect.js @@ -0,0 +1,21 @@ +const { h } = require('preact') + +module.exports = ({ currentDeviceId, videoSources, onChangeVideoSource }) => { + return ( +
+ +
+ ) +} diff --git a/packages/@uppy/webcam/src/index.js b/packages/@uppy/webcam/src/index.js index 2ae7e79df..45f7dc10c 100644 --- a/packages/@uppy/webcam/src/index.js +++ b/packages/@uppy/webcam/src/index.js @@ -115,6 +115,7 @@ module.exports = class Webcam extends Plugin { 'picture' ], mirror: true, + showVideoSourceDropdown: false, facingMode: 'user', preferredImageMimeType: null, preferredVideoMimeType: null, @@ -138,12 +139,22 @@ module.exports = class Webcam extends Plugin { this._stopRecording = this._stopRecording.bind(this) this._oneTwoThreeSmile = this._oneTwoThreeSmile.bind(this) this._focus = this._focus.bind(this) + this._changeVideoSource = this._changeVideoSource.bind(this) this.webcamActive = false if (this.opts.countdown) { this.opts.onBeforeSnapshot = this._oneTwoThreeSmile } + + this.setPluginState({ + hasCamera: false, + cameraReady: false, + cameraError: null, + recordingLengthSeconds: 0, + videoSources: [], + currentDeviceId: null + }) } setOptions (newOpts) { @@ -176,15 +187,18 @@ module.exports = class Webcam extends Plugin { }) } - getConstraints () { + getConstraints (deviceId = null) { const acceptsAudio = this.opts.modes.indexOf('video-audio') !== -1 || this.opts.modes.indexOf('audio-only') !== -1 const acceptsVideo = this.opts.modes.indexOf('video-audio') !== -1 || this.opts.modes.indexOf('video-only') !== -1 || this.opts.modes.indexOf('picture') !== -1 - const videoConstraints = this.opts.videoConstraints ?? { - facingMode: this.opts.facingMode + const videoConstraints = { + ...(this.opts.videoConstraints ?? { facingMode: this.opts.facingMode }), + // facingMode takes precedence over deviceId, and not needed + // when specific device is selected + ...(deviceId ? { deviceId, facingMode: null } : {}) } return { @@ -193,13 +207,14 @@ module.exports = class Webcam extends Plugin { } } - _start () { + _start (options = null) { if (!this.supportsUserMedia) { return Promise.reject(new Error('Webcam access not supported')) } this.webcamActive = true - const constraints = this.getConstraints() + + const constraints = this.getConstraints(options && options.deviceId ? options.deviceId : null) this.hasCameraCheck().then(hasCamera => { this.setPluginState({ @@ -210,7 +225,23 @@ module.exports = class Webcam extends Plugin { return this.mediaDevices.getUserMedia(constraints) .then((stream) => { this.stream = stream + + let currentDeviceId = null + if (!options || !options.deviceId) { + currentDeviceId = stream.getVideoTracks()[0].getSettings().deviceId + } else { + stream.getVideoTracks().forEach((videoTrack) => { + if (videoTrack.getSettings().deviceId === options.deviceId) { + currentDeviceId = videoTrack.getSettings().deviceId + } + }) + } + + // Update the sources now, so we can access the names. + this.updateVideoSources() + this.setPluginState({ + currentDeviceId, cameraReady: true }) }) @@ -465,6 +496,19 @@ module.exports = class Webcam extends Plugin { }, 1000) } + _changeVideoSource (deviceId) { + this._stop() + this._start({ deviceId: deviceId }) + } + + updateVideoSources () { + this.mediaDevices.enumerateDevices().then(devices => { + this.setPluginState({ + videoSources: devices.filter((device) => device.kind === 'videoinput') + }) + }) + } + render () { if (!this.webcamActive) { this._start() @@ -485,6 +529,7 @@ module.exports = class Webcam extends Plugin { return ( { + this.updateVideoSources() + + if (this.stream) { + let restartStream = true + + const { videoSources, currentDeviceId } = this.getPluginState() + + videoSources.forEach((videoSource) => { + if (currentDeviceId === videoSource.deviceId) { + restartStream = false + } + }) + + if (restartStream) { + this._stop() + this._start() + } + } + } + } } uninstall () { diff --git a/packages/@uppy/webcam/src/style.scss b/packages/@uppy/webcam/src/style.scss index fc6f45b16..83e70f4a4 100644 --- a/packages/@uppy/webcam/src/style.scss +++ b/packages/@uppy/webcam/src/style.scss @@ -31,18 +31,76 @@ margin: auto; } - .uppy-Webcam-video--mirrored { - transform: scaleX(-1); +.uppy-Webcam-video--mirrored { + transform: scaleX(-1); +} + +.uppy-Webcam-footer { + width: 100%; + min-height: 75px; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 20px 20px; +} + +.uppy-Webcam-videoSourceContainer { + width: 100%; + flex-grow: 0; +} + +.uppy-size--lg .uppy-Webcam-videoSourceContainer { + width: 33%; + margin: 0; // vertical alignment handled by the flexbox wrapper +} + +.uppy-Webcam-videoSource-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; + + .uppy-size--md & { + font-size: 14px; + margin-bottom: 0; + } +} + + .uppy-Webcam-videoSource-select::-ms-expand { + display: none; } .uppy-Webcam-buttonContainer { - width: 100%; - height: 75px; - border-top: 1px solid $gray-200; - display: flex; - align-items: center; - justify-content: center; - padding: 0 20px; + width: 50%; + margin-left: 25%; + text-align: center; +} + +.uppy-size--lg .uppy-Webcam-buttonContainer { + width: 34%; + margin-left: 0; +} + +.uppy-Webcam-recordingLength { + width: 25%; + flex-grow: 0; + color: $gray-600; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + text-align: right; +} + +.uppy-size--lg .uppy-Webcam-recordingLength { + width: 33%; } .uppy-Webcam-button { @@ -110,13 +168,6 @@ margin-bottom: 30px; } -.uppy-Webcam-recordingLength { - position: absolute; - right: 20px; - color: $gray-600; - font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - .uppy-Webcam-title { font-size: 22px; line-height: 1.35; diff --git a/packages/@uppy/webcam/types/index.d.ts b/packages/@uppy/webcam/types/index.d.ts index 11ee9406b..c8685e26f 100644 --- a/packages/@uppy/webcam/types/index.d.ts +++ b/packages/@uppy/webcam/types/index.d.ts @@ -15,6 +15,7 @@ declare module Webcam { countdown?: number | boolean mirror?: boolean facingMode?: string + showVideoSourceDropdown?: boolean modes?: WebcamMode[] locale?: WebcamLocale title?: string diff --git a/website/src/docs/webcam.md b/website/src/docs/webcam.md index e764ca625..402bc573c 100644 --- a/website/src/docs/webcam.md +++ b/website/src/docs/webcam.md @@ -132,6 +132,10 @@ For a full list of available properties, see MDN's [MediaTrackConstraints][] doc [`height`]: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/height [`facingMode`]: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode +### `showVideoSourceDropdown: false` + +Configures whether or not to show a dropdown which enables to choose the video device to use. This option will have priority over `facingMode` if enabled. The default is `false`. + ### `showRecordingLength: false` Configures whether or not to show the length of the recording while the recording is in progress. The default is `false`. diff --git a/website/src/examples/dashboard/app.es6 b/website/src/examples/dashboard/app.es6 index 1eb8dcb8b..2d60f0e9e 100644 --- a/website/src/examples/dashboard/app.es6 +++ b/website/src/examples/dashboard/app.es6 @@ -145,7 +145,10 @@ function uppySetOptions () { const webcamInstance = window.uppy.getPlugin('Webcam') if (opts.Webcam && !webcamInstance) { - window.uppy.use(Webcam, { target: Dashboard }) + window.uppy.use(Webcam, { + target: Dashboard, + showVideoSourceDropdown: true + }) } if (!opts.Webcam && webcamInstance) { window.uppy.removePlugin(webcamInstance)