Open Multiple Cameras in the Browser

WebRTC (Web Real-Time Communication) is a technology that enables Web applications and sites to capture and optionally stream audio and/or video media, as well as to exchange arbitrary data between browsers. We can use it to open local cameras and stream remote cameras in the browser.

In this article, we will try to open multiple cameras in a web page with WebRTC’s getUserMedia API.

Online demos:

  • Simple. A simple demo to open two cameras.
  • Barcode Scanner. A demo which opens two cameras and reads barcodes from one camera with Dynamsoft Barcode Reader. The camera frames and the barcodes are saved to IndexedDB. It is useful for building a self-checkout kiosk machine which captures both the barcodes and images of the customers.

Demo video of the barcode scanner:

What it Works on

It is not possible to open multiple cameras at the same time on Android and iOS devices. It works fine on PC devices.

Create a Web Page to Open Two Cameras

Let’s write a page to open two cameras.

New HTML File

Create a new HTML file with the following content.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Multiple Camera Simple Example</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <style>
    </style>
  </head>
  <body>
    <div>
      <label>
        Camera 1:
        <select id="select-camera-1"></select>
      </label>
      <label>
        Camera 2:
        <select id="select-camera-2"></select>
      </label>
    </div>
    <div>
      <video id="camera1" controls autoplay playsinline></video>
    </div>
    <div>
      <video id="camera2" controls autoplay playsinline></video>
    </div>
    <button id="btn-open-camera">Open Cameras</button>
    <script>
    </script>
  </body>
</html>

Ask for Camera Permission

Ask for camera permission after the page is loaded.

window.onload = async function(){
  await askForPermissions()
}

async function askForPermissions(){
  var stream;
  try {
    var constraints = {video: true, audio: false}; //ask for camera permission
    stream = await navigator.mediaDevices.getUserMedia(constraints);  
  } catch (error) {
    console.log(error);
  }
  closeStream(stream);
}

function closeStream(stream){
  try{
    if (stream){
      stream.getTracks().forEach(track => track.stop());
    }
  } catch (e){
    alert(e.message);
  }
}

List Camera Devices

List camera devices in the select elements.

var devices;
var camSelect1 = document.getElementById("select-camera-1");
var camSelect2 = document.getElementById("select-camera-2");
async function listDevices(){
  devices = await getCameraDevices()
  for (let index = 0; index < devices.length; index++) {
    const device = devices[index];
    camSelect1.appendChild(new Option(device.label ?? "Camera "+index,device.deviceId));
    camSelect2.appendChild(new Option(device.label ?? "Camera "+index,device.deviceId));
  }
  camSelect2.selectedIndex = 1;
}

async function getCameraDevices(){
  await askForPermissions();
  var allDevices = await navigator.mediaDevices.enumerateDevices();
  var cameraDevices = [];
  for (var i=0;i<allDevices.length;i++){
    var device = allDevices[i];
    if (device.kind == 'videoinput'){
      cameraDevices.push(device);
    }
  }
  return cameraDevices;
}

Open Cameras

Open the cameras after the Open Cameras button is clicked.

document.getElementById("btn-open-camera").addEventListener("click",function(){
  captureCamera(document.getElementById("camera1"),camSelect1.selectedOptions[0].value);
  captureCamera(document.getElementById("camera2"),camSelect2.selectedOptions[0].value);
});

function captureCamera(video, selectedCamera) {
  var constraints = {
    audio:false,
    video:true
  }
  if (selectedCamera) {
    constraints = {
      video: {deviceId: selectedCamera},
      audio: false
    }
  }
  navigator.mediaDevices.getUserMedia(constraints).then(function(camera) {
    video.srcObject = camera;
  }).catch(function(error) {
    alert('Unable to capture your camera. Please check console logs.');
    console.error(error);
  });
}

All right, we’ve finished the page to open two cameras.

Add Barcode Scanning Ability

Next, let’s add the barcode scanning ability to the page.

Include Libraries

Include the Dynamsoft Barcode Reader library in the HTML file.

<script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.31/dist/dbr.js"></script>

Include the localForage library in the HTML file to save scanned records into IndexedDB.

<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>

Scan Barcodes from Camera Frames

  1. Initialize Dynamsoft Barcode Reader with a license. You can apply for a license here.

    async function initDBR(){
      Dynamsoft.DBR.BarcodeScanner.license = 'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='; //one-day public trial
      scanner = await Dynamsoft.DBR.BarcodeScanner.createInstance();
    }
    
  2. Start an interval to scan barcodes from the camera frames.

    var interval;
    var decoding = false;
    function startScanning(){
      stopScanning();
      interval = setInterval(captureAndDecode,200);
    }
    
    function stopScanning(){
      if (interval) {
        clearInterval(interval);
        interval = undefined;
      }
      decoding = false;
    }
       
    async function captureAndDecode(){
      if (decoding === false) {
        decoding = true;
        try {
          let video = document.getElementById("camera1");
    
          var results = await scanner.decode(video);
          console.log(results);
        } catch (error) {
          console.log(error);
        }
        decoding = false;
      }
    }
    

Display Scanned Results in a Table and Save to IndexedDB

After a barcode is found, use Canvas to capture frames as data URLs from the cameras, and display them along with barcodes and date in a table. Save them to IndexedDB as well for persistent data storage.

var recordStore = localforage.createInstance({
  name: "record"
});

function appendRecord(results){
  var cam1 = document.getElementById("camera1");
  var cam2 = document.getElementById("camera2");
  var cvs = document.createElement("canvas");
  var imgDataURL1 = captureFrame(cvs,cam1);
  var imgDataURL2 = captureFrame(cvs,cam2);
  var row = document.createElement("tr");
  var cell1 = document.createElement("td");
  var cell2 = document.createElement("td");
  var cell3 = document.createElement("td");
  var cell4 = document.createElement("td");
  var img1 = document.createElement("img");
  img1.src = imgDataURL1;
  cell1.appendChild(img1);
  var img2 = document.createElement("img");
  img2.src = imgDataURL2;
  cell2.appendChild(img2);
  cell3.innerText = barcodeResultsString(results);
  var date = new Date();
  cell4.innerText = date.toLocaleString();
  row.appendChild(cell1);
  row.appendChild(cell2);
  row.appendChild(cell3);
  row.appendChild(cell4);
  document.querySelector("tbody").appendChild(row);
  if (document.getElementById("save-to-indexedDB").checked) {
    saveRecord(imgDataURL1,imgDataURL2,results[0].barcodeText,date.getTime())
  }
}

function captureFrame(canvas,video){
  var w = video.videoWidth;
  var h = video.videoHeight;
  canvas.width  = w;
  canvas.height = h;
  var ctx = canvas.getContext('2d');
  ctx.drawImage(video, 0, 0, w, h);
  return canvas.toDataURL();
}

function barcodeResultsString(results){
  var s = "";
  for (let index = 0; index < results.length; index++) {
    const result = results[index];
    s = result.barcodeFormatString + ": " + result.barcodeText;
    if (index != results.length - 1) {
      s = s + "\n";
    }
  }
  return s;
}

async function saveRecord(img1,img2,barcodeText,timestamp){
  let existingRecords = await recordStore.getItem(barcodeText);
  if (!existingRecords) {
    existingRecords = [];
  }
  existingRecords.push({img1:img1,img2:img2,text:barcodeText,date:timestamp});
  await recordStore.setItem(barcodeText,existingRecords);
}

Source Code

Get the source code of the demo to have a try:

https://github.com/tony-xlh/getUserMedia-multiple-camera

The barcode scanner demo has some features not covered in this article, like drawing barcode overlays and filtering barcodes scanned based on the time span.