uppy
Version:
Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:
355 lines (303 loc) • 9.64 kB
JavaScript
const { h } = require('preact')
const Plugin = require('../../core/Plugin')
const Translator = require('../../core/Translator')
const {
getFileTypeExtension,
canvasToBlob
} = require('../../core/Utils')
const supportsMediaRecorder = require('./supportsMediaRecorder')
const WebcamIcon = require('./WebcamIcon')
const CameraScreen = require('./CameraScreen')
const PermissionsScreen = require('./PermissionsScreen')
// Setup getUserMedia, with polyfill for older browsers
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
function getMediaDevices () {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return navigator.mediaDevices
}
const getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia
if (!getUserMedia) {
return null
}
return {
getUserMedia (opts) {
return new Promise((resolve, reject) => {
getUserMedia.call(navigator, opts, resolve, reject)
})
}
}
}
/**
* Webcam
*/
module.exports = class Webcam extends Plugin {
constructor (uppy, opts) {
super(uppy, opts)
this.mediaDevices = getMediaDevices()
this.supportsUserMedia = !!this.mediaDevices
this.protocol = location.protocol.match(/https/i) ? 'https' : 'http'
this.id = this.opts.id || 'Webcam'
this.title = 'Webcam'
this.type = 'acquirer'
this.icon = WebcamIcon
const defaultLocale = {
strings: {
smile: 'Smile!'
}
}
// set default options
const defaultOptions = {
onBeforeSnapshot: () => Promise.resolve(),
countdown: false,
locale: defaultLocale,
modes: [
'video-audio',
'video-only',
'audio-only',
'picture'
],
mirror: true,
facingMode: 'user'
}
// merge default options with the ones set by user
this.opts = Object.assign({}, defaultOptions, opts)
this.locale = Object.assign({}, defaultLocale, this.opts.locale)
this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
// i18n
this.translator = new Translator({locale: this.locale})
this.i18n = this.translator.translate.bind(this.translator)
this.install = this.install.bind(this)
this.setPluginState = this.setPluginState.bind(this)
this.render = this.render.bind(this)
// Camera controls
this.start = this.start.bind(this)
this.stop = this.stop.bind(this)
this.takeSnapshot = this.takeSnapshot.bind(this)
this.startRecording = this.startRecording.bind(this)
this.stopRecording = this.stopRecording.bind(this)
this.oneTwoThreeSmile = this.oneTwoThreeSmile.bind(this)
this.focus = this.focus.bind(this)
this.webcamActive = false
if (this.opts.countdown) {
this.opts.onBeforeSnapshot = this.oneTwoThreeSmile
}
}
isSupported () {
return !!this.mediaDevices
}
getConstraints () {
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
return {
audio: acceptsAudio,
video: acceptsVideo ? { facingMode: this.opts.facingMode } : false
}
}
start () {
if (!this.isSupported()) {
return Promise.reject(new Error('Webcam access not supported'))
}
this.webcamActive = true
const constraints = this.getConstraints()
// ask user for access to their camera
return this.mediaDevices.getUserMedia(constraints)
.then((stream) => {
this.stream = stream
// this.streamSrc = URL.createObjectURL(this.stream)
this.setPluginState({
cameraReady: true
})
})
.catch((err) => {
this.setPluginState({
cameraError: err
})
})
}
startRecording () {
// TODO We can check here if any of the mime types listed in the
// mimeToExtensions map in Utils.js are supported, and prefer to use one of
// those.
// Right now we let the browser pick a type that it deems appropriate.
this.recorder = new MediaRecorder(this.stream)
this.recordingChunks = []
this.recorder.addEventListener('dataavailable', (event) => {
this.recordingChunks.push(event.data)
})
this.recorder.start()
this.setPluginState({
isRecording: true
})
}
stopRecording () {
const stopped = new Promise((resolve, reject) => {
this.recorder.addEventListener('stop', () => {
resolve()
})
this.recorder.stop()
})
return stopped.then(() => {
this.setPluginState({
isRecording: false
})
return this.getVideo()
})
.then((file) => this.uppy.addFile(file))
.then(() => {
this.recordingChunks = null
this.recorder = null
const dashboard = this.uppy.getPlugin('Dashboard')
if (dashboard) dashboard.hideAllPanels()
}, (error) => {
this.recordingChunks = null
this.recorder = null
throw error
})
}
stop () {
this.stream.getAudioTracks().forEach((track) => {
track.stop()
})
this.stream.getVideoTracks().forEach((track) => {
track.stop()
})
this.webcamActive = false
this.stream = null
}
getVideoElement () {
return this.el.querySelector('.uppy-Webcam-video')
}
oneTwoThreeSmile () {
return new Promise((resolve, reject) => {
let count = this.opts.countdown
let countDown = setInterval(() => {
if (!this.webcamActive) {
clearInterval(countDown)
this.captureInProgress = false
return reject(new Error('Webcam is not active'))
}
if (count > 0) {
this.uppy.info(`${count}...`, 'warning', 800)
count--
} else {
clearInterval(countDown)
this.uppy.info(this.i18n('smile'), 'success', 1500)
setTimeout(() => resolve(), 1500)
}
}, 1000)
})
}
takeSnapshot () {
if (this.captureInProgress) return
this.captureInProgress = true
this.opts.onBeforeSnapshot().catch((err) => {
const message = typeof err === 'object' ? err.message : err
this.uppy.info(message, 'error', 5000)
return Promise.reject(new Error(`onBeforeSnapshot: ${message}`))
}).then(() => {
return this.getImage()
}).then((tagFile) => {
this.captureInProgress = false
const dashboard = this.uppy.getPlugin('Dashboard')
if (dashboard) dashboard.hideAllPanels()
return this.uppy.addFile(tagFile).catch(() => {
// Ignore
})
}, (error) => {
this.captureInProgress = false
throw error
})
}
getImage () {
const video = this.getVideoElement()
if (!video) {
return Promise.reject(new Error('No video element found, likely due to the Webcam tab being closed.'))
}
const name = `webcam-${Date.now()}.jpg`
const mimeType = 'image/jpeg'
const width = video.videoWidth
const height = video.videoHeight
// const scaleH = this.opts.mirror ? -1 : 1 // Set horizontal scale to -1 if flip horizontal
// const scaleV = 1
// const posX = this.opts.mirror ? width * -1 : 0 // Set x position to -100% if flip horizontal
// const posY = 0
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0)
// ctx.save() // Save the current state
// ctx.scale(scaleH, scaleV) // Set scale to flip the image
// ctx.drawImage(video, posX, posY, width, height) // draw the image
// ctx.restore() // Restore the last saved state
return canvasToBlob(canvas, mimeType).then((blob) => {
return {
source: this.id,
name: name,
data: new File([blob], name, { type: mimeType }),
type: mimeType
}
})
}
getVideo () {
const mimeType = this.recordingChunks[0].type
const fileExtension = getFileTypeExtension(mimeType)
if (!fileExtension) {
return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`))
}
const name = `webcam-${Date.now()}.${fileExtension}`
const blob = new Blob(this.recordingChunks, { type: mimeType })
const file = {
source: this.id,
name: name,
data: new File([blob], name, { type: mimeType }),
type: mimeType
}
return Promise.resolve(file)
}
focus () {
if (this.opts.countdown) return
setTimeout(() => {
this.uppy.info(this.i18n('smile'), 'success', 1500)
}, 1000)
}
render (state) {
if (!this.webcamActive) {
this.start()
}
const webcamState = this.getPluginState()
if (!webcamState.cameraReady) {
return PermissionsScreen(webcamState)
}
return h(CameraScreen, Object.assign({}, webcamState, {
onSnapshot: this.takeSnapshot,
onStartRecording: this.startRecording,
onStopRecording: this.stopRecording,
onFocus: this.focus,
onStop: this.stop,
modes: this.opts.modes,
supportsRecording: supportsMediaRecorder(),
recording: webcamState.isRecording,
mirror: this.opts.mirror,
src: this.stream
}))
}
install () {
this.setPluginState({
cameraReady: false
})
const target = this.opts.target
if (target) {
this.mount(target, this)
}
}
uninstall () {
if (this.stream) {
this.stop()
}
this.unmount()
}
}