Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

利用したのはQuaggaJS。簡単に使えて便利(´ω`)

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

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

作ったのはこんな感じ

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

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

スクリーンショット 2019-09-18 8.18.32.png


QuaggaJSを使ってみる

使い方はこんな感じ。

まずはインストール

$ npm i -S quagga

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

CSSにはBulmaを利用してます。

<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


はまったポイント...

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

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

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

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

Androidスマホから試してみたら、こんなエラーが...

Error: getUserMedia is not defined

以下の記事を見ると、Chromeではlocalhostかhttpsじゃないと動かないらしい...
参考: ChromeではgetUserMediaがHTTPS経由でないと動かなくなっていた – 打つか投げるか

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

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

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

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に以下のサンプルがあったので試してみたところ、
あまりいい感じではなかった...

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を見てみると、
CSSで画面サイズとかを設定しているようなので、そちらを参考にした。

以下のように、constraintsでwidthとheightを指定できそうだったが、
MediaDevices.getUserMedia()で利用するMediaStreamConstraintsの解像度の制約のよう...

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

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

仕方がないので参考記事にあるように、
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を作成する。

--- 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を作成。

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に設定。

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

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


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

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

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

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

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

これ。

スクリーンショット 2019-09-18 9.49.49.png

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

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

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

読み取っている箇所を表示できるよう解析中のコールバックがある。
こちらの記事を参考に、解析中の状況を表示する。

<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

おわりに

Nuxt.jsアプリでもQuaggaJSで簡単にバーコードリーダが作れる(´ω`)

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

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

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

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

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

参考にしたサイト

kira_puka
フリーのエンジニア / 今はNuxt.jsが多め / いつかFlutterをやりたい 受託開発をしながら、アプリ・Webサービス・ゲームを個人開発 Kotlin/Python/Swift/Unity/Java/Haskell/DDD
https://memory-lovers.com
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away