Friday, December 11, 2020

How to Implement Webcam in VueJs Application

In this tutorial, we are going to implement a webcam in the Vuejs application.


Generally, what it does is:

  1. Stream the video from the webcam
  2. Capture photo from the streaming video
  3. Preview the captured photo
  4. Convert it into the file to send to the server
The overall implementation looks as below:




1. Create a sample Vuejs application.

Open the terminal or command prompt and create the application.

vue create vue-camera
  

Select the default option or can select manually, where you can have a choice to set up.
cd vue-camera
yarn serve

2. Create a Vuejs webcam component



Now, let's create a component called Camera.vue.  
<div class="camera-box">
    <div style="display: flex; justify-content: center;">
        <img style="height: 25px;" v-if="isCameraOpen"
             src="https://img.icons8.com/material-outlined/50/000000/camera--v2.png"
             class="button-img camera-shoot" @click="capture"/>
        <div class="camera-button">
            <button type="button" class="button is-rounded cam-button"
                    style="margin-left: 40%; background-color: white; border: 0px;"
                    @click="toggleCamera"
            >
        <span v-if="!isCameraOpen"><img style="height: 25px;" class="button-img"
                                        src="https://img.icons8.com/material-outlined/50/000000/camera--v2.png"></span>
                <span v-else><img style="height: 25px;" class="button-img"
                                  src="https://img.icons8.com/material-outlined/50/000000/cancel.png"></span>
            </button>
        </div>
    </div>
    <div style="height: 200px">
        <div v-if="isCameraOpen" class="camera-canvas">
            <video ref="camera" :width="canvasWidth" :height="canvasHeight" autoplay></video>
            <canvas v-show="false" id="photoTaken" ref="canvas" :width="canvasWidth" :height="canvasHeight"></canvas>
        </div>
    </div>
</div>
Let's set the data used.
export default {
  name: "Camera",
  components: {
  },
  data() {
    return {
      isCameraOpen: false,
      canvasHeight:200,
      canvasWidth:190,
      items: [],
    }
  },
  }
<style scoped>
.camera-box {
  border: 1px dashed #d6d6d6;
  border-radius: 4px;
  padding: 2px;
  width: 80%;
  min-height: 300px;
}

</style>





The HTML file used will show the camera image button to capture the photo. Here, we are using canvas height and width for the photo size. You can always adjust the different size photo by adjusting canvas height and width.

Let's use the different functions used in HTML.
toggleCamera() {
      if (this.isCameraOpen) {
        this.isCameraOpen = false;
        this.stopCameraStream();
      } else {
        this.isCameraOpen = true;
        this.startCameraStream();
      }
    },
This function simply toggles the camera to capture the photo. And start streaming the video. Let's implement the function used inside this toggleCamer().
startCameraStream() {
      const constraints = (window.constraints = {
        audio: false,
        video: true
      });
      navigator.mediaDevices
          .getUserMedia(constraints)
          .then(stream => {
            this.$refs.camera.srcObject = stream;
          }).catch(error => {
        alert("Browser doesn't support or there is some errors." + error);
      });
    },
The above function will start the camera by using the navigator mediaDevices interface which will access to the connected media input device, in our case its camera. And asign the streaming to canvas dom element to show the video. Here we are setting the constraints as audio is false because we don't need the audio to capture the photo.
stopCameraStream() {
      let tracks = this.$refs.camera.srcObject.getTracks();
      tracks.forEach(track => {
        track.stop();
      });
    },
The above function will simply stop the camera by stopping the sequence of track in streaming.
capture() {
      const FLASH_TIMEOUT = 50;
      let self = this;
      setTimeout(() => {
        const context = self.$refs.canvas.getContext('2d');
        context.drawImage(self.$refs.camera, 0, 0, self.canvasWidth, self.canvasHeight);
        const dataUrl = self.$refs.canvas.toDataURL("image/jpeg")
            .replace("image/jpeg", "image/octet-stream");
        self.addToPhotoGallery(dataUrl);
        self.uploadPhoto(dataUrl);
        self.isCameraOpen = false;
        self.stopCameraStream();
      }, FLASH_TIMEOUT);
    },
When a user clicks the camera button, then it will take the video streaming dom context and captured the 2d photo with the given width and height. We are using the setTimeout because video streaming will take some time to stream.




Let's implement the self.addToPhotoGallery() used in the above function. This is to preview the captured photo. For this, we are using the "vue-picture-swipe" library. So first let's add this to our application.

For yarn package manager:

yarn add vue-picture-swipe

For npm package manager:

npm install --save vue-picture-swipe

Import the library in our application:

import VuePictureSwipe from 'vue-picture-swipe';
Register the component:

components: {
    VuePictureSwipe
  },
Use in the template:

<vue-picture-swipe :items="items"></vue-picture-swipe>

addToPhotoGallery(dataURI) {
      this.items.push(
          {
            src: dataURI,
            thumbnail: dataURI,
            w: this.canvasWidth,
            h: this.canvasHeight,
            alt: '' // optional alt attribute for thumbnail image
          }
      )
    },
This will preview the captured pictures.

Finally, using self.uploadPhoto() we can upload the captured picture to the server.

uploadPhoto(dataURL){
      let uniquePictureName = this.generateCapturePhotoName();
      let capturedPhotoFile = this.dataURLtoFile(dataURL, uniquePictureName+'.jpg')
      let formData = new FormData()
      formData.append('file', capturedPhotoFile)
      // Upload api
      // axios.post('http://your-url-upload', formData).then(response => {
      //   console.log(response)
      // })
    },
    generateCapturePhotoName(){
      return  Math.random().toString(36).substring(2, 15)
    },
    dataURLtoFile(dataURL, filename) {
      let arr = dataURL.split(','),
          mime = arr[0].match(/:(.*?);/)[1],
          bstr = atob(arr[1]),
          n = bstr.length,
          u8arr = new Uint8Array(n);

      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], filename, {type: mime});
    },




generateCapturePhotoName() will generate the unique name for each captured picture, and to send to the server, first we are converting the dataUrl value to file using dataURLtoFile()

The overall implementation of the component Camera.vue looks like as below:

<template>
    <div class="camera-box">
        <div style="display: flex; justify-content: center;">
            <img style="height: 25px;" v-if="isCameraOpen"
                 src="https://img.icons8.com/material-outlined/50/000000/camera--v2.png"
                 class="button-img camera-shoot" @click="capture"/>
            <div class="camera-button">
                <button type="button" class="button is-rounded cam-button"
                        style="margin-left: 40%; background-color: white; border: 0px;"
                        @click="toggleCamera"
                >
        <span v-if="!isCameraOpen"><img style="height: 25px;" class="button-img"
                                        src="https://img.icons8.com/material-outlined/50/000000/camera--v2.png"></span>
                    <span v-else><img style="height: 25px;" class="button-img"
                                      src="https://img.icons8.com/material-outlined/50/000000/cancel.png"></span>
                </button>
            </div>
        </div>
        <div style="height: 200px">
            <div v-if="isCameraOpen" class="camera-canvas">
                <video ref="camera" :width="canvasWidth" :height="canvasHeight" autoplay></video>
                <canvas v-show="false" id="photoTaken" ref="canvas" :width="canvasWidth" :height="canvasHeight"></canvas>
            </div>
        </div>
        <vue-picture-swipe :items="items"></vue-picture-swipe>
    </div>
</template>

<script>
    import VuePictureSwipe from 'vue-picture-swipe';

    export default {
        name: "Camera",
        components: {
            VuePictureSwipe
        },
        data() {
            return {
                isCameraOpen: false,
                canvasHeight:200,
                canvasWidth:190,
                items: [],
            }
        },
        methods: {
            toggleCamera() {
                if (this.isCameraOpen) {
                    this.isCameraOpen = false;
                    this.stopCameraStream();
                } else {
                    this.isCameraOpen = true;
                    this.startCameraStream();
                }
            },
            startCameraStream() {
                const constraints = (window.constraints = {
                    audio: false,
                    video: true
                });
                navigator.mediaDevices
                    .getUserMedia(constraints)
                    .then(stream => {
                        this.$refs.camera.srcObject = stream;
                    }).catch(error => {
                    alert("Browser doesn't support or there is some errors." + error);
                });
            },

            stopCameraStream() {
                let tracks = this.$refs.camera.srcObject.getTracks();
                tracks.forEach(track => {
                    track.stop();
                });
            },

            capture() {
                const FLASH_TIMEOUT = 50;
                let self = this;
                setTimeout(() => {
                    const context = self.$refs.canvas.getContext('2d');
                    context.drawImage(self.$refs.camera, 0, 0, self.canvasWidth, self.canvasHeight);
                    const dataUrl = self.$refs.canvas.toDataURL("image/jpeg")
                        .replace("image/jpeg", "image/octet-stream");
                    self.addToPhotoGallery(dataUrl);
                    self.uploadPhoto(dataUrl);
                    self.isCameraOpen = false;
                    self.stopCameraStream();
                }, FLASH_TIMEOUT);
            },

            addToPhotoGallery(dataURI) {
                this.items.push(
                    {
                        src: dataURI,
                        thumbnail: dataURI,
                        w: this.canvasWidth,
                        h: this.canvasHeight,
                        alt: 'some numbers on a grey background' // optional alt attribute for thumbnail image
                    }
                )
            },
            uploadPhoto(dataURL){
                let uniquePictureName = this.generateCapturePhotoName();
                let capturedPhotoFile = this.dataURLtoFile(dataURL, uniquePictureName+'.jpg')
                let formData = new FormData()
                formData.append('file', capturedPhotoFile)
                // Upload image api
                // axios.post('http://your-url-upload', formData).then(response => {
                //   console.log(response)
                // })
            },

            generateCapturePhotoName(){
                return  Math.random().toString(36).substring(2, 15)
            },

            dataURLtoFile(dataURL, filename) {
                let arr = dataURL.split(','),
                    mime = arr[0].match(/:(.*?);/)[1],
                    bstr = atob(arr[1]),
                    n = bstr.length,
                    u8arr = new Uint8Array(n);

                while (n--) {
                    u8arr[n] = bstr.charCodeAt(n);
                }
                return new File([u8arr], filename, {type: mime});
            },
        }
    }
</script>

<style scoped>
    .camera-box {
        border: 1px dashed #d6d6d6;
        border-radius: 4px;
        padding: 2px;
        width: 80%;
        min-height: 300px;
    }

</style>



Share:

1 comment:

  1. We should use `this.$refs.camera[0]` instead of `this.$refs.camera`

    The return of `this.$refs.camera`is an array.

    ReplyDelete