前端實現很哇塞的瀏覽器端掃碼功能🌟

背景

不久前我做了關於獲取瀏覽器攝影機並掃碼識別的功能,本文中梳理了涉及到知識點及具體程式碼實現,整理成此篇文章內容。

本文主要介紹,通過使用基於 vue 技術棧的前端開發技術,在瀏覽器端調起攝影機 📷,並進行掃碼識別功能,對識別到的二維碼進行跳轉或其他操作處理。本文內容分為背景介紹、實現效果、技術簡介、程式碼實現、總結等部分組成。

實現效果

本實例中主要有兩個頁面首頁和掃碼頁,具體實現效果如下圖所示。

  • 首頁:點擊 SCAN QRCODE 按鈕,進入到掃碼頁。
  • 掃碼頁:首次進入時,或彈出 獲取攝影機訪問許可權的系統提示框,點擊允許訪問,頁面開始載入攝影機數據並開始進行二維碼捕獲拾取,若捕獲到二維碼,開始進行二維碼解析,解析成功後載入識別成功彈窗。

📸 在線體驗://dragonir.github.io/h5-scan-qrcode

📌 提示:需要在有攝影機設備的瀏覽器中豎屏訪問。手機橫豎屏檢測小知識可前往我的另一篇文章《五十音小遊戲中的前端知識》 中進行了解。

技術簡介

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: //github.com/dragonir/h5-scan-qrcode

總結

應用擴展

我覺得以下幾個功能都是可以通過瀏覽器調用攝影機並掃描識別來實現的,大家覺得還有哪些 很哇塞🌟 的功能應用可以通過瀏覽器端掃碼實現 😂

  • 🌏 鏈接跳轉。
  • 🛒 價格查詢。
  • 🔒 登錄認證。
  • 📂 文件下載。

兼容性

  • 即使使用了 adaptergetUserMedia API 在部分瀏覽器中也存在不支援的。
  • 低版本瀏覽器(如 iOS 10.3 以下)、Android 小眾瀏覽器(如 IQOO 自帶瀏覽器)不兼容。
  • QQ微信 內置瀏覽器無法調用。

參考資料

作者:dragonir 本文地址://www.cnblogs.com/dragonir/p/15405141.html