123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- const Plugin = require('../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 (core, opts) {
- super(core, 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
- this.focus = this.focus.bind(this)
- 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'
- ]
- }
- // 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.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
- }
- }
- 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) => {
- return this.core.addFile(file)
- }).then(() => {
- this.recordingChunks = null
- this.recorder = null
- }, (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
- this.streamSrc = null
- }
- getVideoElement () {
- return this.target.querySelector('.UppyWebcam-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.core.info(`${count}...`, 'warning', 800)
- count--
- } else {
- clearInterval(countDown)
- this.core.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.core.info(message, 'error', 5000)
- return Promise.reject(new Error(`onBeforeSnapshot: ${message}`))
- }).then(() => {
- return this.getImage()
- }).then((tagFile) => {
- this.captureInProgress = false
- this.core.addFile(tagFile)
- }, (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 canvas = document.createElement('canvas')
- canvas.width = video.videoWidth
- canvas.height = video.videoHeight
- canvas.getContext('2d').drawImage(video, 0, 0)
- 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.core.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 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,
- src: this.streamSrc
- }))
- }
- install () {
- this.setPluginState({
- cameraReady: false
- })
- const target = this.opts.target
- if (target) {
- this.mount(target, this)
- }
- }
- uninstall () {
- if (this.stream) {
- this.stop()
- }
- this.unmount()
- }
- }
|