本文へジャンプ

[HTML5 JavaScript QR WebRTC]JavaScriptでQRコードリーダーを作ってみる

Posted by kenta sugiyama

QRコード、便利ですよね。
いろいろなキャンペーンだったり、LINEの交換とかでも使われていてスマホ全盛期の現在手放せないものになっていますね。
キャンペーン応募だったら何かしらのネイティブアプリ、LINEとかだったらLINE内のリーダーを使えば良いですが、Webアプリ内にリーダー機能を持ちたいというものがあり、制作したのでその覚え書きです。

まず制作するにあたってJavaScriptでQRコードをスキャンするライブラリを選定します。

JavaScriptでQRコードをスキャンするライブラリ

などライブラリはいくつかありますが、今回はjsQRを採用しました。
jsQRはCommonJS形式で書かれていたり、テストコードがちゃんと書かれていたり、Typed Arrayが使われていたり、TypeScriptで記述されていたりと、モダンなライブラリとなっています。
jsqrcodeとinstascanについては開発が若干停滞している模様。

ちなみにreact-qr-readerはjsQRをReactコンポーネントとしてラップしたもののようです。
React使いとしては機会があれば使ってみたいと思います。

スマホの対応状況

JavaScriptでスマホのカメラを操作するわけですが、カメラ(マイク)とウェブブラウザ間でリアルタイム通信するのにWebRTC (Web Real Time Communication)を使います。
スマホでWebRTCが使えるようになったのは割と最近でiOS 11〜Android 7.0〜です。
今回使用するマイクとカメラに対するストリームアクセス MediaStream (別名 getUserMedia)についてはiOS 12〜です。

大まかなロジック

video にカメラをつなぎ、静止画を canvas にコピーしたあとに画像データとして取り出します。
この画像データをjsQRライブラリに渡しチェック。もし画像内にQRコードが存在している場合はその内容を表示します。
QRコード周りの面倒な処理はすべてライブラリがやってくれるので、それ以外の部分は自分で書きます。

実装

実装に入る前にまずgetUserMediaを使用するための前提としてAPIを呼び出すページはHTTPS通信である必要があります。
唯一例外として localhost のみ HTTP通信での利用が許可されています。

jsQRの使い方

<script src="jsQR.js"></script>
<script>
// Canvasの内容を画像として取り出す
const canvas = document.querySelector("canvas");
const ctx = canvas.getContetext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// jsQRでチェック
const code = jsQR(imageData.data, canvas.width, canvas.height);
if (code) {
  console.log("Found QR code", code);
}
</script>

戻り値はハッシュ(連想配列)として返ってきます。内容は以下の通り。

key説明
binaryData認識したQRコードの画像データ(バイナリ)
data認識したQRコード内にあるテキスト情報
location認識したQRコードが画像内のどこで見つかったかの座標情報

jsQRは非常にシンプルで、canvasの内容を画像として取り出したあとに、jsQR()に渡し戻り値をチェックするだけです。
存在しなければfalseになります。

カメラ設定など

// カメラ設定
const constraints = {
  audio: false,
  video: {
    width: canvas.width,
    height: canvas.height,
    facingMode: "environment"
  }
};

const drawLine = (ctx, pos, options={color:"blue", size:5})=>{
  // 線のスタイル設定
  ctx.strokeStyle = options.color;
  ctx.lineWidth   = options.size;

  // 線を描く
  ctx.beginPath();
  ctx.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
  ctx.lineTo(pos.topRightCorner.x, pos.topRightCorner.y);
  ctx.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y);
  ctx.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y);
  ctx.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
  ctx.stroke();
}

const checkPicture = ()=>{
  // 映像をCanvasへ
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

  // QRコード読取
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const code = jsQR(imageData.data, canvas.width, canvas.height);

  if( code ){
    // 存在する場合
    // 結果を表示
    console.log(code.data);  // 取得した文字列
    drawLine(ctx, code.location);       // 見つかった箇所に線を引く

    // video と canvas を入れ替え
    canvas.style.display = 'block';
    video.style.display = 'none';
    video.pause();
  } else{
    // 存在しない場合
    // 0.3秒後にもう一度チェックする
    setTimeout( () => {
      checkPicture();
    }, 300);
  }
}
// カメラを video と同期
navigator.mediaDevices.getUserMedia(constraints)
  .then( (stream) => {
    video.srcObject = stream;
    video.onloadedmetadata = (e) => {
      video.play();

      // QRコードのチェック開始
      checkPicture();
    };
  })
  .catch( (err) => {
    console.log(err.name + ": " + err.message);
  });

facingModeは "user" でインカメラ、"environment"でアウトカメラになります。
実際には code.data で取得した情報をもとにAPIと通信したりしますがそこは割愛。
要件的にiPadのみで良かったので問題なかったですが、iPhoneだと video が同期されずカメラ起動時の画で止まってしまうようでした。
今後改善されるのだろうか。。
iPhoneでもカメラ起動時にカメラのフレーム内にQRコードが収まっていればちゃんと認識されます。

まとめ

iPhoneだとちょっと期待通りには動きませんでしたが、iPadでは問題なく動作してくれました。
ちょっと前まで端末自体の機能だったりへのアクセスは結構面倒だった記憶がありましたが、だいぶお手軽になった印象です。
WebRTCとか知ってはいても通常のWebサイトだとなかなか活かせる場がないので良いアウトプットの機会になりました。

多方面にアンテナを張り、新しいことをインプットして皆さまからのご相談をお待ちしています!
ぜひMONSTER DIVEまでお声がけください!!

Recent Entries
MD EVENT REPORT
What's Hot?