Vue Camera Component for Barcode Scanning

Using the getUserMedia API, we can get the camera preview in a video element, which makes it possible to do computer vision tasks like barcode scanning in a browser.

In this article, we are going to write a camera component for Vue using getUserMedia and build a barcode scanning demo using Dynamsoft Barcode Reader that acquires the camera image data from the component.

Build a Vue Camera Component

We are going to write a Vue camera component and make it a Vue plugin that can be used in other projects.

New Project

Create a new Vue 3 project named vue-vision-camera:

vue create vue-vision-camera

New Component

Add a new component named VisionCamera.vue under src\components.

We can modify App.vue to use the component:

<template>
  <div>
    <VisionCamera></VisionCamera>
  </div>
</template>

<script>
import VisionCamera from './components/VisionCamera.vue'

export default {
  name: 'App',
  components: {
    VisionCamera
  },
}
</script>

Then run npm run serve to test it.

Use getUserMedia to Open the Camera

Let’s modify the component so that it can open the camera.

  1. Add a video element in the template:

    <div class="camera-container full">
      <video class="camera full" ref="camera" v-on:loadeddata="onCameraOpened" muted autoplay="true" playsinline="true" webkit-playsinline></video>
      <slot></slot>
    </div>
    

    JavaScript:

    export default {
      setup(props,context) {
        const camera = ref(null);
        const onCameraOpened = () => {
         console.log("opened");
        };
        return {
          camera,
          onCameraOpened
        };
      }
    }
    

    CSS:

    .camera-container {
      position: relative;
    }
          
    .camera {
      position: absolute;
      object-fit: cover;
    }
          
    .full {
      width:100%;
      height:100%;
      left:0;
      top:0;
    }
    
  2. Add three props: isActive, desiredCamera and desiredResolution:

    props: {
      desiredCamera: String, //specify which camera to use
      desiredResolution: {width:Number,height:Number}, //specify the camera's resolution
      isActive: Boolean, //control whether the camera is open or not
    },
    
  3. Add three emitted events:

    emits: ['opened', // triggered when the camera is opened
            'closed', // triggered when the camera is closed
            'devicesLoaded' // triggered when the device list is loaded
           ],
    

    Update the onCameraOpened function to pass the camera as an argument.

    const onCameraOpened = () => {
      console.log("opened");
      context.emit("opened", camera.value);
    };
    
  4. Load the list of existing camera devices:

    let devices = null;
    const loadDevices = async () => {
      const constraints = {video: true, audio: false};
      const stream = await navigator.mediaDevices.getUserMedia(constraints) // ask for permission
      const mediaDevices = await navigator.mediaDevices.enumerateDevices();
      let cameraDevices = [];
      for (let i=0;i<mediaDevices.length;i++){
        let device = mediaDevices[i];
        if (device.kind == 'videoinput'){ // filter out audio devices
          cameraDevices.push(device);
        }
      }
      devices = cameraDevices;
      const tracks = stream.getTracks();
       for (let i=0;i<tracks.length;i++) {
         const track = tracks[i];
         track.stop();  // stop the opened camera
       }
      context.emit("devicesLoaded", devices);
    }
    
  5. When the camera component is mounted and the isActive prop is set to true, open the camera. If isActive is set to false, close the camera.

    Events handlers:

    onMounted(() => {
      if (props.isActive != false) {
        playWithDesired();
      }
    });
       
    watch(() => props.isActive, (newVal) => {
      if (newVal === true) {
        playWithDesired();
      }else{
        stop();
      }
    });
    

    The playWithDesired function which opens the desired camera with desired resolution:

    const playWithDesired = async () => {
      if (!devices) {
        await loadDevices(); // load the camera devices list if it hasn't been loaded
      }
      let desiredDevice = getDesiredDevice(devices)
         
      if (desiredDevice) {
        let options = {};
        options.deviceId=desiredDevice;
        if (props.desiredResolution) {
          options.desiredResolution=props.desiredResolution;
        }
        play(options);
      }else{
        throw new Error("No camera detected");
      }
    }
      
    const getDesiredDevice = (devices) => {
      var count = 0;
      var desiredIndex = 0;
      for (var i=0;i<devices.length;i++){
        var device = devices[i];
        var label = device.label || `Camera ${count++}`;
        if (props.desiredCamera) {
          if (label.toLowerCase().indexOf(props.desiredCamera.toLowerCase()) != -1) {
            desiredIndex = i;
            break;
          } 
        }
      }
    
      if (devices.length>0) {
        return devices[desiredIndex].deviceId; // return the device id
      }else{
        return null;
      }
    }
      
    const play = (options) => {
      stop(); // close before play
      var constraints = {};
    
      if (options.deviceId){
        constraints = {
          video: {deviceId: options.deviceId},
          audio: false
        }
      }else{
        constraints = {
          video: {width:1280, height:720},
          audio: false
        }
      }
         
      if (options.desiredResolution) {
        constraints["video"]["width"] = options.desiredResolution.width;
        constraints["video"]["height"] = options.desiredResolution.height;
      }
      navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
        localStream = stream;
        // Attach local stream to video element
        camera.value.srcObject = stream;
      }).catch(function(err) {
        console.error('getUserMediaError', err, err.stack);
      });
    }
       
    const stop = () => {
      try{
        if (localStream){
          localStream.getTracks().forEach(track => track.stop());
          context.emit('closed');
        }
      } catch (e){
        alert(e.message);
      }
    };
    
  6. In the onBeforeUnmount event, stop the camera.

    onBeforeUnmount(() => {
      stop();
    });
    
  7. Update App.vue to have a test.

    Here, we make the camera full screen and open it. A close button is added to close the camera.

    Template:

    <div class="vision-camera">
      <VisionCamera 
        :isActive="isActive" 
        :desiredResolution="{width:1280,height:720}"
        desiredCamera="back"
        @devicesLoaded="devicesLoaded"
        @closed="closed"
        @opened="opened"
      >
        <button class="close-btn" v-on:click="closeCamera" >Close</button>
      </VisionCamera>
    </div>
    

    JavaScript:

    setup(){
      const isActive = ref(true);
    
      const devicesLoaded = (devices) => {
        console.log(devices);
      }
    
      const opened = (camera) => {
        console.log("camera opened");
      }
    
      const closed = () => {
        console.log("camera closed");
      }
    
      return {
        isActive,
        devicesLoaded,
        closed,
        opened
      }
    }
    

    CSS:

    .vision-camera {
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      position: absolute;
    }
    
    .close-btn {
      top: 0;
      right: 0;
      position: absolute;
    }
    

Update the Project to Make it a Vue Plugin

We can update the project to make it a Vue plugin so that we can install and use the component in other projects.

  1. Create an index.js to define the plugin and serve as the entry file.

    import VisionCamera from './components/VisionCamera.vue'
    const install = (app) => {
      app.component('vision-camera', VisionCamera);
    }
    
    export {
      VisionCamera
    }
    
    const plugin = {
      install
    }
    export default plugin;
    
  2. Update the build command in the package.json to build the library.

    - "build": "vue-cli-service build",
    + "build": "vue-cli-service build --target lib --name VueVisionCamera src/index.js",
    
  3. Add main and unpkg to package.json:

    {
      "main": "dist/VueVisionCamera.common.js",
      "unpkg": "dist/VueVisionCamera.umd.min.js"
    }
    
  4. Add css: { extract: false } to vue.config.js so that the css is bundled in the component’s JS file.

    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
    + css: { extract: false },
      transpileDependencies: true
    })
    

All right, we can now build the library and use it in other projects.

Build a Barcode Scanner using the Component

Let’s take a step further to use the component to build a barcode scanner.

New Project

vue create barcode-scanner

Install Dependencies

npm install vue-vision-camera dynamsoft-javascript-barcode

Add the Camera Component

In App.vue’s template, add the VisionCamera component, a start button to start the camera and a close button to close the camera.

<div class="controls" :style="{display: isActive ? 'none' : '' }">
  <button v-on:click="startCamera">
    Start Camera
  </button>
</div>
<div class="vision-camera" :style="{display: isActive ? '' : 'none' }">
  <VisionCamera 
    :isActive="isActive" 
    :desiredResolution="{width:1280,height:720}"
    desiredCamera="back"
    @devicesLoaded="devicesLoaded"
    @closed="closed"
    @opened="opened"
  >
    <button class="close-btn" v-on:click="closeCamera" >Close</button>
  </VisionCamera>
</div>

Add Barcode Scanning Function

  1. Initialize Dynamsoft Barcode Reader.

    import { BarcodeReader } from "dynamsoft-javascript-barcode";
    BarcodeReader.engineResourcePath = "https://unpkg.com/dynamsoft-javascript-barcode@9.0.2/dist/";
    
    export default {
      name: 'App',
      components: {
        VisionCamera
      },
      setup(){
        let reader;
        onMounted(async ()=>{
          BarcodeReader.license = "DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==";
          reader = await BarcodeReader.createInstance();
        })
      }
    }
    

    You can apply for your own license here.

  2. When the camera is opened, set an interval to read barcodes from the video. If the camera is closed, then stop the interval. If barcodes are found, display the result and close the camera.

    let interval;
    let camera;
    let decoding = false;
    let scanned = false;
    const opened = (cam) => {
      camera = cam;
      startDecoding();
    }
    
    const closed = () => {
      stopDecoding();
    }
       
    const stopDecoding = () => {
      clearInterval(interval);
    }
    
    const startDecoding = () => {
      scanned = false;
      const decode = async () => {
        if (decoding === false && reader && camera) {
          decoding = true;
          const results = await reader.decode(camera);
          decoding = false;
          if (results.length>0 && scanned === false) {
            alert(results[0].barcodeText);
            scanned = true;
            stopDecoding();
            isActive.value = false; // close the camera
          }
        }
      }
      interval = setInterval(decode,40);
    }
    

We can also draw barcode overlays using SVG. You can check out the online demo to have a try.

Source Code

https://github.com/xulihang/vue-vision-camera