この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは!LINE事業部のたにもんです!
先日、以下の記事でQRコードを読み取る LIFF アプリを作成しました。
この記事で紹介した 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コードは株式会社デンソーウェーブの登録商標です。