💮

【iOS対応】LIFFでQRコードリーダーを利用する

2021/02/15に公開

LIFF(LINEミニアプリ)とQRコードリーダーの相性は良さそうなものですが、技術的制約でiOSでは利用できません
しかしながら昨年末飛び込んできたこのニュース。

https://bugs.webkit.org/show_bug.cgi?id=208667

OK, let's use bug 220184 for custom schemes and getUserMedia.
I am closing this bug here since WKWebView applications can now have access to getUserMedia.

iOS14.3以降で、WKWebViewでgetUserMediaが動くようになったようです🙌
"getUserMediaが使えるなら自分で作ればいいじゃない!"ということで、やってみました。結論としてはバッチリ動きます✌️

完成イメージ

こんなものができます。せっかくなので連続読み取りに対応してみました。

プロジェクトの下準備

今回、Webアプリケーションとして動的なものは作りませんのでどの言語・どのフレームワークでも良いのですが、ここではPythonのFlaskを使った場合を例にしていきます。

Python側はデフォルトの設定でインスタンスを起動するのみです。ポート番号は5000にしていますが何でもOKです。

run.py
from flask import Flask
app = Flask(__name__)
app.run(port=5000)

続いてHTMLです。body要素の最後でQRコード読み取りライブラリのjsQRとLIFFのSDKを読み込むようにしています。Bootstrap5も読み込んでいますが、これも何でもOKです。

static/reader.html
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
        <title>QRCode Reader</title>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col">
                    <h1>QRコードリーダー</h1>
                    <div>
                        <video autoplay playsinline="true" style="width:100%; height: 100%; object-fit: fill;">
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <span id="decoded-value"></span>
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <button type="button" class="btn btn-primary" onclick="startReader();">Start</button>
                    <button type="button" class="btn" onclick="stopReader();">Stop</button>
                </div>
            </div>
        </div>

        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/jsqr@latest/dist/jsQR.min.js"></script>
	<script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
    </body>
</html>

ここで一旦動作確認してみます。

$ python run.py

アプリが起動したら http://localhost:5000/static/reader.html にアクセスしましょう。ボタンが2つ表示されていればOKです。

QRコード読み取り処理の追加

ここからが本番です。まずは読み取り処理をクラスとして実装します。body要素の最後に以下script要素を追加しましょう。

static/reader.html
<script>
    class QRCodeReader {
        constructor (videoElement) {
            this.video = videoElement;
            this.canvas = document.createElement("canvas");
            this.context = this.canvas.getContext("2d");
            this.decoderId;
            this.decodedValue;
        }

        // Start QR Code Reader
        start(onCompleted, stopOnComplete) {
            this.decoderId = null;
            this.decodedValue = null;

            // Start camera
            navigator.mediaDevices.getUserMedia({
                audio: false,
                video: {
                    width: 500,
                    height: 500,
                    frameRate: {
                        max: 5  // 5fps
                    },
                    facingMode : {
                        exact : "environment"
                    }
                }
            }).then((mediaStream) => {
                // Bind media stream with video
                this.video.srcObject = mediaStream;
            });

            // Start reading
            this.decoderId = setInterval(() => {
                this.decodeQRCode();
                if (this.decodedValue || this.decodedValue == 0) {
                    if (stopOnComplete) {
                        this.stop();
                    }
                    onCompleted(this.decodedValue);
                }
            }, 200);  // 5fps -> read every 200ms
        };

        // Stop QR Code Reader
        stop() {
            if (this.video.srcObject) {
                // Stop camera
                this.video.srcObject.getVideoTracks().forEach((track) => {
                    track.stop();
                });
                // Stop QR Code decoder
                clearInterval(this.decoderId);
            }
        };

        decodeQRCode() {
            const width = this.video.videoWidth;
            const height = this.video.videoHeight;
            if (width > 0 && height > 0) {
                // Get snapshot from video
                this.canvas.width = width;
                this.canvas.height = height;
                this.context.clearRect(0, 0, width, height);
                this.context.drawImage(this.video, 0, 0, width, height);
                const imageData = this.context.getImageData(0, 0, width, height);
                // Decode QR Code
                const code = jsQR(imageData.data, imageData.width, imageData.height);
                if (code) {
                    this.decodedValue = code.data;
                }
            }
        }
    }
</script>

続いてこのQRCodeReaderクラスを使ってカメラ映像を表示する処理や、読み取りの実行処理を追加していきます。先のクラス定義の下にコードを追加もしくは別script要素として追加しましょう。

static/reader.html
<script>
    // QRコードリーダーの初期化
    const reader = new QRCodeReader(document.querySelector("video"));

    // QRコードリーダー起動処理
    const startReader = () => {
        reader.start((value) => {
            // QRコード読み取り時に実行される処理(valueが読み取り値)
            document.querySelector("#decoded-value").textContent = value;
        }, false);  // false:読み取り後にリーダーを閉じない
    }

    // QRコードリーダー停止処理
    const stopReader = () => {
        reader.stop();
    }
</script>

ここまで追加したら、ページを再読み込みしてStart readerボタンを押してください。カメラが起動しますので、何かQRコードを読み取ってみましょう。読み取った文字列がボタンの上あたりに表示されたら成功です。表示されない場合はFlaskを再起動してから再読み込みし直してみてください。

なお読み取り頻度については200ms(毎秒5回)にしていますが、ここは負荷と反応速度とをトレードオフにしてチューニングできるところかと思います。

LIFF化する

最後に、このQRコードリーダーがLIFFとして動作するようにしていきます。

インターネットアクセスの提供

LIFFとして利用するには、HTTPSでインターネットからアクセスできる必要があります。開発PCを暫定的にインターネット公開するためにはngrokを利用しましょう。インストールしたら以下の通り実行します。ポート番号はFlaskアプリに合わせてください。

$ ngrok http 5000

起動したら、ngrokにより提供されたインターネットアドレス(xxxxxx部分は可変)を利用して、https://xxxxxx.ngrok.io/static/reader.html にアクセスしてみましょう。ローカルホストへのアクセスと同じものが表示されたら成功です。ngrokはこの記事の最後まで停止しないでください。

LIFFアプリの登録

LINE Developers ConsoleにてLIFFアプリを作成し、Endpoint URLに先のngrokのURL(reader.htmlまで)を登録します。LIFFアプリの登録手順については公式のドキュメントなどを参考にしてください。
なおMessaging API(BOT)のチャネルにLIFFを追加する手順で解説している記事が多く存在していますが、現在はLINEログインのチャンネルに追加するのが正しい手順です。

登録完了後、以下の情報を控えておきます。値は例です。

LIFFの初期化処理の追加

コンソールで控えておいたLIFF IDを使用してLIFFアプリを初期化する処理を追加します。最後に追加したscript要素の末尾に以下の処理を追加します。

static/reader.html
// LIFFの初期化
liff.init({
    liffId: "1234567890-ABCDEFGH"
})
.then(() => {
    accessToken = liff.getAccessToken();
    // AccessTokenの表示。これをサーバーサイドに渡してユーザー情報等を取得
    alert(accessToken);
})
.catch((err) => {
    alert(err);
});

処理を追加したら再読み込みして試してみましょう。PCのブラウザではアクセストークンが取得できないのでnullが、LINE上ではアクセストークンのJWTが表示されると思います。その後の読み取りは、冒頭の完成イメージの通り。

おわりに

このように、iOSでもLIFF(LINEミニアプリ)上でQRコードリーダーを使うことができるようになりました。もちろんiOS14.3以降が前提ですのでユースケース次第では注意が必要です。
取得した値はサンプルではただボタンの上に表示しているだけですが、LIFFアプリの中で如何様にも利用することができます。
SDK標準のQRコードリーダーよりも制約が少ないと思いますので、アイディア次第でいろんなことができるんじゃないかと思います👍

Discussion