浏览器中打开多个摄像头

WebRTC是一种使Web应用程序和网站能够捕获和流式传输音频或视频媒体,以及在浏览器之间交换任意数据的技术。我们可以使用它在浏览器中打开本地相机和流式传输远程相机的内容。

在本文中,我们将尝试使用WebRTC的getUserMedia接口在网页中打开多个摄像头。

在线demo:

  • 简单版。打开两个摄像头的一个简单demo。
  • 扫码版。打开两个摄像头,使用Dynamsoft Barcode Reader从一个摄像头扫码。相机画面和码值会将保存到IndexedDB中。主要模仿快递超市取件机器的功能。

条码扫描应用的演示视频

支持的平台

在Android和iOS设备上没有办法在浏览器同时打开多个摄像头。PC设备可以正常打开多个摄像头。

编写打开两个摄像头的页面

先写一个打开两个摄像头的页面。

新建HTML文件

创建一个包含以下内容的新HTML文件。

html 复制代码
<!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>

请求相机权限

页面加载后请求相机权限。

js 复制代码
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);
  }
}

列出相机设备

select元素中列出相机设备。

js 复制代码
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;
}

打开摄像头

点击按钮后打开相机。

js 复制代码
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);
  });
}

打开两个摄像头的页面就写好了。

添加条码扫描功能

接下来,将条码扫描功能添加到页面中。

添加类库

添加Dynamsoft Barcode Reader。

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

添加localForage便于操作IndexedDB。

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

从视频帧扫描条码

  1. 使用许可证初始化Dynamsoft Barcode Reader。可以在此处申请许可证。

    js 复制代码
    async function initDBR(){
      Dynamsoft.DBR.BarcodeScanner.license = 'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='; //one-day public trial
      scanner = await Dynamsoft.DBR.BarcodeScanner.createInstance();
    }
  2. 创建一个interval,不断捕获视频帧并解码。

    js 复制代码
    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;
      }
    }

在表中显示扫描结果并保存到IndexedDB

找到条码后,使用Canvas从相机捕获帧,保存为dataURL,并将它们与条码和日期一起显示在表中。同时也保存到IndexedDB中做持久性数据存储。

js 复制代码
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);
}

源代码

欢迎下载源代码并尝试使用:

github.com/tony-xlh/ge...

demo包含本文没有介绍的功能,比如在扫描界面上绘制条码和根据时间过滤扫描到的条码以避免重复读码。

相关推荐
Mintopia13 分钟前
像素的进化史诗:计算机图形学与屏幕的千年之恋
前端·javascript·计算机图形学
Mintopia16 分钟前
Three.js 中三角形到四边形的顶点变换:一场几何的华丽变身
前端·javascript·three.js
归于尽31 分钟前
async/await 从入门到精通,解锁异步编程的优雅密码
前端·javascript
陈随易32 分钟前
Kimi k2不行?一个小技巧,大幅提高一次成型的概率
前端·后端·程序员
猩猩程序员38 分钟前
Rust 动态类型与类型反射详解
前端
杨进军39 分钟前
React 实现节点删除
前端·react.js·前端框架
yanlele1 小时前
【实践篇】【01】我用做了一个插件, 点击复制, 获取当前文章为 Markdown 文档
前端·javascript·浏览器
爱编程的喵1 小时前
React useContext 深度解析:告别组件间通信的噩梦
前端·react.js
望获linux2 小时前
【实时Linux实战系列】多核同步与锁相(Clock Sync)技术
linux·前端·javascript·chrome·操作系统·嵌入式软件·软件
魂祈梦2 小时前
rsbuild的环境变量
前端