複数のQRコードを連続して読み取れる LIFF アプリを作ってみた

2022.02.22

こんにちは!LINE事業部のたにもんです!

先日、以下の記事でQRコードを読み取る LIFF アプリを作成しました。

LIFF アプリでカメラを起動してQRコードを読み取る機能を作って動かしてみた

この記事で紹介した LIFF アプリでは、QRコードを1つ読み取ると「コードリーダー」が閉じてしまい、複数のQRコードを読み取りたい場合に不便でした。

そこで、複数のQRコードを効率的に読み取れる LIFF アプリを作成しました! 実際に動かした様子は次のとおりです。

以下の4つのQRコードを連続して読み取れていることが分かるかと思います!

  • https://www.google.com/
  • https://www.amazon.co.jp/
  • https://www.facebook.com/
  • https://www.apple.com/

今回開発したソースコードはこちらのリポジトリで公開しているので、実装が気になる方は参考にしてください!

環境

  • node: 16.14.0
  • npm: 8.3.1
  • AWS CDK: 2.12.0 (build c9786db)
  • iOS: 15.2.1
  • LINE: 12.1.0

全体像

今回は以下のような QRCodeScanner コンポーネントを作成することで、複数のQRコードを連続で読み取れるようにしました。 このコンポーネントでどのようにQRコードを読み取っているのかを説明していきます。

// app/src/components/QRCodeScanner.tsx

import React, { useEffect, useRef, useState } from 'react';
import jsQR from 'jsqr';

const videoWidth: number = 500;
const videoHeight: number = 500;
const videoFrameRate: number = 5;

const constraints: MediaStreamConstraints = {
  audio: false,
  video: {
    width: videoWidth,
    height: videoHeight,
    frameRate: {
      max: videoFrameRate,
    },
    facingMode: {
      exact: 'environment',
    },
  },
};

const QRCodeScanner: React.VFC = () => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const intervalRef = useRef<number>();
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [isContinue, setIsContinue] = useState(false);
  const [qrCodeData, setQrCodeData] = useState<string[]>([]);

  useEffect(() => {
    const openCamera = async () => {
      const video = videoRef.current;
      if (video) {
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        video.srcObject = stream;
      }
    };
    openCamera();
  }, []);

  useEffect(() => {
    if (!isContinue) {
      return;
    }

    const decodeQRCode = () => {
      const context = canvasRef?.current?.getContext('2d');
      const video = videoRef?.current;

      if (!context || !video) {
        return;
      }

      context.drawImage(video, 0, 0, videoWidth, videoHeight);
      const imageData = context.getImageData(0, 0, videoWidth, videoHeight);
      const code = jsQR(imageData.data, videoWidth, videoHeight);

      return code?.data;
    };

    const intervalId = window.setInterval(() => {
      const decodedValue = decodeQRCode();

      if (!decodedValue || qrCodeData.includes(decodedValue)) {
        return;
      }

      setQrCodeData([...qrCodeData, decodedValue]);
    }, 1_000 / videoFrameRate);
    intervalRef.current = intervalId;

    return () => {
      clearInterval(intervalRef.current);
    };
  }, [isContinue, qrCodeData]);

  const handleStart = () => {
    setIsContinue(true);
  };

  const handleStop = () => {
    setIsContinue(false);
  };

  return (
    <div>
      <p>QR Code Scanner</p>
      <div style={{ display: 'grid' }}>
        <div>
          <video
            autoPlay
            playsInline={true}
            ref={videoRef}
            style={{ width: '100%' }}
          >
            <canvas width={videoWidth} height={videoHeight} ref={canvasRef} />
          </video>
        </div>
        <div>
          <p>{qrCodeData.join('\n')}</p>
        </div>
        <div>
          <button onClick={handleStart}>Start Scan</button>
          <button onClick={handleStop}>Stop Scan</button>
        </div>
      </div>
    </div>
  );
};

export default QRCodeScanner;

カメラの起動

以下の部分では getUserMedia メソッドを用いてカメラを起動します。 この処理は useEffect の依存リストに空配列を指定することで、コンポーネントの初回レンダリング時のみ実行されるようにしています。

  useEffect(() => {
    const openCamera = async () => {
      const video = videoRef.current;
      if (video) {
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        video.srcObject = stream;
      }
    };
    openCamera();
  }, []);

QRコードの読み取りループ

以下の部分では、setInterval メソッドを用いて一定時間ごと(今回は200ミリ秒ごと)にQRコードの読み取りを行い、qrCodeData 配列を更新しています。 QRコードの読み取りには、後述する decodeQRCode 関数を利用しています。 なお、QRコードを読み取るか否かを isContinue 変数で制御しています。

  useEffect(() => {
    if (!isContinue) {
      return;
    }

    const decodeQRCode = () => { /* 略 */ };

    const intervalId = window.setInterval(() => {
      const decodedValue = decodeQRCode();

      if (!decodedValue || qrCodeData.includes(decodedValue)) {
        return;
      }

      setQrCodeData([...qrCodeData, decodedValue]);
    }, 1_000 / videoFrameRate);
    intervalRef.current = intervalId;

    return () => {
      clearInterval(intervalRef.current);
    };
  }, [isContinue, qrCodeData]);

decodeQRCode 関数

前回の記事 では、LIFF SDK v2 で提供されている scanCodeV2 メソッドを利用してQRコードを読み取りました。 この scanCodeV2 メソッドの仕様により、QRコードを1つ読み取るとコードリーダーが閉じてしまっていたので、今回は jsQR というライブラリを利用してQRコード読み取り処理を自前で書きました。 ちなみに、jsQR は scanCodeV2 メソッドの内部でも利用されています(2022年2月22日現在)。

    const decodeQRCode = () => {
      const context = canvasRef?.current?.getContext('2d');
      const video = videoRef?.current;

      if (!context || !video) {
        return;
      }

      context.drawImage(video, 0, 0, videoWidth, videoHeight);
      const imageData = context.getImageData(0, 0, videoWidth, videoHeight);
      const code = jsQR(imageData.data, videoWidth, videoHeight);

      return code?.data;
    };

QRコード読み取りの開始・終了

isContinue 変数に true / false を設定する関数をそれぞれ作成し、これらを Start Scan / Stop Scan というボタンの Click イベントハンドラとして登録することで、QRコード読み取りの開始・終了を制御しています。

  const handleStart = () => {
    setIsContinue(true);
  };

  const handleStop = () => {
    setIsContinue(false);
  };

参考

QRコードは株式会社デンソーウェーブの登録商標です。