关于前端:前端实现很哇塞的浏览器端扫码功能🌟

背景

不久前我做了对于获取浏览器摄像头并扫码辨认的性能,本文中梳理了波及到知识点及具体代码实现,整顿成此篇文章内容。

本文次要介绍,通过应用基于 vue 技术栈的前端开发技术,在浏览器端调起摄像头 📷,并进行扫码辨认性能,对辨认到的二维码进行跳转或其余操作解决。本文内容分为背景介绍、实现成果、技术简介、代码实现、总结等局部组成。

实现成果

本实例中次要有两个页面首页和扫码页,具体实现成果如下图所示。

  • 首页:点击 SCAN QRCODE 按钮,进入到扫码页。
  • 扫码页:首次进入时,或弹出 获取摄像头拜访权限的零碎提示框,点击容许拜访,页面开始加载摄像头数据并开始进行二维码捕捉拾取,若捕捉到二维码,开始进行二维码解析,解析胜利后加载辨认胜利弹窗。

📸 在线体验:https://dragonir.github.io/h5…

📌 提醒:须要在有摄像头设施的浏览器中竖屏拜访。手机横竖屏检测小常识可返回我的另一篇文章《五十音小游戏中的前端常识》 中进行理解。

技术简介

WebRTC API

WebRTC (Web Real-Time Communications) 是一项实时通信技术,它容许网络应用或者站点,在不借助两头媒介的状况下,建设浏览器之间 点对点(Peer-to-Peer) 的连贯,实现视频流和(或)音频流或者其余任意数据的传输。WebRTC 蕴含的这些规范使用户在无需装置任何插件或者第三方的软件的状况下,创立 点对点(Peer-to-Peer) 的数据分享和电话会议成为可能。

三个次要接口

  • MediaStream:可能通过设施的摄像头及话筒取得视频、音频的同步流。
  • RTCPeerConnection:是 WebRTC 用于构建点对点之间稳固、高效的流传输的组件。
  • RTCDataChannel:使得浏览器之间建设一个高吞吐量、低延时的信道,用于传输任意数据。

🔗 返回 MDN 深刻学习:WebRTC_API

WebRTC adapter

尽管 WebRTC 标准曾经绝对健全巩固了,然而并不是所有的浏览器都实现了它所有的性能,有些浏览器须要在一些或者所有的 WebRTC API上增加前缀能力失常应用。

WebRTC 组织在 github 上提供了一个 WebRTC适配器(WebRTC adapter) 来解决在不同浏览器上实现 WebRTC 的兼容性问题。这个适配器是一个 JavaScript垫片,它能够让你依据 WebRTC 标准形容的那样去写代码,在所有反对 WebRTC 的浏览器中不必去写前缀或者其余兼容性解决办法。

🔗 返回 MDN 深刻学习:WebRTC adapter

外围的API navigator.mediaDevices.getUserMedia

网页调用摄像头须要调用 getUserMedia APIMediaDevices.getUserMedia() 会提醒用户给予应用媒体输出的许可,媒体输出会产生一个 MediaStream,外面蕴含了申请的媒体类型的轨道。此流能够蕴含一个视频轨道(来自硬件或者虚构视频源,比方相机、视频采集设施和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚构音频源,比方麦克风、A/D转换器 等等),也可能是其它轨道类型。

它返回一个 Promise 对象,胜利后会 resolve 回调一个 MediaStream对象;若用户回绝了应用权限,或者须要的媒体源不可用,promisereject 回调一个 PermissionDeniedError 或者 NotFoundError 。(返回的 promise对象 可能既不会 resolve 也不会 reject,因为用户不是必须抉择容许或回绝。)

通常能够应用 navigator.mediaDevices 来获取 MediaDevices ,例如:

navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    // 应用这个stream
  })
  .catch(function(err) {
    // 解决error
  })

🔗 返回 MDN 深刻学习:navigator.mediaDevices.getUserMedia

二维码解析库 JSQR

jsQR 是一个纯 JavaScript 二维码解析库,该库读取原始图像或者是摄像头,并将定位,提取和解析其中的任何 QR码

如果要应用 jsQR 扫描网络摄像头流,则须要 ImageData 从视频流中提取,而后能够将其传递给 jsQR

jsQR 导出一个办法,该办法承受 4 个参数,别离是解码的 图像数据 以及 可选的对象 进一步配置扫描行为。

imageData:格局为 [r0, g0, b0, a0, r1, g1, b1, a1, ...]Uint8ClampedArray( 8位无符号整型固定数组)rgba 像素值。

const code = jsQR(imageData, width, height, options);
if (code) {
  console.log('找到二维码!', code);
}

🔗 返回 github 深刻理解:jsQR

代码实现

流程

整个扫码流程如下图所示:页面初始化,先查看浏览器是否反对 mediaDevices 相干API,浏览器进行调去摄像头,调用失败,就执行失败回调;调用胜利,进行捕捉视频流,而后进行扫码辨认,没有扫瞄到可辨认的二维码就持续扫描,扫码胜利后绘制扫描胜利图案并进行胜利回调。

下文内容对流程进行拆分,别离实现对应的性能。

扫码组件 Scaner

页面构造

咱们先看下页面构造,次要由 4 局部组成:

  • 提示框。
  • 扫码框。
  • video:展现摄像头捕捉视频流。
  • canvas: 绘制视频帧,用于二维码辨认。
<template>
  <div class="scaner" ref="scaner">
    <!-- 提示框:用于在不兼容的浏览器中显示提醒语 -->
    <div class="banner" v-if="showBanner">
      <i class="close_icon" @click="() => showBanner = false"></i>
      <p class="text">若以后浏览器无奈扫码,请切换其余浏览器尝试</p>
    </div>
    <!-- 扫码框:显示扫码动画 -->
    <div class="cover">
      <p class="line"></p>
      <span class="square top left"></span>
      <span class="square top right"></span>
      <span class="square bottom right"></span>
      <span class="square bottom left"></span>
      <p class="tips">将二维码放入框内,即可主动扫描</p>
    </div>
    <!-- 视频流显示 -->
    <video
      v-show="showPlay"
      class="source"
      ref="video"
      :width="videoWH.width"
      :height="videoWH.height"
      controls
    ></video>
    <canvas v-show="!showPlay" ref="canvas" />
    <button v-show="showPlay" @click="run">开始</button>
  </div>
</template>

办法:绘制

  • 画线。
  • 画框(用于扫码胜利后绘制矩形图形)。

// 画线
drawLine (begin, end) {
  this.canvas.beginPath();
  this.canvas.moveTo(begin.x, begin.y);
  this.canvas.lineTo(end.x, end.y);
  this.canvas.lineWidth = this.lineWidth;
  this.canvas.strokeStyle = this.lineColor;
  this.canvas.stroke();
},
// 画框
drawBox (location) {
  if (this.drawOnfound) {
    this.drawLine(location.topLeftCorner, location.topRightCorner);
    this.drawLine(location.topRightCorner, location.bottomRightCorner);
    this.drawLine(location.bottomRightCorner, location.bottomLeftCorner);
    this.drawLine(location.bottomLeftCorner, location.topLeftCorner);
  }
},

办法:初始化

  • 查看是否反对。
  • 调起摄像头。
  • 成功失败解决。

// 初始化
setup () {
  // 判断了浏览器是否反对挂载在MediaDevices.getUserMedia()的办法
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    this.previousCode = null;
    this.parity = 0;
    this.active = true;
    this.canvas = this.$refs.canvas.getContext("2d");
    // 获取摄像头模式,默认设置是后置摄像头
    const facingMode = this.useBackCamera ? { exact: 'environment' } : 'user';
    // 摄像头视频解决
    const handleSuccess = stream => {
       if (this.$refs.video.srcObject !== undefined) {
        this.$refs.video.srcObject = stream;
      } else if (window.videoEl.mozSrcObject !== undefined) {
        this.$refs.video.mozSrcObject = stream;
      } else if (window.URL.createObjectURL) {
        this.$refs.video.src = window.URL.createObjectURL(stream);
      } else if (window.webkitURL) {
        this.$refs.video.src = window.webkitURL.createObjectURL(stream);
      } else {
        this.$refs.video.src = stream;
      }
      // 不心愿用户来拖动进度条的话,能够间接应用playsinline属性,webkit-playsinline属性
      this.$refs.video.playsInline = true;
      const playPromise = this.$refs.video.play();
      playPromise.catch(() => (this.showPlay = true));
      // 视频开始播放时进行周期性扫码辨认
      playPromise.then(this.run);
    };
    // 捕捉视频流
    navigator.mediaDevices
      .getUserMedia({ video: { facingMode } })
      .then(handleSuccess)
      .catch(() => {
        navigator.mediaDevices
          .getUserMedia({ video: true })
          .then(handleSuccess)
          .catch(error => {
            this.$emit("error-captured", error);
          });
      });
  }
},

办法:周期性扫描

run () {
  if (this.active) {
    // 浏览器在下次重绘前循环调用扫码办法
    requestAnimationFrame(this.tick);
  }
},

办法:胜利回调

// 二维码辨认胜利事件处理
found (code) {
  if (this.previousCode !== code) {
    this.previousCode = code;
  } else if (this.previousCode === code) {
    this.parity += 1;
  }
  if (this.parity > 2) {
    this.active = this.stopOnScanned ? false : true;
    this.parity = 0;
    this.$emit("code-scanned", code);
  }
},

办法:进行


// 齐全进行
fullStop () {
  if (this.$refs.video && this.$refs.video.srcObject) {
    // 进行视频流序列轨道
    this.$refs.video.srcObject.getTracks().forEach(t => t.stop());
  }
}

办法:扫描

  • 绘制视频帧。
  • 扫码辨认。

// 周期性扫码辨认
tick () {
  // 视频处于筹备阶段,并且曾经加载足够的数据
  if (this.$refs.video && this.$refs.video.readyState === this.$refs.video.HAVE_ENOUGH_DATA) {
    // 开始在画布上绘制视频
    this.$refs.canvas.height = this.videoWH.height;
    this.$refs.canvas.width = this.videoWH.width;
    this.canvas.drawImage(this.$refs.video, 0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
    // getImageData() 复制画布上制订矩形的像素数据
    const imageData = this.canvas.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
    let code = false;
    try {
      // 辨认二维码
      code = jsQR(imageData.data, imageData.width, imageData.height);
    } catch (e) {
      console.error(e);
    }
    // 如果辨认出二维码,绘制矩形框
    if (code) {
      this.drawBox(code.location);
      // 辨认胜利事件处理
      this.found(code.data);
    }
  }
  this.run();
},

父组件

Scaner 的父组件次要加载页面,并展现 Scaner 扫码后果的回调。

页面构造

<template>
  <div class="scan">
    <!-- 页面导航栏 -->
    <div class="nav">
      <a class="close" @click="() => $router.go(-1)"></a>
      <p class="title">Scan QRcode</p>
    </div>
    <div class="scroll-container">
      <!-- 扫码子组件 -->
      <Scaner
        v-on:code-scanned="codeScanned"
        v-on:error-captured="errorCaptured"
        :stop-on-scanned="true"
        :draw-on-found="true"
        :responsive="false"
      />
    </div>
  </div>
</template>

父组件办法

import Scaner from '../components/Scaner';

export default {
  name: 'Scan',
  components: {
    Scaner
  },
  data () {
    return {
      errorMessage: "",
      scanned: ""
    }
  },
  methods: {
    codeScanned(code) {
      this.scanned = code;
      setTimeout(() => {
        alert(`扫码解析胜利: ${code}`);
      }, 200)
    },
    errorCaptured(error) {
      switch (error.name) {
        case "NotAllowedError":
          this.errorMessage = "Camera permission denied.";
          break;
        case "NotFoundError":
          this.errorMessage = "There is no connected camera.";
          break;
        case "NotSupportedError":
          this.errorMessage =
            "Seems like this page is served in non-secure context.";
          break;
        case "NotReadableError":
          this.errorMessage =
            "Couldn't access your camera. Is it already in use?";
          break;
        case "OverconstrainedError":
          this.errorMessage = "Constraints don't match any installed camera.";
          break;
        default:
          this.errorMessage = "UNKNOWN ERROR: " + error.message;
      }
      console.error(this.errorMessage);
     alert('相机调用失败');
    }
  },
  mounted () {
    var str = navigator.userAgent.toLowerCase();
    var ver = str.match(/cpu iphone os (.*?) like mac os/);
    // 经测试 iOS 10.3.3以下零碎无奈胜利调用相机摄像头
    if (ver && ver[1].replace(/_/g,".") < '10.3.3') {
     alert('相机调用失败');
    }
  }

残缺代码

🔗 github: https://github.com/dragonir/h…

总结

利用扩大

我感觉以下几个性能都是能够通过浏览器调用摄像头并扫描辨认来实现的,大家感觉还有哪些 很哇塞🌟 的性能利用能够通过浏览器端扫码实现 😂

  • 🌏 链接跳转。
  • 🛒 价格查问。
  • 🔒 登录认证。
  • 📂 文件下载。

兼容性

  • 即便应用了 adaptergetUserMedia API 在局部浏览器中也存在不反对的。
  • 低版本浏览器(如 iOS 10.3 以下)、Android 小众浏览器(如 IQOO 自带浏览器)不兼容。
  • QQ微信 内置浏览器无奈调用。

参考资料

  • [1]. Taking still photos with WebRTC
  • [2]. Choosing cameras in JavaScript with the mediaDevices API
  • [3]. 如何应用JavaScript拜访设施前后摄像头

作者:dragonir 本文地址:https://segmentfault.com/a/11…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理