|
@@ -2,14 +2,50 @@ const { h } = require('preact')
|
|
|
const { Plugin } = require('@uppy/core')
|
|
|
const Translator = require('@uppy/utils/lib/Translator')
|
|
|
const getFileTypeExtension = require('@uppy/utils/lib/getFileTypeExtension')
|
|
|
+const mimeTypes = require('@uppy/utils/lib/mimeTypes')
|
|
|
const canvasToBlob = require('@uppy/utils/lib/canvasToBlob')
|
|
|
const supportsMediaRecorder = require('./supportsMediaRecorder')
|
|
|
const CameraIcon = require('./CameraIcon')
|
|
|
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
|
|
|
+/**
|
|
|
+ * Normalize a MIME type or file extension into a MIME type.
|
|
|
+ *
|
|
|
+ * @param {string} fileType - MIME type or a file extension prefixed with `.`.
|
|
|
+ * @returns {string|undefined} The MIME type or `undefined` if the fileType is an extension and is not known.
|
|
|
+ */
|
|
|
+function toMimeType (fileType) {
|
|
|
+ if (fileType[0] === '.') {
|
|
|
+ return mimeTypes[fileType.slice(1)]
|
|
|
+ }
|
|
|
+ return fileType
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Is this MIME type a video?
|
|
|
+ *
|
|
|
+ * @param {string} mimeType - MIME type.
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+function isVideoMimeType (mimeType) {
|
|
|
+ return /^video\/[^*]+$/.test(mimeType)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Is this MIME type an image?
|
|
|
+ *
|
|
|
+ * @param {string} mimeType - MIME type.
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+function isImageMimeType (mimeType) {
|
|
|
+ return /^image\/[^*]+$/.test(mimeType)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Setup getUserMedia, with polyfill for older browsers
|
|
|
+ * Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
|
|
+ */
|
|
|
function getMediaDevices () {
|
|
|
// eslint-disable-next-line compat/compat
|
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
@@ -62,6 +98,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
stopRecording: 'Stop video recording',
|
|
|
allowAccessTitle: 'Please allow access to your camera',
|
|
|
allowAccessDescription: 'In order to take pictures or record video with your camera, please allow camera access for this site.',
|
|
|
+ recordingStoppedMaxSize: 'Recording stopped because the file size is about to exceed the limit',
|
|
|
recordingLength: 'Recording length %{recording_length}'
|
|
|
}
|
|
|
}
|
|
@@ -78,6 +115,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
],
|
|
|
mirror: true,
|
|
|
facingMode: 'user',
|
|
|
+ preferredImageMimeType: null,
|
|
|
preferredVideoMimeType: null,
|
|
|
showRecordingLength: false
|
|
|
}
|
|
@@ -92,18 +130,18 @@ module.exports = class Webcam extends Plugin {
|
|
|
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._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
|
|
|
+ this.opts.onBeforeSnapshot = this._oneTwoThreeSmile
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -136,7 +174,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- start () {
|
|
|
+ _start () {
|
|
|
if (!this.isSupported()) {
|
|
|
return Promise.reject(new Error('Webcam access not supported'))
|
|
|
}
|
|
@@ -161,26 +199,63 @@ module.exports = class Webcam extends Plugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- startRecording () {
|
|
|
+ /**
|
|
|
+ * @returns {object}
|
|
|
+ */
|
|
|
+ _getMediaRecorderOptions () {
|
|
|
const options = {}
|
|
|
|
|
|
- // Safari don't have support for isTypeSupported api.
|
|
|
+ // Try to use the `opts.preferredVideoMimeType` or one of the `allowedFileTypes` for the recording.
|
|
|
+ // If the browser doesn't support it, we'll fall back to the browser default instead.
|
|
|
+ // Safari doesn't have the `isTypeSupported` API.
|
|
|
if (MediaRecorder.isTypeSupported) {
|
|
|
- const preferredVideoMimeType = this.opts.preferredVideoMimeType
|
|
|
+ const { restrictions } = this.uppy.opts
|
|
|
+ let preferredVideoMimeTypes = []
|
|
|
+ if (this.opts.preferredVideoMimeType) {
|
|
|
+ preferredVideoMimeTypes = [this.opts.preferredVideoMimeType]
|
|
|
+ } else if (restrictions.allowedFileTypes) {
|
|
|
+ preferredVideoMimeTypes = restrictions.allowedFileTypes.map(toMimeType).filter(isVideoMimeType)
|
|
|
+ }
|
|
|
|
|
|
- // Attempt to use the passed preferredVideoMimeType (if any) during recording.
|
|
|
- // If the browser doesn't support it, we'll fall back to the browser default instead
|
|
|
- if (preferredVideoMimeType && MediaRecorder.isTypeSupported(preferredVideoMimeType) && getFileTypeExtension(preferredVideoMimeType)) {
|
|
|
- options.mimeType = preferredVideoMimeType
|
|
|
+ const acceptableMimeTypes = preferredVideoMimeTypes.filter((candidateType) =>
|
|
|
+ MediaRecorder.isTypeSupported(candidateType) &&
|
|
|
+ getFileTypeExtension(candidateType))
|
|
|
+ if (acceptableMimeTypes.length > 0) {
|
|
|
+ options.mimeType = acceptableMimeTypes[0]
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- this.recorder = new MediaRecorder(this.stream, options)
|
|
|
+ return options
|
|
|
+ }
|
|
|
+
|
|
|
+ _startRecording () {
|
|
|
+ this.recorder = new MediaRecorder(this.stream, this._getMediaRecorderOptions())
|
|
|
this.recordingChunks = []
|
|
|
+ let stoppingBecauseOfMaxSize = false
|
|
|
this.recorder.addEventListener('dataavailable', (event) => {
|
|
|
this.recordingChunks.push(event.data)
|
|
|
+
|
|
|
+ const { restrictions } = this.uppy.opts
|
|
|
+ if (this.recordingChunks.length > 1 &&
|
|
|
+ restrictions.maxFileSize != null &&
|
|
|
+ !stoppingBecauseOfMaxSize) {
|
|
|
+ const totalSize = this.recordingChunks.reduce((acc, chunk) => acc + chunk.size, 0)
|
|
|
+ // Exclude the initial chunk from the average size calculation because it is likely to be a very small outlier
|
|
|
+ const averageChunkSize = (totalSize - this.recordingChunks[0].size) / (this.recordingChunks.length - 1)
|
|
|
+ const expectedEndChunkSize = averageChunkSize * 3
|
|
|
+ const maxSize = Math.max(0, restrictions.maxFileSize - expectedEndChunkSize)
|
|
|
+
|
|
|
+ if (totalSize > maxSize) {
|
|
|
+ stoppingBecauseOfMaxSize = true
|
|
|
+ this.uppy.info(this.i18n('recordingStoppedMaxSize'), 'warning', 4000)
|
|
|
+ this._stopRecording()
|
|
|
+ }
|
|
|
+ }
|
|
|
})
|
|
|
- this.recorder.start()
|
|
|
+
|
|
|
+ // use a "time slice" of 500ms: ondataavailable will be called each 500ms
|
|
|
+ // smaller time slices mean we can more accurately check the max file size restriction
|
|
|
+ this.recorder.start(500)
|
|
|
|
|
|
if (this.opts.showRecordingLength) {
|
|
|
// Start the recordingLengthTimer if we are showing the recording length.
|
|
@@ -195,7 +270,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- stopRecording () {
|
|
|
+ _stopRecording () {
|
|
|
const stopped = new Promise((resolve, reject) => {
|
|
|
this.recorder.addEventListener('stop', () => {
|
|
|
resolve()
|
|
@@ -226,12 +301,6 @@ module.exports = class Webcam extends Plugin {
|
|
|
}).then(() => {
|
|
|
this.recordingChunks = null
|
|
|
this.recorder = null
|
|
|
-
|
|
|
- // Close the Dashboard panel if plugin is installed
|
|
|
- // into Dashboard (could be other parent UI plugin)
|
|
|
- // if (this.parent && this.parent.hideAllPanels) {
|
|
|
- // this.parent.hideAllPanels()
|
|
|
- // }
|
|
|
}, (error) => {
|
|
|
this.recordingChunks = null
|
|
|
this.recorder = null
|
|
@@ -239,7 +308,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- stop () {
|
|
|
+ _stop () {
|
|
|
this.stream.getAudioTracks().forEach((track) => {
|
|
|
track.stop()
|
|
|
})
|
|
@@ -250,11 +319,11 @@ module.exports = class Webcam extends Plugin {
|
|
|
this.stream = null
|
|
|
}
|
|
|
|
|
|
- getVideoElement () {
|
|
|
+ _getVideoElement () {
|
|
|
return this.el.querySelector('.uppy-Webcam-video')
|
|
|
}
|
|
|
|
|
|
- oneTwoThreeSmile () {
|
|
|
+ _oneTwoThreeSmile () {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
let count = this.opts.countdown
|
|
|
|
|
@@ -277,7 +346,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- takeSnapshot () {
|
|
|
+ _takeSnapshot () {
|
|
|
if (this.captureInProgress) return
|
|
|
this.captureInProgress = true
|
|
|
|
|
@@ -286,18 +355,13 @@ module.exports = class Webcam extends Plugin {
|
|
|
this.uppy.info(message, 'error', 5000)
|
|
|
return Promise.reject(new Error(`onBeforeSnapshot: ${message}`))
|
|
|
}).then(() => {
|
|
|
- return this.getImage()
|
|
|
+ return this._getImage()
|
|
|
}).then((tagFile) => {
|
|
|
this.captureInProgress = false
|
|
|
- // Close the Dashboard panel if plugin is installed
|
|
|
- // into Dashboard (could be other parent UI plugin)
|
|
|
- // if (this.parent && this.parent.hideAllPanels) {
|
|
|
- // this.parent.hideAllPanels()
|
|
|
- // }
|
|
|
try {
|
|
|
this.uppy.addFile(tagFile)
|
|
|
} catch (err) {
|
|
|
- // Logging the error, exept restrictions, which is handled in Core
|
|
|
+ // Logging the error, except restrictions, which is handled in Core
|
|
|
if (!err.isRestriction) {
|
|
|
this.uppy.log(err)
|
|
|
}
|
|
@@ -308,32 +372,32 @@ module.exports = class Webcam extends Plugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- getImage () {
|
|
|
- const video = this.getVideoElement()
|
|
|
+ _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 = `cam-${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
|
|
|
+
|
|
|
+ const { restrictions } = this.uppy.opts
|
|
|
+ let preferredImageMimeTypes = []
|
|
|
+ if (this.opts.preferredImageMimeType) {
|
|
|
+ preferredImageMimeTypes = [this.opts.preferredImageMimeType]
|
|
|
+ } else if (restrictions.allowedFileTypes) {
|
|
|
+ preferredImageMimeTypes = restrictions.allowedFileTypes.map(toMimeType).filter(isImageMimeType)
|
|
|
+ }
|
|
|
+
|
|
|
+ const mimeType = preferredImageMimeTypes[0] || 'image/jpeg'
|
|
|
+ const ext = getFileTypeExtension(mimeType) || 'jpg'
|
|
|
+ const name = `cam-${Date.now()}.${ext}`
|
|
|
|
|
|
return canvasToBlob(canvas, mimeType).then((blob) => {
|
|
|
return {
|
|
@@ -365,7 +429,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
return Promise.resolve(file)
|
|
|
}
|
|
|
|
|
|
- focus () {
|
|
|
+ _focus () {
|
|
|
if (!this.opts.countdown) return
|
|
|
setTimeout(() => {
|
|
|
this.uppy.info(this.i18n('smile'), 'success', 1500)
|
|
@@ -374,7 +438,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
|
|
|
render (state) {
|
|
|
if (!this.webcamActive) {
|
|
|
- this.start()
|
|
|
+ this._start()
|
|
|
}
|
|
|
|
|
|
const webcamState = this.getPluginState()
|
|
@@ -388,11 +452,11 @@ module.exports = class Webcam extends Plugin {
|
|
|
return (
|
|
|
<CameraScreen
|
|
|
{...webcamState}
|
|
|
- onSnapshot={this.takeSnapshot}
|
|
|
- onStartRecording={this.startRecording}
|
|
|
- onStopRecording={this.stopRecording}
|
|
|
- onFocus={this.focus}
|
|
|
- onStop={this.stop}
|
|
|
+ onSnapshot={this._takeSnapshot}
|
|
|
+ onStartRecording={this._startRecording}
|
|
|
+ onStopRecording={this._stopRecording}
|
|
|
+ onFocus={this._focus}
|
|
|
+ onStop={this._stop}
|
|
|
i18n={this.i18n}
|
|
|
modes={this.opts.modes}
|
|
|
showRecordingLength={this.opts.showRecordingLength}
|
|
@@ -418,7 +482,7 @@ module.exports = class Webcam extends Plugin {
|
|
|
|
|
|
uninstall () {
|
|
|
if (this.stream) {
|
|
|
- this.stop()
|
|
|
+ this._stop()
|
|
|
}
|
|
|
|
|
|
this.unmount()
|