h5纯网页实现调用设备摄像头进行扫描二维码

注意: 仅支持 https
兼容性自测:在 android7 上的 UCchromeios13.3.1上的 safri 浏览器上测试可正常扫码, ios11 上测试有问题

主要过程就是利用 MediaDevices.getUserMedia() 拿到 设备摄像头 的媒体流,使用 video 播放,然后 使用canvas每一帧 绘制 video 的画面, 再定时使用 qrcode 来解析 canvas 生成的图片,识别二维码

MDN 上的MediaDevices.getUserMedia()MediaDevices.enumerateDevices() 介绍

MediaDevices.getUserMedia() 会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道。 此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。它返回一个 Promise 对 象,成功后会resolve回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise会reject回调一个 PermissionDeniedError 或者 NotFoundError 。

MediaDevices 的方法 enumerateDevices() 请求一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备
等。 返回的 Promise 完成时,会带有一个描述设备的 MediaDeviceInfo 的数组。

首先在 index.html 里引入 qrcode 相关的库文件(包含了解析二维码图片),qrcode下载地址,另外,本示例写在 vue 项目里

作者的qrcode项目的git地址:https://github.com/LazarSoft/jsqrcode

<body>
    <div id="app"></div>
    <!-- built files will be auto injected -->

    <script type="text/javascript" src="qrcode/grid.js"></script>
    <script type="text/javascript" src="qrcode/version.js"></script>
    <script type="text/javascript" src="qrcode/detector.js"></script>
    <script type="text/javascript" src="qrcode/formatinf.js"></script>
    <script type="text/javascript" src="qrcode/errorlevel.js"></script>
    <script type="text/javascript" src="qrcode/bitmat.js"></script>
    <script type="text/javascript" src="qrcode/datablock.js"></script>
    <script type="text/javascript" src="qrcode/bmparser.js"></script>
    <script type="text/javascript" src="qrcode/datamask.js"></script>
    <script type="text/javascript" src="qrcode/rsdecoder.js"></script>
    <script type="text/javascript" src="qrcode/gf256poly.js"></script>
    <script type="text/javascript" src="qrcode/gf256.js"></script>
    <script type="text/javascript" src="qrcode/decoder.js"></script>
    <script type="text/javascript" src="qrcode/qrcode.js"></script>
    <script type="text/javascript" src="qrcode/findpat.js"></script>
    <script type="text/javascript" src="qrcode/alignpat.js"></script>
    <script type="text/javascript" src="qrcode/databr.js"></script>
</body>

页面上一个 canvas 和一个 video

<div class="scan-modal">
    <canvas id="qr-canvas" ref="canvas"></canvas>
    <div class="scan-tips">放入框内,自动扫描</div>
    <div id="outdiv" style="display: block;transform: scale(1);opacity: 0">
    <video id="video"
      ref="video"
      :muted="true"
      webkit-playsinline="true"
      :width="vWidth"
      :height="vHeight"
      :autoplay="true"
      x5-video-player-type="h5"
      playsinline></video>
   </div>
</div>

这块自动播放在某些设备上会出问题,注意加上 `autoplay` 和 `muted` 属性

第一步,兼容处理 getUserMediaenumerateDevices (android部分摄像头获取处理)


function isAndroid() {
   var u = navigator.userAgent;
   return u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
 }

let mediaConfig = isAndroid() ? {} : {
   audio: false,
   video: {facingMode: "environment"}
}

if (isAndroid) {
   // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
  if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
  }

  if (navigator.mediaDevices.enumerateDevices === undefined) {
    navigator.mediaDevices.enumerateDevices = function (constraints) {
      var enumerateDevices = navigator.enumerateDevices
      // 首先,如果有enumerateDevices的话,就获得它

      // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
      if (!enumerateDevices) {
         return Promise.reject(new Error('enumerateDevices is not implemented in this 
      browser'));
      }

      // 否则,为老的navigator.enumerateDevices方法包裹一个Promise
      return new Promise(function (resolve, reject) {
        enumerateDevices.call(navigator, constraints, resolve, reject);
      });
    }
  }

  // 这一步操作是为了处理android设备的摄像头获取方式
  navigator.mediaDevices.enumerateDevices().then(function (devices) {
    let videos = []

    devices.forEach(function (dv) {
      var kind = dv.kind;
      if (kind.indexOf('video') !== -1) {
         videos.push(dv.deviceId);
      }
    });
     
   // 默认使用后置摄像头。根据测试,在android手机上后置摄像头的id会在之后获取
   mediaConfig = {
      audio: false,
      video: {deviceId: videos[videos.length - 1]}
      }
   });
}

// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
   navigator.mediaDevices.getUserMedia = function (constraints) {
   var getUserMedia = navigator.getUserMedia || navigator.webKitGetUserMedia ||
   navigator.moxGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
   // 首先,如果有getUserMedia的话,就获得它

   // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
   if (!getUserMedia) {
      return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
   }

   // 否则,为老的navigator.getUserMedia方法包裹一个Promise
      return new Promise(function (resolve, reject) {
         getUserMedia.call(navigator, constraints, resolve, reject);
      });
     }
}


第二步,获取媒体流,使用 video 播放


let _this = this
navigator.mediaDevices.getUserMedia(mediaConfig).then(function (stream) {
   _this.stream = stream
   // 旧的浏览器可能没有srcObject
   _this.$nextTick(_ => {
       _this.video = document.getElementById('video');
       _this.canvas = document.getElementById("qr-canvas");
       var width = document.body.clientWidth - 30;
       var height = width = 260;
       _this.canvas.style.width = width + "px";
       _this.canvas.style.height = height + "px";
       _this.canvas.width = width;
       _this.canvas.height = height;
       _this.ctx = _this.$refs.canvas.getContext("2d");
       _this.ctx.clearRect(0, 0, 260, 260);
       _this.vHeight = height + "px";
       _this.vWidth = width + "px";

       if ("srcObject" in video) {
          _this.video.srcObject = _this.stream;
          _this.video.muted = true
       } else {
         // 防止在新的浏览器里使用它,应为它已经不再支持了
          _this.video.src = window.URL.createObjectURL(_this.stream);
       }
       _this.video.onloadedmetadata = function (e) {
         _this.playVideo()
        };
       })
      })
     .catch(function (err) {
        _this.isScan = false
        console.log(err.name + ": " + err.message);
     });

第三步,使用 canvas 每一帧绘制 video 画面,使用 qrcode 定时解析图片

  
  playVideo() {
      let _this = this
      _this.video.muted = true
      _this.video.play();
      _this.$nextTick(_ => {
        _this.drawInterval = window.setInterval(function () {
          _this.ctx.drawImage(_this.video, 0, 0);
        }, 60);
        // // 定时进行图片转换成二维码
        _this.getImgTiming = window.setInterval(function () {
          _this.getQrCode();
        }, 1000);
      })
 }

// 关闭摄像头
    closeCamera() {
      if (!this.$refs['video']) {
        return
      }
      if (!this.$refs['video'].srcObject) {
        this.$refs['video'].src = null;
        return
      }
      let stream = this.$refs['video'].srcObject
      let tracks = stream.getTracks()
      tracks.forEach(track => {
        track.stop()
      })
      this.$refs['video'].srcObject = null
    }
    
    // qrcode解析回调
     qrcode.callback = function (res) {
        if (res == 'error decoding QR Code') {
        } else {
          clearInterval(_this.drawInterval)
          clearInterval(_this.getImgTiming);
          _this.code = res 
          _this.isScan = false
          _this.video && _this.video.pause()
          return false;
        }
      };


   // 转化图片,并解析
    getQrCode() {
      var dataURL = this.canvas.toDataURL("image/png");
      var re = this.getBlobBydataURI(dataURL, 'image/png');
      qrcode.decode(this.getObjectURL(re));
    },


    getObjectURL(file) {
      var url = null;
      if (window.createObjectURL != undefined) { // basic
        url = window.createObjectURL(file);
      } else if (window.URL != undefined) { // mozilla(firefox)
        url = window.URL.createObjectURL(file);
      } else if (window.webkitURL != undefined) { // webkit or chrome
        url = window.webkitURL.createObjectURL(file);
      }
      return url;
    },


    getBlobBydataURI(dataURI, type) {
      var binary = atob(dataURI.split(',')[1]);
      var array = [];
      for (var i = 0; i < binary.length; i++) {
        array.push(binary.charCodeAt(i));
      }
      return new Blob([new Uint8Array(array)], {type: type});
    },


相关资料

https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/mediaDevices

如果觉得此文章有用或者有其他使用问题,欢迎在下方留言~


版权属于:xigua

本文链接:https://xianh5.com/archives/19/

转载时须注明出处及本声明

最后修改:2020 年 03 月 30 日
如果觉得我的文章对你有用,请随意赞赏或留下你的评论~