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

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

## 背景
- Vuejsでできること色々調べる機会があった
- いろんなWebAPIが存在していることに気づいた
  - [Image Capture API](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Image_Capture_API)
  - [Web Bluetooth API](https://ics.media/entry/15520)
- 案件の仕様の中で**webアプリケーション内のみで**qr読み取る必要が出てきた

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

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

実装後デモ▼
![chat_2.gif](https://qiita-image-store.s3.amazonaws.com/0/158393/8195c676-12bf-d908-f112-7622efdedf50.gif)

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

試してみた。

```javascript

// 顔認識
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`はバグが発見されているので、廃止予定となっている。

- [Navigator.getUserMedia](https://developer.mozilla.org/ja/docs/Web/API/Navigator/getUserMedia)
- [MediaDevices.getUserMedia()](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia)

試して見た。

js部分▼

```javascript
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部分▼

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

firefoxでできた！！

<img width="1132" alt="スクリーンショット 2018-06-13 23.21.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/158393/05156de0-ad1b-9983-9815-81c47f52adff.png">


しかし、createObjectURLは廃止予定だから使うな！という警告をconsoleログで受けた。。。
[調べてみるとバグがあるらしい。](https://www.fxsitecompat.com/ja/docs/2017/url-createobjecturl-stream-has-been-deprecated/)

<img width="1435" alt="スクリーンショット 2018-06-13 23.22.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/158393/72a2f1f7-17ae-f02c-608b-a144f89e89ea.png">


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

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

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

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

```javascript
 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](https://qiita-image-store.s3.amazonaws.com/0/158393/399feaaa-1680-99c2-e9cc-d52af2cec98d.gif)

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

## [LazarSoft/jsqrcode](https://github.com/LazarSoft/jsqrcode) (不採用)

[こちらの記事](https://qiita.com/shora_kujira16/items/a9e3aa0b06525c00a7e0)を参考にQR画像からデータをデコードするライブラリを探した。

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

## [cozmo/jsQR](https://github.com/cozmo/jsQR)

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

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

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

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

```javascript
      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読取できるように修正

```javascript
      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として扱うことにした。修正したもの▼

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

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

```javascript
	
	// 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コンポーネントにまとめることができました。

```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](https://qiita-image-store.s3.amazonaws.com/0/158393/8195c676-12bf-d908-f112-7622efdedf50.gif)

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

## 所感
- もはやwebでなんでもできるなあと思った。こういうところからpwaが流行りそうな理由なのか？
- QRが遠いと思ったより認識が弱い。
  - 他のライブラリと比較したい(するとはいっていない)
- アンドロイド・iPhoneでの検証は未検証なのできちんとやりたい。
- [Web Bluetooth API](https://ics.media/entry/15520)も触りたい