Quaggajsでバーコードリーダー作ったらカメラ周りでハマった話

在庫管理に利用されているバーコード（JANコード）ってご存知ですよね？[Quaggajs](https://serratus.github.io/quaggaJS/)というライブラリでそのリーダーを簡単に実装できるらしいです。実際にスマホのWebブラウザで動くアプリを作ってみてハマったポイントをメモとして残しておきます。ライブラリの詳細は上記リンク先を参照ください。

#とりあえず作ってみる
とりあえずインストール。

````
$ npm install quagga
````

LiveStreamで起動する場合、次のコードで起動できます。

````js
import Quagga from "quagga";
// LiveStreamを表示するvideoDOMを挿入したい親DOM
const camera = document.getElementById("camera");
// QuaggaJSをLiveStreamで起動する
start(camera);
function start(camera) {
  // QuaggaJs初期化のConfig
  const config = {
    inputStream: {
      type: "LiveStream",
      target: camera,
      constraints: {
        // MediaStreamConstraintsを参照
        audio: false,
        video: {
          facingMode: "environment",
        },
      },
    },
    decoder: {
      // バーコードのデコーダー
      readers: ["ean_reader"], // EANコードはJANコードの海外での名称
    },
  };
  // QuaggaJSを起動
  Quagga.init(
    config,
    // カメラの起動後に実行
    async (err) => {
      if (err) {
        console.log(err);
      } else {
        // バーコード読み取りをスタート
        Quagga.start();
      }
    }
  );
  // コード読み取り完了時の処理
  Quagga.onDetected((result) => {
    // 読み取れたコードをコンソールに表示
    console.log(result.codeResult.code);
    // カメラを停止
    Quagga.stop();
  });
}
````
このコードでWindows, Mac, iPhoneなどでは動きました。（localhost以外の場合はHTTPS通信に注意！）
少し古いライブラリなのでちょっとドキュメント読んだりするのに手間取るかもしれませんが、概ねこんな感じで実装できます。

#ハマったポイント
###HTTPSじゃないと動かない
ローカル開発しているときにこれでハマりました。スマホで起動したい時には、HTTPS通信じゃないとそもそもカメラが起動できないのを失念してました。https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia
###マルチカメラのレンズ選択
こいつが曲者で、一部のAndroidスマホではデフォルトのレンズが望遠レンズになるらしく、カメラ起動の際にMediaDeviceを選んでやる必要があります。QuaggaJsではカメラ選択はサポートされてないっぽい（やり方あったら教えてください！）ので、カメラは自分で起動する必要があります。この場合は次のコードで起動できます。

````js
import Quagga from "quagga";
// LiveStreamを表示するvideoDOMを挿入したい親DOM
const camera = document.getElementById("camera");
const video = document.createElement("video");
video.autoplay = true;
video.playsInline = true;
camera.appendChild(video);
// カメラを起動しスナップショットをQuaggaJSで読み取る
multiCameraStart(camera, video);
async function multiCameraStart(camera, video) {
  // 使用したいカメラを選択
  const devices = await navigator.mediaDevices.enumerateDevices();
  // カメラを選ぶ。Androidでは"camera2 0"のラベルがついているものが標準らしい。
  const videoDevices = devices.filter(
    (v) =>
      v.kind == "videoinput" && v.label.indexOf("camera2 0, facing back") >= 0
  );
  if (videoDevices.length == 0) {
    // 該当のカメラがないならQuaggaJsをLivestreamで起動
    start(camera);
  } else {
    // 該当のカメラを起動
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        deviceId: videoDevices[0].deviceId,
      },
    });
    // videoにstreamを入力
    video.srcObject = stream;
    // ビデオトラックを取得
    const track = stream.getVideoTracks()[0];
    // 読み取り用canvasの作成
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const trackSettings = track.getSettings();
    canvas.width = trackSettings.width;
    canvas.height = trackSettings.height;
    // 0.3秒ごとにcanvasにスナップショットをとり、QuaggaJsでバーコードを読み取る
    let interval = window.setInterval(() => {
      // canvasにスナップショットを作成
      ctx.drawImage(video, 0, 0, trackSettings.width, trackSettings.height);
      // canvasの画像をblobに変換
      canvas.toBlob(
        (blob) => {
          if (blob) {
            // blobをurlに変換
            const reader = new FileReader();
            reader.readAsDataURL(blob);
            reader.onload = () => {
              // quaggaを起動
              const config = {
                decoder: {
                  readers: ["ean_reader"],
                  multiple: false, // 精度向上のため複数入力は解除
                },
                locate: true, // 精度向上のためlocateオプションを設定
                src: reader.result,
                singleChannel: false,
              };
              // QuaggaJsを画像読み取りで起動
              Quagga.decodeSingle(config);
            };
          }
        },
        "image/jpeg",
        1
      );
    }, 300);

    // 読み取りに成功した場合の設定
    let code;
    let count = 0;
    Quagga.onDetected((result) => {
      // 3連続で同じコードを得られるまで検知
      if (code == result.codeResult.code) {
        count++;
      } else {
        // 前回のコードと違った場合
        count = 0;
        code = result.codeResult.code;
      }
      // 3連続で同じコードを得られた場合はカメラを停止
      if (count >= 3) {
        // コンソールにコードを表示
        console.log(code);
        // 画像の読み取りを停止
        window.clearInterval(interval);
        // カメラを停止
        const tracks = stream.getVideoTracks();
        for (let i = 0; i < tracks.length; i++) {
          tracks[i].stop();
        }
      }
    });
  }
}
````
順番に解説していきます。
####カメラデバイスの起動
````js
// 使用したいカメラを選択
const devices = await navigator.mediaDevices.enumerateDevices();
// カメラを選ぶ。Androidでは"camera2 0"のラベルがついているものが標準らしい。
const videoDevices = devices.filter(
  (v) =>
    v.kind == "videoinput" && v.label.indexOf("camera2 0, facing back") >= 0
);
````
`mediaDevices.enumerateDevices`でメディアデバイスを取得し、その中から適切なデバイスを選んでいます。Androidで調べてみるとラベルに`camera2 0`が含まれているものが標準的なレンズみたいだったので、これを選んで起動しています。`devices`のカメラデバイスをそれぞれ一度起動してみるのが良いかもしれません。なお、**ブラウザのカメラ使用を許可していないと`devices`のラベルが正確に読みとられないので、事前にカメラ使用を許可させる必要があることにも注意してください！**

````js
// 該当のカメラを起動
const stream = await navigator.mediaDevices.getUserMedia({
  audio: false,
  video: {
    deviceId: videoDevices[0].deviceId,
  },
});
// videoにstreamを入力
video.srcObject = stream;
````
先ほど選んだカメラデバイスのデバイスIDを設定して起動します。なお、全体のコードでは該当のビデオデバイスが取得できなかった場合は仕方なくQuaggaJsをLiveStreamで起動しています。

####スナップショットを作成しBlobURLに変換してQuaggaJsでデコード
````js
// ビデオトラックを取得
const track = stream.getVideoTracks()[0];
// 読み取り用canvasの作成
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const trackSettings = track.getSettings();
canvas.width = trackSettings.width;
canvas.height = trackSettings.height;
````
ここでは画像を描画するcanvas要素を作成しています。

````js
// 0.3秒ごとにcanvasにスナップショットをとり、QuaggaJsでバーコードを読み取る
let interval = window.setInterval(() => {
  // canvasにスナップショットを作成
  ctx.drawImage(video, 0, 0, trackSettings.width, trackSettings.height);
  // canvasの画像をblobに変換
  canvas.toBlob(
    (blob) => {
      if (blob) {
        // blobをurlに変換
        const reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onload = () => {
          // quaggaを起動
          const config = {
            decoder: {
              readers: ["ean_reader"],
              multiple: false, // 精度向上のため複数入力は解除
            },
            locate: true, // 精度向上のためlocateオプションを設定
            src: reader.result,
            singleChannel: false,
          };
          // QuaggaJsを画像読み取りで起動
          Quagga.decodeSingle(config);
        };
      }
    },
    "image/jpeg",
    1
  );
}, 300);
````
ここでは適当なインターバルで先ほどのcanvas要素にvideo要素のスナップショットを描画しています。さらに、描画した画像データをblobに変換してQuaggaJsに渡し、画像解析してデコードしてもらいます。
####デコードが完了した場合の処理
````js
  // 読み取りに成功した場合の設定
  let code;
  let count = 0;
  Quagga.onDetected((result) => {
    // 3連続で同じコードを得られるまで検知
    if (code == result.codeResult.code) {
      count++;
    } else {
      // 前回のコードと違った場合
      count = 0;
      code = result.codeResult.code;
    }
    // 3連続で同じコードを得られた場合はカメラを停止
    if (count >= 3) {
      // コンソールにコードを表示
      console.log(code);
      // 画像の読み取りを停止
      window.clearInterval(interval);
      // カメラを停止
      const tracks = stream.getVideoTracks();
      for (let i = 0; i < tracks.length; i++) {
        tracks[i].stop();
      }
    }
  });
}
````
読み取り精度に若干問題があったので、ここでは３連続で同じコードが得られた場合のみ成功としています。カメラを終了する際には、`stream`を直接触ってカメラを停止しています。

#まとめ
ライブラリは便利ですが、[mozilla](https://developer.mozilla.org/)などのドキュメントを正しく理解するのが大切だと改めて気付かされました！
##サンプルアプリ
サンプルアプリを作ってみたので、よかったら使ってみてください。[バーコードリーダーサンプルアプリ](https://github.com/mininjin/BarcoderReaderSample)