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.
-
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; }
-
Add three props:
isActive
,desiredCamera
anddesiredResolution
: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 },
-
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); };
-
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); }
-
When the camera component is mounted and the
isActive
prop is set to true, open the camera. IfisActive
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); } };
-
In the
onBeforeUnmount
event, stop the camera.onBeforeUnmount(() => { stop(); });
-
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.
-
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;
-
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",
-
Add
main
andunpkg
topackage.json
:{ "main": "dist/VueVisionCamera.common.js", "unpkg": "dist/VueVisionCamera.umd.min.js" }
-
Add
css: { extract: false }
tovue.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
-
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.
-
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.