31
头图

background

Not long ago, I did a function about obtaining the browser camera and scanning code recognition. In this article, I sorted out the knowledge points and specific code implementations, and organized them into this article.

This article mainly introduces that by using the front-end development technology vue 📷 is adjusted on the browser side, and the code scanning recognition function is performed, and the recognized QR code is jumped or processed by other operations. The content of this article is divided into background introduction, realization effect, technical introduction, code realization, summary and other parts.

Achieve effect

In this example, there are mainly two pages, the home page and the code scan page. The specific implementation effect is shown in the figure below.

  • Home page: Click the SCAN QRCODE button to enter the scan code page.
  • Scan code page: When you enter for the first time, or the system prompt box of obtain camera access permission pops up, click to allow access, the page starts to load the camera data and start to capture the QR code, if the QR code is captured, start the QR code After the analysis is successful, the pop-up window of successful recognition will be loaded.

📸 online experience: https://dragonir.github.io/h5-scan-qrcode

📌 Tip: You need to access it in a vertical screen in a browser with a camera device. For mobile phone horizontal and vertical screen detection go to my other article 16167882b928bb "Front-end knowledge in games" 16167882b928be.

Technology Brief

WebRTC API

the WebRTC (Web Real-Time Communications) is a real-time communication technology , which allows network applications or sites, in without the aid of an intermediary case, establishing between the browser peer (Peer-to-Peer) of Connect to realize the transmission of video stream and (or) audio stream or other arbitrary data. WebRTC Peer-to-Peer data sharing and conference calls without installing any plug-ins or third-party software.

Three main interfaces :

  • MediaStream : Able to obtain a synchronized stream of video and audio through the device's camera and microphone.
  • RTCPeerConnection : 06167882b92a39 is a WebRTC used to build stable and efficient streaming between peers.
  • RTCDataChannel : Makes a high-throughput, low-latency channel between browsers for the transmission of arbitrary data.
🔗 Go to MDN in-depth study: WebRTC_API

WebRTC adapter

Although the WebRTC specification is relatively sound and stable, not all browsers implement all of its functions. Some browsers need WebRTC API in order to be used normally.

WebRTC organization provides a WebRTC adapter github to solve the compatibility problem WebRTC on different browsers. This adapter is a JavaScript shim, which allows you to write code as described in the WebRTC specification, without writing prefixes or other compatibility solutions WebRTC

🔗 Go to MDN in-depth study: WebRTC adapter

Core API navigator.mediaDevices.getUserMedia

The webpage calls the camera need to call getUserMedia API , MediaDevices.getUserMedia() will prompt the user to give permission to use the media input, the media input will generate a MediaStream , which contains the track of the requested media type. This stream can contain a video track (from hardware or virtual video sources, such as cameras, video capture devices, and screen sharing services, etc.), an audio track (also from hardware or virtual audio sources, such as microphones, A/D converters, etc.) Etc.), it may also be other track types.

It returns a Promise object, and after success, it will resolve back a MediaStream object by 06167882b92d7f; if the user refuses the permission or the required media source is not available, promise will reject back a PermissionDeniedError or NotFoundError . (The returned promise object may be neither resolve nor reject , because the user does not have to choose to allow or deny.)

You can usually use navigator.mediaDevices to get MediaDevices , for example:

navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    // 使用这个stream
  })
  .catch(function(err) {
    // 处理error
  })
🔗 go to MDN to learn more: navigator.mediaDevices.getUserMedia

QR code analysis library JSQR

jsQR is a pure JavaScript QR code parsing library, which reads the original image or camera, and locates, extracts and parses any QR code in it.

If you want to use jsQR scan the webcam stream, you need ImageData to extract from the video stream, and then you can pass it to jsQR .

jsQR derives a method that accepts 4 parameters, which are decoded image data, width, height, and optional objects to further configure the scanning behavior.

imageData : format [r0, g0, b0, a0, r1, g1, b1, a1, ...] of Uint8ClampedArray (bit unsigned integer. 8 fixed array) rgba pixel value.

const code = jsQR(imageData, width, height, options);
if (code) {
  console.log('找到二维码!', code);
}
🔗 Go to github to learn more: jsQR

Code

Process

The entire code scanning process is shown in the following figure: page initialization, first check whether the browser supports mediaDevices related API , the browser adjusts the camera, the call fails, the failure callback is executed; the call is successful, the video stream is captured, and then the code scan recognition is performed , Continue to scan without scanning a recognizable QR code. After the code is scanned successfully, a successful scan pattern is drawn and a successful callback is performed.

The following content splits the process and realizes the corresponding functions respectively.

Scan code component Scaner

Page structure

Let's first look at the page structure, which is mainly composed of 4 :

  • Prompt box.
  • Scan code box.
  • video : Show that the camera captures the video stream.
  • canvas : Draw video frames for QR code recognition.
<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>

Method: draw

  • Draw a line.
  • Picture frame (used to draw rectangular graphics after scanning the code successfully).

// 画线
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);
  }
},

Method: Initialize

  • Check if it is supported.
  • Turn up the camera.
  • Success and failure processing.

// 初始化
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);
          });
      });
  }
},

Method: Periodic scanning

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

Method: success callback

// 二维码识别成功事件处理
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);
  }
},

Method: stop


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

Method: Scan

  • Draw the video frame.
  • Scan code recognition.

// 周期性扫码识别
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();
},

Parent component

Scaner mainly loads the page and displays the callback of the scanning result of Scaner

Page structure

<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>

Parent component method

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('相机调用失败');
    }
  }

Complete code

🔗 github: https://github.com/dragonir/h5-scan-qrcode

Summarize

Application extension

I think the following functions can be achieved by calling the camera and scanning recognition through the browser. What functional applications do you think are very wow, 16167882b93b19 can be achieved by scanning the code on the browser side 😂 ?

  • 🌏 link jump.
  • 🛒 price inquiry.
  • 🔒 login authentication.
  • 📂 file download.

compatibility

  • even with adapter , getUserMedia API there are not supported in some browsers.
  • low-version browsers (such as iOS 10.3 below), Android niche browsers (such as IQOO own browser) are not compatible.
  • QQ , WeChat built-in browser cannot be called.

Reference

address of this article: 16167882b93d5a https://segmentfault.com/a/1190000040809122

dragonir
1.8k 声望3.9k 粉丝

Accepted ✔