Nuxt.jsでバーコードリーダを作ったら、いろいろハマった上にiOSのPWAでカメラにアクセスできなかった話

Nuxt.jsで開発している[Webサービス](https://tsundoku.site)にバーコードリーダ機能をつけようとしたら、
いろいろハマったので、そのときの備忘録。

利用したのは[QuaggaJS](https://serratus.github.io/quaggaJS/)。簡単に使えて便利(*´ω｀*)

はまったポイントは、以下の4点...
1. httpsじゃないとカメラを取得できない
2. QuaggaJSで表示されてないHTML要素を指定するとエラー
3. size/width/heightを指定してもいい感じにならない
4. iOSのPWAではカメラにアクセスできない

いろいろハマったけど、QuaggaJS自体がすごく良いので、サクッとできた♪

### 作ったのはこんな感じ

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">バーコードリーダ、できてきた♪<br>いい感じな気がする(*´ω｀*) <a href="https://t.co/eDJH4P57pZ">pic.twitter.com/eDJH4P57pZ</a></p>&mdash; 積読ハウマッチ📚きらぷか (@kira_puka) <a href="https://twitter.com/kira_puka/status/1173965092649521152?ref_src=twsrc%5Etfw">September 17, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

動きとしては、こんな感じでシンプル。

1. ボタンを押すと、モーダルが開いて、カメラ移動
1. バーコードを読み取り終えると、終了してPageに結果を返す

<img width="600" alt="スクリーンショット 2019-09-18 8.18.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b3d4472e-8214-c260-b627-e3c7f603a7f5.png">

---

## QuaggaJSを使ってみる

使い方はこんな感じ。

### まずはインストール

```console
$ npm i -S quagga
```

### バーコードリーダモーダルはこんな感じ

CSSには[Bulma](https://bulma.io)を利用してます。

```html
<template>
  <div class="dialog modal is-active" v-if="active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <!-- カメラの映像を表示させるDIV -->
      <div id="camera-area" class="camera-area"></div>
    </div>
    <!-- 右上の閉じるボタン -->
    <button class="modal-close is-large" aria-label="close" @click.prevent.stop="onClickCancel"></button>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Emit, Watch } from "nuxt-property-decorator";

@Component
export default class ModalReader extends Vue {
  // モーダルの表示/非表示のprop
  @Prop({ required: true }) active!: boolean;

  // QuaggaJSのインスタンス
  private Quagga;

  /**
   * Vueインスタンス破棄時、カメラを起動していたらストップする
   */
  destroyed() {
    if (!!this.Quagga) this.Quagga.stop();
  }
  
  // ****************************************************
  // * watch
  // ****************************************************
  /**
   * this.activeを監視してQuaggaの開始/停止をする
   */
  @Watch("active")
  private onChangeActive(val: boolean) {
    this.$nextTick(() => {
      if (val) {
        // モーダル表示時、Quaggaを起動
        this.initQuagga();
      } else {
        // モーダル非表示時、Quaggaを停止
        if (!!this.Quagga) this.Quagga.stop();
      }
    });
  }

  // ****************************************************
  // * methods
  // ****************************************************
  /**
   * Quaggaの初期化処理
   */
  private initQuagga() {
    // requireで読み込み
    this.Quagga = require("quagga");
    
    // バーコード検出時の処理を設定
    this.Quagga.onDetected(this.onDetected);
    
    // Quaggaの設定項目
    const config = {
      // カメラの映像の設定
      inputStream: {
        type: "LiveStream",
        // カメラ映像を表示するHTML要素の設定
        target: document.querySelector("#camera-area"),
        // バックカメラの利用を設定. (フロントカメラは"user")
        constraints: { facingMode: "environment" },
        // 検出範囲の指定: 上下30%は対象外
        area: { top: "30%", right: "0%", left: "0%", bottom: "30%" }
      },
      // 解析するワーカ数の設定
      numOfWorkers: navigator.hardwareConcurrency || 4,
      // バーコードの種類を設定: ISBNは"ean_reader"
      decoder: { readers: ["ean_reader"] }
    }
    // 初期化の開始。合わせて、初期化後の処理を設定
    this.Quagga.init(config, this.onInitilize);
  }
  
  /**
   * Quaggaの初期化完了後の処理
   * errorがなければ、起動する
   */
  private onInitilize(error) {
    if (!!error) {
      // エラーがある場合は、キャンセルをEmitする
      console.error(`Error: ${error}`, error);
      this.onClickCancel();
      return;
    }
    
    // エラーがない場合は、読み取りを開始
    console.info("Initialization finished. Ready to start");
    this.Quagga.start();
  }

  /**
   * バーコード検出時の処理
   */
  private onDetected(success) {
    // ISBNは'success.codeResult.code'から取得
    const isbn = success.codeResult.code;
    // ISBNをEmitで返却する
    this.onSuccess(isbn);
  }
  
  // ****************************************************
  // * emit
  // ****************************************************
  @Emit("cancel")
  private onClickCancel() {}

  @Emit("success")
  private onSuccess(code) {
    return code;
  }
}
</script>

<style lang="scss">
.camera-area {
  overflow: hidden;
  height: 300px;
  width: 300px;
  
  /**
   * 指定したDIV配下にvideoとcanvasが追加される
   * 4:3になるため、margin-topで調整
   */
  video, canvas {
    margin-top: -50px;
    width: 300px;
    height: 400px;
  }
}
</style>
```

ページ側でpropのactiveを切り替えて、モーダルの表示をすればOK。簡単(*´ω｀*)

videoやcanvasなどは、Quaggaが生成するよう。

![スクリーンショット 2019-09-18 10.03.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fdf8635d-b5aa-70cd-c56c-5c1970e703b2.png)


---

## はまったポイント...

PC上で開発しているときは割とサクッとできたんですが、
テストでいろんなところにハマリポイントが..

### 1. httpsじゃないとカメラを取得できない

ローカルで実装できたので、nuxt.config.tsを以下のようにして、

```typescript
const config: NuxtConfiguration = {
  server: {
    port: 3000,
    host: "0.0.0.0" 
  }
};
```

Androidスマホから試してみたら、こんなエラーが...
> Error: getUserMedia is not defined

以下の記事を見ると、Chromeではlocalhostかhttpsじゃないと動かないらしい...
参考: [ChromeではgetUserMediaがHTTPS経由でないと動かなくなっていた – 打つか投げるか](https://blog.ko31.com/201606/perari-to-https/)


以下のようなローカルで起動したNuxt.jsをhttps化する方法もあるけど、
ステージング環境のFirebaseプロジェクトを用意して対応。。
参考: [Nuxt.jsでlocalhostをSSL化する方法 - Qiita](https://qiita.com/yoshinbo/items/5838f43cbbd75e396c35)


### 2. QuaggaJSで表示されてないHTML要素を指定するとエラー

初期化処理の以下の部分で`#camera-area`を指定してるが、
最初はmounted内で行っていた。


```javascript
private initQuagga() {
  // Quaggaの設定項目
  const config = {
    // カメラの映像の設定
    inputStream: {
      // カメラ映像を表示するHTML要素の設定
      target: document.querySelector("#camera-area"),
    },
  }
}
```

すると、こんなエラーが...

> error TypeError: Cannot read property 'setAttribute' of undefined
> at Object.o.createLiveStream

よく見直してみると、`v-if="active"`で表示を切り替えているので、
`document.querySelector("#camera-area")`で取得できてなかった...


### 3. size/width/heightを指定してもいい感じにならない

公式サイトの[Example](https://serratus.github.io/quaggaJS/)に以下のサンプルがあったので試してみたところ、
あまりいい感じではなかった...

```javascript
inputStream: {
    size: 800  // restrict input-size to be 800px in width (long-side)
}
```

`#camera-area`のwidthを300にしていたため、
`size: 300`で指定してみたところ、なかなか検出されず。。。
この設定を外すとすぐに検出できた( ﾟдﾟ)!

GitHubにあるexampleの[quaggaJS/live_w_locator.html](https://github.com/serratus/quaggaJS/blob/master/example/live_w_locator.html)を見てみると、
[CSS](https://github.com/serratus/quaggaJS/blob/master/example/css/styles.css)で画面サイズとかを設定しているようなので、そちらを参考にした。

以下のように、constraintsでwidthとheightを指定できそうだったが、
[MediaDevices.getUserMedia()](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia)で利用する[MediaStreamConstraints](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API/Constraints)の解像度の制約のよう...

```javascript
inputStream: {
  constraints: {
    width: 640,
    height: 480,
  }
}
```

日本語の情報が少ないので、トライ&エラーで確認...

### 4. iOSのPWAではカメラにアクセスできない

Androidもうまくいき、iOSのブラウザでもできるようになったが、
iOSのPWAで確認したところ、またこのエラーが...

> Error: getUserMedia is not defined

以下の記事を見てみると、iOSのPWAでは制限があるらしい...
> iOSのPWAでは表示モードがstandaloneの場合にカメラを開くことができない。

参考: [PWAでカメラを使うためiOSとAndroidで異なるmanifestを読み込む - Qiita](https://qiita.com/1pp0/items/a3f0ea14910e25f49878)


なんてこった。。ほんとうに残念である。。

仕方がないので[参考記事](https://qiita.com/1pp0/items/a3f0ea14910e25f49878)にあるように、
Nuxt.jsでもmanifest.jsonを2つ用意してUAで切り替えるように変更。。

#### 【暫定対処】Nuxt.jsでOSに応じてmanifest.jsonを切り替える

方法としてはこんな感じ。

1. iOS用のmanifest.json(manifest_ios.json)を用意する
2. UA応じたmanifest.jsonの`<link>`を生成するpluginを作成
3. 作ったプラグインをnuxt.config.tsに設定

##### 1. iOS用のmanifest.jsonの作成

参考記事にある通り、manifest.jsonをコピーして、
`"display": "standalone"`を`"display": "browser"`にしただけの
manifest_ios.jsonを作成する。

```diff
--- static/manifest.json        2019-09-17 21:29:18.000000000 +0900
+++ static/manifest_ios.json    2019-09-18 00:21:54.000000000 +0900
@@ -3,7 +3,7 @@
   "short_name": "積読ハウマッチ",
   "description": "買った本を読まずに積んでおく「積読」、全部でいくらか知っていますか？「積読ハウマッチ」は積んである本の総額がわかる書籍管理サービスです。",
   "start_url": "/",
-  "display": "standalone",
+  "display": "browser",
   "background_color": "#ffffff",
   "theme_color": "#776f59",
   "orientation": "portrait-primary",
```

##### 2. UA応じた`<link>`を生成するpluginを作成

以下の`plugins/pwa-setup.ts`を作成。

```typescript
const userAgent = navigator.userAgent.toLowerCase();
const iOS = userAgent.indexOf("iphone") > 0 || userAgent.indexOf("ipad") > 0;

// manifestのlinkタグを生成
function setManifest(path) {
  const manifest = document.createElement("link");
  manifest.rel = "manifest";
  manifest.href = path;
  document.head.appendChild(manifest);
}

setManifest(iOS ? "/manifest_ios.json" : "/manifest.json");
```

##### 3. nuxt.config.tsの設定

作ったプラグインをnuxt.config.tsに設定。

```typescript
const config: NuxtConfiguration = {
  plugins: [
    { src: "~/plugins/pwa-setup", ssr: false }
  ]
}
```

これでNuxt.jsでもUAに応じて切り替えができるように(*´ω｀*)
ただ、iOSのPWAがただのブラウザショートカットに...

---

## おまけ: その他、もろもろ

上記以外のQuaggaJSの使い方や開発で役立ったことを五月雨に。

#### A) Android実機のデバッグはDevToolsでできる

Android実機でChromeを開いて確認していた時、コンソールログがみたいなと思ったら、
DevToolsでリモートデバッグできた!! DevTollsすごい(*´ω｀*)

- [Android 端末のリモート デバッグを行う  |  Tools for Web Developers  |  Google Developers](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/?hl=ja)
- [Android の Chrome で開発者ツールを使う方法 - Qiita](https://qiita.com/hojishi/items/12b726f8b02ef3d713e4)

#### バーコードリーダっぽく四角い枠をつける

これ。

<img width="325" alt="スクリーンショット 2019-09-18 9.49.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/1e030b9c-cdff-41ef-5a90-10e50c03f3ad.png">


`#camera-area`内にdivをもたせて表示させている

```html
<template>
  <div class="dialog modal is-active" v-if="active">
    <div class="modal-background"></div>
    <div class="modal-content">
      <div id="camera-area" class="camera-area">
        <!-- 青い四角のDIV -->
        <div class="detect-area"></div>
      </div>
    </div>
    <button class="modal-close is-large" aria-label="close" @click.prevent.stop="onClickCancel"></button>
  </div>
</template>
<style lang="scss">
.camera-area {
  margin: auto;
  overflow: hidden;
  height: 300px;
  width: 300px;
  /* relativeに設定 */
  position: relative;
  
  video, canvas {
    margin-top: -50px;
    width: 300px;
    height: 400px;
  }
  
  /* 検出範囲のサイズに合わせ枠線を引く */
  .detect-area {
    position: absolute;
    top: 30%;
    bottom: 30%;
    left: 10%;
    right: 10%;

    border: 2px solid #0000ff;
  }
}
</style>
```

#### 解析中っぽく緑の枠を出す

読み取っている箇所を表示できるよう解析中のコールバックがある。
[こちらの記事](https://kuroeveryday.blogspot.com/2017/12/barcode-scaner-with-quaggajs.html)を参考に、解析中の状況を表示する。

```html
<script lang="ts">
@Component
export default class ModalBarcodeReader extends Vue {
  private initQuagga() {
    this.Quagga = require("quagga");
    // 解析中に呼び出される処理を設定
    this.Quagga.onProcessed(this.onProcessed);
    this.Quagga.onDetected(this.onDetected);
    // ...
  }
  
  /**
   * バーコード読み取り中時の処理
   */
  private onProcessed(data) {
    const ctx = this.Quagga.canvas.ctx.overlay;
    const canvas = this.Quagga.canvas.dom.overlay;

    if (!data) return;

    // 認識したバーコードを緑の枠で囲む
    if (data.boxes) {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      const hasNotRead = box => box !== data.box;
      data.boxes.filter(hasNotRead).forEach(box => {
        this.Quagga.ImageDebug.drawPath(box, { x: 0, y: 1 }, ctx, { color: "green", lineWidth: 2 });
      });
    }

    // 読み取ったバーコードを青の枠で囲む
    if (data.box) {
      this.Quagga.ImageDebug.drawPath(data.box, { x: 0, y: 1 }, ctx, { color: "blue", lineWidth: 2 });
    }

    // 読み取ったバーコードに赤い線を引く
    if (data.codeResult && data.codeResult.code) {
      this.Quagga.ImageDebug.drawPath(data.line, { x: "x", y: "y" }, ctx, { color: "red", lineWidth: 3 });
    }
  }
}
</script>

<style lang="scss">
.camera-area {
  /* ... */

  /* オーバーレイ */
  .drawingBuffer {
    position: absolute;
    left: 0;
  }
}
</style>
```

以下のサンプル画像のように、
読み取ったバーコードの枠を囲ったり、線を引いたりもできる(*´ω｀*)

![スクリーンショット 2019-09-18 10.13.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/8bfadb8e-0cdd-bc42-f673-762e64246810.png)

## おわりに

Nuxt.jsアプリでも[QuaggaJS](https://serratus.github.io/quaggaJS/)で簡単にバーコードリーダが作れる(*´ω｀*)

ただ、getUserMedia()にも、iOSのPWAには罠が。。

## こんなのつくってます!!

バーコードリーダ機能もある積読用の読書管理アプリ
『積読ハウマッチ』をリリースしました！
[積読ハウマッチ](https://tsundoku.site)は、Nuxt.js+Firebaseで開発してます!

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/>

もしよかったら、遊んでみてくださいヽ(=´▽`=)ﾉ

要望・感想・アドバイスなどあれば、
公式アカウント([@MemoryLoverz](https://twitter.com/MemoryLoverz))や開発者([@kira_puka](https://twitter.com/kira_puka))まで



## 参考にしたサイト
- [QuaggaJS, an advanced barcode-reader written in JavaScript](https://serratus.github.io/quaggaJS/)
- [Quagga Sandbox](https://serratus.github.io/quaggaJS/v1.0.0-beta.1/examples/sandbox/)
- [quaggaJS/live_w_locator.html at master · serratus/quaggaJS](https://github.com/serratus/quaggaJS/blob/master/example/live_w_locator.html)
- [QuaggaJSを使ったバーコードリーダ実装 | WatchContents](https://watchcontents.com/quaggajs-barcode-reader/)
- [Quagga.jsを使ってブラウザ上からJavaScriptでバーコードを読み取る | Black Everyday Company](https://kuroeveryday.blogspot.com/2017/12/barcode-scaner-with-quaggajs.html)
- [PWAでカメラを使うためiOSとAndroidで異なるmanifestを読み込む - Qiita](https://qiita.com/1pp0/items/a3f0ea14910e25f49878)
- [MediaDevices.getUserMedia() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia)
- [MediaDevices.enumerateDevices() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/enumerateDevices)
- [ブラウザからメディアデバイスを操る - getUserMedia()の基本 | CodeGrid](https://app.codegrid.net/entry/2017-get-user-media-1)
- [ブラウザでバーコード/QRコードリーダー【実装・カスタマイズ編】 - Qiita](https://qiita.com/mm_sys/items/6e5e927ef75ab82fa8d3#webcodecamjs%E3%82%92%E5%B1%95%E9%96%8B)
- [WebサイトからスマートフォンのカメラでQRコードを読み取る – JavaScript (Android、iOS、PC対応) – flow of water](http://fow.minim.ne.jp/?p=383)
- [バーコード読み取り | KuJunDev](http://kujundev.wp.xdomain.jp/?p=437)
- [Javascriptのバーコードライブラリ - Qiita](https://qiita.com/k-murayama/items/eddcc974bd0dd3a214ed)
- [PWAでカメラを使うためiOSとAndroidで異なるmanifestを読み込む - Qiita](https://qiita.com/1pp0/items/a3f0ea14910e25f49878)