WebサイトからスマートフォンのカメラでQRコードを読み取る – JavaScript (Android、iOS、PC対応)

投稿者: | 2018年5月3日

追記

少しだけ改良したコードの記事を投稿しています。合わせてお読みください。

概要

スマートフォン向けのWebサイトで、QRコードを読み取っていろいろやることになりました。
最初に実装したのが、JavaScriptの LazarSoft/jsqrcode というライブラリーを使用したものでした。このライブラリー自体は与えられた画像からQRコードを読み取るものです。カメラから画像を読み取るのは、フォームの input を使用しました。
以下のように input を記述することで、PCだとファイル選択となりますが、Androidでは画像を取得するいくつかの選択肢が表示されます。ユーザーがカメラを選択すれば、カメラで撮影した画像を QRコードとして読み取れます。iOSではカメラが起動するので、QRコードを撮影できます。

<form action="" method="POST" enctype="multipart/form-data">
    <input type="file" accept="image/*" capture="environment">
</form>

この方法の問題は、カメラの撮影ボタン(シャッターボタン)を押したときに、QRコードの読み取りを行うことです。撮影ボタンを押すときに位置がずれてしまったり、ピントが外れてしまったりして、うまく読み取れないことがあります。読み取りに失敗した場合、またカメラを起動して撮影を行う必要があります。

そこで、よくあるQRコード読み取りアプリのように、撮影ボタンを押さずにQRコードを読み取る機能を実装しようと思い、やっとそれを実現できました。

前提

  • iOSは、iOS11以上に対応しています。
  • iOSはSafariだけ動作します。
  • Androidでは、よほど古いものでなければ動くと思います。もしかしたら標準のブラウザでは動作しないかもしれません。
  • インターネット上で動作させるには、サイトのアドレスが https で始まっている必要があります。

必要なライブラリー

以下のライブラリーを使用しています。

QRコード解析

QRコードの情報を読み取るのに、以下のライブラリーを使用しています。
LazarSoft/jsqrcode https://github.com/LazarSoft/jsqrcode

ただし、実際に使用しているのは上記ライブラリーを使いやすく改変したものです。
改変したライブラリーは以下にあります。ページ末尾の最小化済みライブラリーをダウンロードしてください。
ネイティブアプリ不要!モバイルWebサイトにQRコードリーダーを実装する方法

カメラ情報取得

instascan https://github.com/schmich/instascan

このライブラリーは「カメラから画像取得して、QRコード解析する」という必要なものがすべてそろっていますが、iOSには対応していません。
今回はカメラ情報の取得だけに利用し、カメラからの画像取得には使用していません。
なお、上記のページにあるライブラリーでは、iOSのカメラ情報も正確に取得できませんでした。iOSに対応できるよう修正されたものが以下にあるので、以下のページから取得してください。
https://github.com/JoseCDB/instascan/tree/ios-rear-camera

また、instascan.js ファイルも必要になります(上記のライブラリーに含まれていない)。以下のページの中央付近に instascan.zip へのリンクがあるので、そこから取得してください。オリジナルのページにある instascan.min.js では動作しない可能性があります。
https://github.com/schmich/instascan/issues/105

コード例

index.html

<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, intial-scale=1, mininum-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no"
    />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
</head>

<body>
    <h1>QRコード読み取りデモ2</h1>
    <video id="video" width="300" height="200" autoplay playsinline></video>
    <img id="img" />
    <div style="display: none;">
        <canvas id="canvas"></canvas>
    </div>
    <div>
        <input type="text" id="qr" value="">
    </div>
    <div>
        <h2>カメラ一覧</h2>
        <div id="cameras">
        </div>
        <button type="button" id="changeCamera">カメラ切り替え</button>
    </div>
    <div>
        <h2>アクティブなカメラ</h2>
        <p id="active-camera"></p>
    </div>

    <script src="./js/instascan/instascan.js" charset="UTF-8"></script>
    <script src="./js/qr/qr_packed.js" charset="UTF-8"></script>
    <script type="text/javascript">
        var ios = /iPad|iPhone|iPod/.test(navigator.userAgent);
        var devices
        var activeIndex;
        var iosRear = false;

        // カメラ情報取得
        Instascan.Camera.getCameras()
            .then(function (cameras) {
                var list = document.getElementById('cameras');
                devices = cameras;
                var c = new Array();
                for (var i = 0; i < cameras.length; i++) {
                    c.push('カメラ名: ' + cameras[i].name);
                    c.push('カメラID: ' + cameras[i].id);
                }

                list.innerHTML = c.join('<br>');
                changeCamera(devices.length - 1);
            })
            .catch(function (err) {
                alert('カメラが見つかりません');
            });

        var video = document.getElementById('video');
        var localStream = null;

        function decodeImageFromBase64(data, callback) {
            qrcode.callback = callback;
            qrcode.decode(data);
        }

        document.getElementById('changeCamera').addEventListener('click', function () {
            let newIndex = activeIndex + 1;
            if (newIndex >= devices.length) {
                newIndex = 0;
            }
            changeCamera(newIndex);
        }, false);

        function decode() {
            if (localStream) {
                var canvas = document.getElementById('canvas');
                var ctx = canvas.getContext('2d');
                var img = document.getElementById('img');
                var h;
                var w;

                if (window.innerHeight > window.innerWidth) {
                    w = video.offsetHeight;
                    h = video.offsetWidth;
                } else {
                    w = video.offsetWidth;
                    h = video.offsetHeight;
                }

                canvas.setAttribute('width', w);
                canvas.setAttribute('height', h);
                ctx.drawImage(video, 0, 0, w, h);

                decodeImageFromBase64(canvas.toDataURL('image/png'), function (decodeInformation) {
                    var input = document.getElementById('qr');
                    if (!(decodeInformation instanceof Error)) {
                        input.value = decodeInformation;
                    }
                });
            }
        }

        function startReadQR() {
            setInterval('decode()', 500);
        }

        function changeCamera(index) {
            if (localStream) {
                localStream.getVideoTracks()[0].stop();
            }

            activeIndex = index;
            iosRear = !iosRear;
            var p = document.getElementById('active-camera');
            p.innerHTML = devices[activeIndex].name + '(' + devices[activeIndex].id + ')';
            setCamera();
        }

        function setCamera() {
            navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || windiow.navigator.mozGetUserMedia;
            window.URL = window.URL || window.webkitURL;

            var videoOptions;

            if (ios) {
                videoOptions = {
                    facingMode: {
                        exact: (iosRear) ? 'environment' : 'user'
                    }
                };
            } else {
                videoOptions = {
                    mandatory: {
                        sourceId: devices[activeIndex].id,
                        minWidth: 600,
                        maxWidth: 800,
                        minAspectRatio: 1.6
                    },
                    optional: []
                };
            }

            navigator.getUserMedia(
                {
                    audio: false,
                    video: videoOptions
                },
                function (stream) {
                    if (ios) {
                        video.srcObject = stream;
                    } else {
                        video.src = window.URL.createObjectURL(stream);
                    }
                    localStream = stream;
                },
                function (err) {

                }
            );

            startReadQR();
        }
    </script>

</body>

</html>

iOSではアクティブなカメラと、実際に有効になっているカメラが食い違っていることがありますが、これは(上記コードの)仕様です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)