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)