WebサイトからカメラでQRコードを読み取る(完全版) – Android、iOS対応

投稿者: | 2018年10月17日

2019/8/10 追記
ここに掲載しているコードでは、iOS12や、Android 9 でカメラを起動できなくなっています。
iOS12等に対応したコードを、こちらの記事に掲載しました。

今までに2回、サイトからのQRコード読み取りについて書きましたが、今回が完全版となります。
どのあたりが完全版かというと、ビデオカメラでの読み取りに対応していない iOS10 以下の iPhone や、iPhone で Safari 以外のブラウザを使用した場合でも、QRコードを読み取れるようにしたものです。ビデオカメラでの読み取りに対応していない場合、通常の(静止画の)カメラで読み取ることになります。

このコードを実行するには、以下のライブラリが必要となります。
LazarSoft/jsqrcode https://github.com/LazarSoft/jsqrcode

実際に試してみようと思う方は、以下のサイトを参考にして、ライブラリを改変する必要があります(または、改変したライブラリをダウンロードします)。
ネイティブアプリ不要!モバイルWebサイトにQRコードリーダーを実装する方法

なお、実際に動作するサンプルを以下に置いています。
https://frostmoon.net/r-test/

注意事項

  • QRコードをビデオカメラで読み取ることのできるスマホは、Android、iOS11以上のiPhoneです。
  • iOSでビデオカメラから読み取ることができるブラウザは、Safariだけです。
  • PCでもビデオカメラで読み取ることができますが、対応するカメラが付いている必要があります。
  • カメラからの読み取りをインターネット上のサイトで行うには、サイトがSSL化(httpsから始まるURL)されている必要があります。

上記サンプルページのソースコード全文は、以下になります。

<!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" />

    <style>
        .qrbtn {
            background-color: red;
            color: #fff;
            margin: 3em auto;
            width: 100%;
            height: 2em;
            padding: 20px;
            border-radius: 9px;
        }
    </style>
</head>

<body>
    <h1>QRコード読み取りデモ</h1>

    <div id="video-input">
        <div style="text-align: center">
            <video id="video" style="width: 80%; height: auto;" autoplay playsinline></video>
        </div>
        <img id="img" />
        <div style="display: none">
            <canvas id="canvas"></canvas>
        </div>
        <div>
            <button type="button" id="changeCamera">前面/背面 切り替え</button>
        </div>
        <div style="margin-top: 3em">
            <p style="font-weight: bold; margin-bottom: 5px">アクティブなカメラ</p>
            <p id="active-camera" style="margin-top: 5px"></p>
        </div>
    </div>

    <div id="photo-input" style="display: none">
        <div style="text-align: center">
            <label for="input-qr" class="qrbtn">QRコードを読み取る</label>
            <input type="file" id="input-qr" accept="image/*" capture="environment" tabindex="-1" style="display: none" onchange="openQRCamera(this);">
        </div>
    </div>

    <div style="margin-top: 3em">
        <label for="qr">読み取ったQRコード<br></label>
        <input type="text" id="qr" value="" style="width:100%">
    </div>
    <div style="margin-top: 3em">
        <button type="button" id="toCamera">通常カメラでの読み取りに切り替え</button>
        <button type="button" id="toMovie" style="display: none">ビデオカメラでの読み取りに切り替え</button>
    </div>
</body>

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


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

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

        w = video.videoWidth;
        h = video.videoHeight;

        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 openQRCamera(node) {
    var reader = new FileReader();
    reader.onload = function() {
        node.value = '';
        qrcode.callback = function(res) {
            if (res instanceof Error) {
                alert('QRコードが見つかりませんでした。QRコードがカメラのフレーム内に収まるよう、再度撮影してください。');
            } else {
                var qr = document.getElementById('qr');
                qr.value = res;
            }
        };

        qrcode.decode(reader.result);
    };

    reader.readAsDataURL(node.files[0]);
}

window.onload = function() {
    var modeChange = function(mode) {
        if (mode === 'camera') {
            document.getElementById('video-input').style.display = 'none';
            document.getElementById('photo-input').style.display = 'block';
            document.getElementById('toCamera').style.display = 'none';
            document.getElementById('toMovie').style.display = 'block';
        } else {
            document.getElementById('video-input').style.display = 'block';
            document.getElementById('photo-input').style.display = 'none';
            document.getElementById('toCamera').style.display = 'block';
            document.getElementById('toMovie').style.display = 'none';
        }
    };

    if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
        modeChange('camera');
        return;
    }

    // カメラ情報取得
    navigator.mediaDevices.enumerateDevices()
        .then(function(cameras) {
            var cams = [];
            cameras.forEach(function(device) {
                if (device.kind === 'videoinput') {
                    cams.push({
                        'id': device.deviceId,
                        'name': device.label
                    });
                }
            });

            devices = cams;
            changeCamera(devices.length - 1);
        })
        .catch(function (err) {
            alert('カメラが見つかりません');
        });

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

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

    var changeCamera = function(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();
    };

    var setCamera = function() {
        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'
                },
                mandatory: {
                    sourceId: devices[activeIndex].id,
                    minWidth: 600,
                    maxWidth: 800,
                    minAspectRatio: 1.6
                },
                optional: []
            };
        } 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 {
                    // 2019-07-21修正
                    // video.src = window.URL.createObjectURL(stream);
                    // 以下のコードでないと動かなくなったようです
                    video.srcObject = stream;
                }
                localStream = stream;
            },
            function (err) {

            }
        );

        startReadQR();
    };

    document.getElementById('toCamera').addEventListener('click', function() {
        modeChange('camera');
    });

    document.getElementById('toMovie').addEventListener('click', function() {
        modeChange('video');
    });

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

</html>

WebサイトからカメラでQRコードを読み取る(完全版) – Android、iOS対応」への3件のフィードバック

  1. MN

    こんにちは。貴重なコードありがとうございます。
    iOS12(iPad)からビデオカメラ機能を確認したところ、
    サンプルサイト・コードを移植して自作したサイトともに、動かない状態です。
    navigator.mediaDevices.enumerateDevicesがSafariに対応していないようですが、関係あるでしょうか。

    返信
    1. kino_3240 投稿作成者

      iOS12のSafariの場合、video のサイズが640×480、または1280×720のどちらかしか動作しないとか、frameRate: 15 を指定しなければならないとかの情報が散見されます。
      おそらくは、navigator.mediaDevices.enumerateDevices を実行する前に、navigator.mediaDevices.getUserMedia で 、iOS12 の Safari だけの適切なオプションを指定する必要があると思います。
      今、いろいろと試してみているところではありますが、なかなかうまく動作してくれません。

      返信
      1. MN

        新バージョンでの仕様変更によるものなのですね。私も試しておりますが、なかなかうまくいきませんでした。
        既に試されてるとのことで、進展を祈ります。(私の方でも、参考にさせていただきます。)

        返信

コメントを残す

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

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