続・Webの技術だけで作るQRコードリーダー


この記事はPWA Advent Calendar 2020の16日目の記事です。
(だいぶ遅れてすみません)

以前に書いたWebの技術だけで作るQRコードリーダーの続編です。
以前の記事ではjsQRというライブラリを使用してQRコードの読み込みをしていましたが、ブラウザ標準のShape Detection APIというAPIで同じことが実現できそうだったので試してみました。

Shape Detection APIはEditor's draft(2020年12月23日現在)ですが、デスクトップ版とAndroid版のChrome ver83からはデフォルトで有効になっているようです。
※以前は chrome://frag からフラグを有効化しないと使えませんでした。
参考:https://www.chromestatus.com/feature/4757990523535360

Shape Detection APIは、QRコードのスキャンだけではなく以下の3つの事が可能です。

  • Barcode Detection(バーコードスキャン)
  • Face Detection(顔検出)
  • Text Detection(テキスト認識)

これらについては、以前にイベントで発表したので興味がある人は見てみてください。
スライド:ブラウザの新しいAPIで遊んでみる
動画:https://youtu.be/CS2tzUpYvQA?t=5253

Barcode Detectionの使い方

こんな感じでインスタンスを作成してimg要素を渡せばOKです。

// インスタンスの作成
const barcodeDetector = new BarcodeDetector()
// 画像要素を取得
const image = document.getElementById('image');
// 取得した画像要素をdetectに渡す
barcodeDetector.detect(image)
    .then(barcodes => {
      barcodes.forEach(barcode => console.log(barcode.rawValue))
    }
    .catch(err => {
      console.log(err)
    })

上手く検出できれば以下のようなオブジェクトで値を受け取ることができます。

座標なども取得できますが、実際に使うのは rawValue のところになると思います。
また、Barcode Detection APIはQRコードだけではなく、様々なバーコードのフォーマットに対応しています。詳しくはMDNなどで確認できます

作ったもの

動作しているGIFです。(上部の隙間はPCにスマホの画面を写しているためなので気にしないでください…)

実際に公開されていますので、AndroidでChrome(ver83以上)を使っている方はぜひ実機で試してみてください。
GitHubのdevelopブランチでソースコードも公開しています。
Barcode Detection APIに対応しているかでQRの読み込み方法を分岐させるようにしたのでmasterブランチにマージしました。

サイト:https://simple-qr.netlify.com/
GitHub:https://github.com/KanDai/simple-qr-reader

実装

JavaScript全体のソースコードは以下のようになっています。
HTMLやCSSはGitHubから確認ください。

2021/3/23追記:記事執筆後も機能追加などしているためGitHubのコードと変わっていますが、Barcode Detection APIの説明はこちらの方がわかりやすいのでコードは執筆時点のままです。

app.js
if (!navigator.mediaDevices) {
    document.querySelector('#js-unsupported').classList.add('is-show')
}

if (window.BarcodeDetector == undefined) {
    console.log('Barcode Detector is not supported by this browser.')
    document.querySelector('#js-unsupported').classList.add('is-show')
}

const video = document.querySelector('#js-video')

const checkImage = () => {
    const barcodeDetector = new BarcodeDetector()
    barcodeDetector
        .detect(video)
        .then((barcodes) => {
            if (barcodes.length > 0) {
                // QRコードの読み取りに成功したらモーダル開く
                for (let barcode of barcodes) {
                    openModal(barcode.rawValue)
                }
            } else {
                // QRコードが見つからなかったら再度実行
                setTimeout(() => {
                    checkImage()
                }, 200)
            }
        })
        .catch((e) => {
            console.error('Barcode Detection failed, boo.')
        })
}

navigator.mediaDevices
    .getUserMedia({
        audio: false,
        video: {
            facingMode: {
                exact: 'environment',
            },
        },
    })
    .then((stream) => {
        video.srcObject = stream
        video.onloadedmetadata = () => {
            video.play()
            checkImage()
        }
    })
    .catch((err) => {
        alert('Error!!')
    })

const openModal = (url) => {
    document.querySelector('#js-result').innerText = url
    document.querySelector('#js-link').setAttribute('href', url)
    document.querySelector('#js-modal').classList.add('is-show')
}

document.querySelector('#js-modal-close').addEventListener('click', () => {
    document.querySelector('#js-modal').classList.remove('is-show')
    checkImage()
})

前回から変わったところを中心に説明していきます。
全体像から確認したい方は前回の記事も合わせてご覧ください。

Canvasが不要になった

最初の説明でimg要素を渡すと書きましたが、実はvideo要素もそのまま渡せます。
なので、前回の記事で行っていたCanvasで画像化するいう実装が不要になりました。

検出結果は配列

複数の検出結果が得られる場合があるからだと思いますが、画像検出の結果は配列で受け取るため、結果をループで回して処理するような書き方になります。
また、検出できなかった場合もエラーではなく配列の length が0になります。

所感

Barcode Detectionの実装も難しくなく、全体的に以前の実装を少し変えるだけで簡単に実装することができました。
さらに、Canvasの処理が要らなくなったこともあって少し手軽になりました。

ブラウザ標準のAPIでこれができるのはとても良いですね。早く勧告になってほしい…