Browse Source

Implement reading audio+video from Webcam. (#175)

* webcam: Add options to select whether video/audio should be recorded.

* webcam: add audio/video stream capture.

* webcam: Make sure querySelector for `video` element always picks our current element.

* webcam: Add buttons to control video recording.

* webcam: Pull `Recording` status into State object.

* webcam: Only show recording buttons if `MediaRecorder` is available.

* webcam: Add `modes` list option to determine whether video/audio should be recorded.
Renée Kooi 8 years ago
parent
commit
93d3606c4a

+ 24 - 0
src/core/Utils.js

@@ -146,6 +146,23 @@ function getFileType (file) {
   // return mime.lookup(file.name)
 }
 
+// TODO Check which types are actually supported in browsers. Chrome likes webm
+// from my testing, but we may need more.
+// We could use a library but they tend to contain dozens of KBs of mappings,
+// most of which will go unused, so not sure if that's worth it.
+const mimeToExtensions = {
+  'video/ogg': 'ogv',
+  'audio/ogg': 'ogg',
+  'video/webm': 'webm',
+  'audio/webm': 'webm',
+  'video/mp4': 'mp4',
+  'audio/mp3': 'mp3'
+}
+
+function getFileTypeExtension (mimeType) {
+  return mimeToExtensions[mimeType] || null
+}
+
 // returns [fileName, fileExt]
 function getFileNameAndExtension (fullFileName) {
   var re = /(?:\.([^.]+))?$/
@@ -238,6 +255,11 @@ function createImageThumbnail (imgDataURI, newWidth) {
   })
 }
 
+function supportsMediaRecorder () {
+  return typeof MediaRecorder === 'function' && !!MediaRecorder.prototype &&
+    typeof MediaRecorder.prototype.start === 'function'
+}
+
 function dataURItoBlob (dataURI, opts, toFile) {
   // get the base64 data
   var data = dataURI.split(',')[1]
@@ -433,9 +455,11 @@ module.exports = {
   readFile,
   createImageThumbnail,
   getProportionalImageHeight,
+  supportsMediaRecorder,
   isTouchDevice,
   getFileNameAndExtension,
   truncateString,
+  getFileTypeExtension,
   getFileType,
   secondsToTime,
   dataURItoBlob,

+ 17 - 9
src/plugins/Webcam/CameraScreen.js

@@ -1,5 +1,10 @@
 const html = require('yo-yo')
-const CameraIcon = require('./CameraIcon')
+const SnapshotButton = require('./SnapshotButton')
+const RecordButton = require('./RecordButton')
+
+function isModeAvailable (modes, mode) {
+  return modes.indexOf(mode) !== -1
+}
 
 module.exports = (props) => {
   const src = props.src || ''
@@ -11,10 +16,18 @@ module.exports = (props) => {
     video = html`<video class="UppyWebcam-video" autoplay src="${src}"></video>`
   }
 
+  const shouldShowRecordButton = props.supportsRecording && (
+    isModeAvailable(props.modes, 'video-only') ||
+    isModeAvailable(props.modes, 'audio-only') ||
+    isModeAvailable(props.modes, 'video-audio')
+  )
+
+  const shouldShowSnapshotButton = isModeAvailable(props.modes, 'picture')
+
   return html`
     <div class="UppyWebcam-container" onload=${(el) => {
       props.onFocus()
-      document.querySelector('.UppyWebcam-stopRecordBtn').focus()
+      document.querySelector('.UppyWebcam-recordButton').focus()
     }} onunload=${(el) => {
       props.onStop()
     }}>
@@ -22,13 +35,8 @@ module.exports = (props) => {
         ${video}
       </div>
       <div class='UppyWebcam-buttonContainer'>
-        <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-stopRecordBtn"
-          type="button"
-          title="Take a snapshot"
-          aria-label="Take a snapshot"
-          onclick=${props.onSnapshot}>
-          ${CameraIcon()}
-        </button>
+        ${shouldShowRecordButton ? RecordButton(props) : null}
+        ${shouldShowSnapshotButton ? SnapshotButton(props) : null}
       </div>
       <canvas class="UppyWebcam-canvas" style="display: none;"></canvas>
     </div>

+ 27 - 0
src/plugins/Webcam/RecordButton.js

@@ -0,0 +1,27 @@
+const html = require('yo-yo')
+const RecordStartIcon = require('./RecordStartIcon')
+const RecordStopIcon = require('./RecordStopIcon')
+
+module.exports = function RecordButton ({ recording, onStartRecording, onStopRecording }) {
+  if (recording) {
+    return html`
+      <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-recordButton"
+        type="button"
+        title="Stop Recording"
+        aria-label="Stop Recording"
+        onclick=${onStopRecording}>
+        ${RecordStopIcon()}
+      </button>
+    `
+  }
+
+  return html`
+    <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-recordButton"
+      type="button"
+      title="Begin Recording"
+      aria-label="Begin Recording"
+      onclick=${onStartRecording}>
+      ${RecordStartIcon()}
+    </button>
+  `
+}

+ 7 - 0
src/plugins/Webcam/RecordStartIcon.js

@@ -0,0 +1,7 @@
+const html = require('yo-yo')
+
+module.exports = (props) => {
+  return html`<svg class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
+    <circle cx="50" cy="50" r="40" />
+  </svg>`
+}

+ 7 - 0
src/plugins/Webcam/RecordStopIcon.js

@@ -0,0 +1,7 @@
+const html = require('yo-yo')
+
+module.exports = (props) => {
+  return html`<svg class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
+    <rect x="15" y="15" width="70" height="70" />
+  </svg>`
+}

+ 14 - 0
src/plugins/Webcam/SnapshotButton.js

@@ -0,0 +1,14 @@
+const html = require('yo-yo')
+const CameraIcon = require('./CameraIcon')
+
+module.exports = function SnapshotButton ({ onSnapshot }) {
+  return html`
+    <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-recordButton"
+      type="button"
+      title="Take a snapshot"
+      aria-label="Take a snapshot"
+      onclick=${onSnapshot}>
+      ${CameraIcon()}
+    </button>
+  `
+}

+ 75 - 4
src/plugins/Webcam/index.js

@@ -1,6 +1,8 @@
 const Plugin = require('../Plugin')
 const WebcamProvider = require('../../uppy-base/src/plugins/Webcam')
-const { extend } = require('../../core/Utils')
+const { extend,
+        getFileTypeExtension,
+        supportsMediaRecorder } = require('../../core/Utils')
 const WebcamIcon = require('./WebcamIcon')
 const CameraScreen = require('./CameraScreen')
 const PermissionsScreen = require('./PermissionsScreen')
@@ -20,7 +22,13 @@ module.exports = class Webcam extends Plugin {
 
     // set default options
     const defaultOptions = {
-      enableFlash: true
+      enableFlash: true,
+      modes: [
+        'video-audio',
+        'video-only',
+        'audio-only',
+        'picture'
+      ]
     }
 
     this.params = {
@@ -54,6 +62,8 @@ module.exports = class Webcam extends Plugin {
     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.webcam = new WebcamProvider(this.opts, this.params)
     this.webcamActive = false
@@ -77,8 +87,64 @@ module.exports = class Webcam extends Plugin {
       })
   }
 
+  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.updateState({
+      isRecording: true
+    })
+  }
+
+  stopRecording () {
+    return new Promise((resolve, reject) => {
+      this.recorder.addEventListener('stop', () => {
+        this.updateState({
+          isRecording: false
+        })
+
+        const mimeType = this.recordingChunks[0].type
+        const fileExtension = getFileTypeExtension(mimeType)
+
+        if (!fileExtension) {
+          reject(new Error(`Could not upload file: Unsupported media type "${mimeType}"`))
+          return
+        }
+
+        const file = {
+          source: this.id,
+          name: `webcam-${Date.now()}.${fileExtension}`,
+          type: mimeType,
+          data: new Blob(this.recordingChunks, { type: mimeType })
+        }
+
+        this.core.emitter.emit('core:file-add', file)
+
+        this.recordingChunks = null
+        this.recorder = null
+
+        resolve()
+      })
+
+      this.recorder.stop()
+    })
+  }
+
   stop () {
-    this.stream.getVideoTracks()[0].stop()
+    this.stream.getAudioTracks().forEach((track) => {
+      track.stop()
+    })
+    this.stream.getVideoTracks().forEach((track) => {
+      track.stop()
+    })
     this.webcamActive = false
     this.stream = null
     this.streamSrc = null
@@ -90,7 +156,7 @@ module.exports = class Webcam extends Plugin {
       mimeType: 'image/jpeg'
     }
 
-    const video = document.querySelector('.UppyWebcam-video')
+    const video = this.target.querySelector('.UppyWebcam-video')
 
     const image = this.webcam.getImage(video, opts)
 
@@ -119,8 +185,13 @@ module.exports = class Webcam extends Plugin {
 
     return CameraScreen(extend(state.webcam, {
       onSnapshot: this.takeSnapshot,
+      onStartRecording: this.startRecording,
+      onStopRecording: this.stopRecording,
       onFocus: this.focus,
       onStop: this.stop,
+      modes: this.opts.modes,
+      supportsRecording: supportsMediaRecorder(),
+      recording: state.webcam.isRecording,
       getSWFHTML: this.webcam.getSWFHTML,
       src: this.streamSrc
     }))

+ 1 - 1
src/scss/_webcam.scss

@@ -37,7 +37,7 @@
   right: 30px;
 }
 
-.UppyWebcam-stopRecordBtn .UppyIcon {
+.UppyWebcam-recordButton .UppyIcon {
   width: 50%;
   height: 50%;
   position: relative;

+ 10 - 3
src/uppy-base/src/plugins/Webcam.js

@@ -13,7 +13,8 @@ module.exports = class Webcam {
 
     // set default options
     const defaultOptions = {
-      enableFlash: true
+      enableFlash: true,
+      modes: []
     }
 
     const defaultParams = {
@@ -107,10 +108,16 @@ module.exports = class Webcam {
     this.userMedia = this._userMedia === undefined ? this.userMedia : this._userMedia
     return new Promise((resolve, reject) => {
       if (this.userMedia) {
+        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
+
         // ask user for access to their camera
         this.mediaDevices.getUserMedia({
-          audio: false,
-          video: true
+          audio: acceptsAudio,
+          video: acceptsVideo
         })
         .then((stream) => {
           return resolve(stream)