From ef5ba9c8fc3435a8121c8bbef943abbbbc8ec004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 18 Oct 2020 13:21:59 +0200 Subject: [PATCH] webcam: add video source selector (#2492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * webcam: add showVideoSourceDropdown * webcam: update video device list when adding/removing a video device * webcam: cleaner changes * delete extraneous file * webcam: store sources data in state * webcam: regather sources after getting access * webcam: put the source selector next to the buttons * webcam: put the video source selection on its own row in mobile view * website: show webcam video source dropdown * enable webcam features in dev example * facingMode takes precedence over deviceId, and not needed when specific device is selected fixes issue where on mobile (iOS) the device selection was ignored due to facingMode being set to 'user' * Remove “video source” copy, style select, deal with iOS zoom-in issue Co-authored-by: Tsy-Jon Lau Co-authored-by: Artur Paikin --- examples/dev/Dashboard.js | 6 +- packages/@uppy/webcam/src/CameraScreen.js | 20 +++-- packages/@uppy/webcam/src/RecordingLength.js | 4 +- .../@uppy/webcam/src/VideoSourceSelect.js | 21 +++++ packages/@uppy/webcam/src/index.js | 81 ++++++++++++++++-- packages/@uppy/webcam/src/style.scss | 83 +++++++++++++++---- packages/@uppy/webcam/types/index.d.ts | 1 + website/src/docs/webcam.md | 4 + website/src/examples/dashboard/app.es6 | 5 +- 9 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 packages/@uppy/webcam/src/VideoSourceSelect.js 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)