JavaScript
QRcode
getUserMedia
vue.js

サクッとjsでQRリーダー実装

こんばんは。
最近チャリンコを購入して、チャリンコ通勤始めました。道中のお弁当屋とか発見できてQOL爆上げ中のmorifujiです。

背景

  • Vuejsでできること色々調べる機会があった
  • いろんなWebAPIが存在していることに気づいた
  • 案件の仕様の中でwebアプリケーション内のみでqr読み取る必要が出てきた

目標

  1. ブラウザで指定のURLを開き
  2. QRリーダーを起動(スマホのカメラ画面にならずに)
  3. QRを撮影すると、瞬時に解析して画面に表示

※スマートフォンでの検証はまだ。確認し次第追記します :bow:

実装後デモ▼
chat_2.gif

shape detection API (不採用)

リアルタイムに物体を認識するためのAPI。
人間の顔(FaceDetection)とバーコード(BarcodeDetection)の二種類がある。
https://wicg.github.io/shape-detection-api/

試してみた。

// 顔認識
if (window.FaceDetector == undefined) {
  console.error('Face Detection not supported on this platform');
}

// バーコード認識
if (window.BarcodeDetector == undefined) {
  console.error('Barcode Detection not supported on this platform');
}

let barcodeDetector = new BarcodeDetector();
// Assuming |theImage| is e.g. a <img> content, or a Blob.
barcodeDetector.detect(theImage)
.then(detectedCodes => {
  for (const barcode of detectedCodes) {
    console.log(' Barcode ${barcode.rawValue}' +
        ' @ (${barcode.boundingBox.x}, ${barcode.boundingBox.y}) with size' +
        ' ${barcode.boundingBox.width}x${barcode.boundingBox.height}');
  }
}).catch(() => {
  console.error("Barcode Detection failed, boo.");
})

結果。どれも未対応。悲しい

ブラウザ 対応有無
firefox X
iOSのsafari X
chrome X

getUserMedia

WebRTCを実現するためのAPIの一種。以下の二種類あるが、前者Navigatorはバグが発見されているので、廃止予定となっている。

試して見た。

js部分▼

let p = navigator.mediaDevices.getUserMedia({ audio: false, video: true });

p.then(function(stream) {
   var video = document.querySelector('video');
   video.src = window.URL.createObjectURL(mediaStream);
   video.onloadedmetadata = function(e) {
      // Do something with the video here.
   };
};

html部分▼

        <div class="container">
          <div class="columns">
            <div class="column">
              <video
                :width="width"
                :height="height"
                autoplay/>
            </div>
           </div>
         </div>

firefoxでできた!!

スクリーンショット 2018-06-13 23.21.13.png

しかし、createObjectURLは廃止予定だから使うな!という警告をconsoleログで受けた。。。
調べてみるとバグがあるらしい。

スクリーンショット 2018-06-13 23.22.46.png

createObjectURLではなく、videoのdomのsrcObjectに直接ぶっ込めばいいとのこと。修正▼

// var video = document.querySelector('video');
// video.src = window.URL.createObjectURL(mediaStream);
document.querySelector("video").srcObject = mediaStream;

canvasにコピー

qrを読み取るライブラリはあまたあるようだが、そのどれも、 canvasからimageを取得している。
なのでまずはwebカメラを表示しているvideoタグの動画を、canvasにコピーしないといけない。

以下、先ほどのコードに追記

 const video = document.querySelector("video");
 const canv = document.querySelector("canvas");
 const context = canv.getContext("2d");

// canvasに描写
 context.drawImage(video, 0, 0, this.width, this.height);

これでボタンを押すたびにvideoタグからcanvasにコピーする(スクショする)ような感じになった。

chat.gif

※上のgifだと、domのサイズ指定してないのでcanvasが引き伸ばされてます

LazarSoft/jsqrcode (不採用)

こちらの記事を参考にQR画像からデータをデコードするライブラリを探した。

start数につられてLazarSoft/jsqrcodeを使おうとしたが、良いサンプルがなかったのと、最終更新が古かったので、やめた、

cozmo/jsQR

最近も開発が進んでいるようなので、このライブラリに決めた。使い方はこんな感じ

const code = jsQR(imageData, width, height);

if (code) {
  console.log("Found QR code", code);
}

めっちゃ簡単。今までのと合わせると、以下のような感じ

      const video = document.querySelector("video");
      let canv = document.querySelector("canvas");

      const context = canv.getContext("2d");
      context.drawImage(video, 0, 0, this.width, this.height);
      const imageData = context.getImageData(0, 0, this.width, this.height);
      const code = jsQR(imageData.data, imageData.width, imageData.height);
      if (code) {
        console.log("Found QR code", code, code.data);
      }

jsQR()の第一引数には、Uint8ClampedArray型が来るはずだが、どうやら canvas.getContext("2d").getImageData(0, 0, this.width, this.height);で取得できるようだ。

定期的に画像を認識

今の状態だと、QRを認識するには毎回ボタンを押す必要があるので、setIntervalで毎時間(0.5秒)ごとにcanvas描写&qr読取できるように修正

      const video = document.querySelector("video");
      let canv = document.querySelector("canvas");

      const context = canv.getContext("2d");
      setInterval(() => {
          context.drawImage(video, 0, 0, this.width, this.height);
          const imageData = context.getImageData(0, 0, this.width, this.height);
          const code = jsQR(imageData.data, imageData.width, imageData.height);
          if (code) {
              console.log("Found QR code", code, code.data);
        }
      }, 500);

負荷を軽減

macがホカホカになっていたので、負荷を軽減したい。
1:videoタグとcanvasタグで同時に描写が起こってるので、canvasタグの方をjs内で仮想DOMとして扱うことにした。修正したもの▼

// let canv = document.querySelector("canvas");
let canv = document.createElement("canvas");
canv.height = this.height;
canv.width = this.width;

2:videoタグのフレームレートを落とす
getUserMedia()の引数を以下のように修正

    // let p = navigator.mediaDevices.getUserMedia({ audio: false, video: true });
    let p = navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          width: this.width,
          height: this.height,
          frameRate: { ideal: 5, max: 15 }
        }
    });

まとめ

筆者はvuejsのsfcでwebを作っているので、こんな感じのvueコンポーネントにまとめることができました。

<template>
  <div>
    <section>
      <div class="container">
        <div class="columns">
          <div class="column">
            <b-message
              :active.sync="isReadQr"
              title="読み取り内容">
              {{ json }}
            </b-message>
            {{ isReadQr }}
          </div>
        </div>
      </div>
    </section>
    <section class="hero is-medium has-text-centered">
      <div class="hero-body hero-body-hp-main">
        <div class="container">
          <div class="columns">
            <div class="column">
              <button
                class="button"
                @click="cameraStart">カメラスタート</button>
            </div>
            <div class="column">
              <button
                class="button"
                @click="readImage">読み取りスタート</button>
            </div>
          </div>
          <div class="columns">
            <div class="column">
              <video
                :width="width"
                :height="height"
                autoplay/>
            </div>
            <div class="column">
              <div :style="{ width: width + 'px', height: height+ 'px' }">
                <!-- canvasなしでも仮想DOMを作成して描画 -->
                <!-- <canvas
                  :width="width"
                  :height="height"
                /> -->
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<script>
import jsQR from "jsQR";

export default {
  name: "Qr",
  data() {
    return {
      srcObject: "",
      width: 500,
      height: 500,
      json: null
    };
  },
  computed: {
    isReadQr: () => {
      return Boolean(this.json);
    }
  },
  methods: {
    cameraStart() {
      const p = navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
          width: this.width,
          height: this.height,
          frameRate: { ideal: 5, max: 15 }
        }
      });
      p.then(function(mediaStream) {
        document.querySelector("video").srcObject = mediaStream;
      });
    },
    readImage() {
      const video = document.querySelector("video");
      const canv = document.createElement("canvas");
      canv.height = this.height;
      canv.width = this.width;

      const context = canv.getContext("2d");

      setInterval(() => {
        console.log("search .....");
        context.drawImage(video, 0, 0, this.width, this.height);
        const imageData = context.getImageData(0, 0, this.width, this.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height);
        if (code) {
          console.log("Found QR code", code, code.data);
          this.json = code.data;
        }
      }, 500);
    }
  }
};
</script>

デモ▼
chat_2.gif

行きつけの美容院さんのQRコードを使わさせていただきました。感謝 :bow:

所感

  • もはやwebでなんでもできるなあと思った。こういうところからpwaが流行りそうな理由なのか?
  • QRが遠いと思ったより認識が弱い。
    • 他のライブラリと比較したい(するとはいっていない)
  • アンドロイド・iPhoneでの検証は未検証なのできちんとやりたい。
  • Web Bluetooth APIも触りたい