画面イメージ(認識箇所を赤枠で囲います)
前書き
ブラウザでQRコードの認識できるのか?と調べたところ、jsqrというライブラリで簡単に出来ることがわかりました。
そのReact版です。
- 自作のjs-QR-readerをReactに移植
技術的な特徴
-
カメラ動画を<video>タグでブラウザに表示。タイマーで定期的に画像化してQR認識ライブラリjsqrに引き渡します。
-
DOM(videoタグ)を直接操作する必要があるため
useRef()
フックを利用 -
setInterval()
で定期的にQRコード認識ライブラリを呼び出します -
認識時にcallBack関数で通知し、停止するかそのまま継続するか選ぶことができます。
主な機能
- props:<video>タグのサイズ、停止/再開(
pause
)、認識時枠表示(showQRFrame
)を指定します
export type QRReaderProps = {
width?: number,
height?: number,
pause?: boolean,
showQRFrame?: boolean,
timerInterval?: number,
onRecognizeCode?: (e: QRCode) => boolean,
}
- 認識したタイミングで、利用側にcallback(
onRecognizeCode()
)を行います。
const onRecognizeCode = (e: QRCode) => {
setCode(e.data);
if (stopOnRecognize) {
setQRParam( e => { return {...e, pause: true}; });
}
}
QR認識コンポーネント概要
一部を抜粋し、説明用に書き換えてあります。
- <video>タグを配置し、ReactからDOMを直接操作するためuseRef()で参照します。
const video = useRef(null as HTMLVideoElement);
<video ref={video}></video>
実際のソースでは、styled-componentを利用しているため、<VideoArea>になっています。
- カメラの入力ストリームを取得し、videoタグに接続します。
const constraints = {
audio: false,
video: {
facingMode: 'environment',
width,
height,
}};
// useRef()を利用する場合、video.currentで参照します
const stream = await navigator.mediaDevices.getUserMedia(constraints);
video.current.srcObject = stream;
video.current.play();
- video動画を画像に変換するために必要なcanvas1を用意します。サイズはvideo側と合わせます。
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d');
- タイマー(
setInterval()
)を利用して定期的にQRコード認識処理を呼び出します。
timerId.current = setInterval(() => {
// video動画から画像に変換
context.drawImage(video.current, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
// QR認識ライブラリに引き渡す
const qr = jsqr(imageData.data, imageData.width, imageData.height);
}
-
qr
がnullでなければコードが認識されています。
-
qr.data
に読み取った値が入っています -
qr.location
には、QRコードの位置が入っています。認識したコードを赤枠で囲うためdrawRect()
を呼び出して枠をオーバーレイ表示しています -
props.onRecognizeCode
がnullでなければ、呼び出し元にコールバックします
if (qr) {
console.log(qr.data);
if (props.showQRFrame) {
drawRect(qr.location.topLeftCorner, qr.location.bottomRightCorner);
}
if (props.onRecognizeCode) props.onRecognizeCode(qr);
}
コンポーネント全体
import React, { useState, useEffect, useRef } from 'react';
import styled from 'styled-components';
import jsqr, { QRCode } from 'jsqr';
export type { QRCode } from 'jsqr';
export type QRReaderProps = {
width?: number,
height?: number,
pause?: boolean,
showQRFrame?: boolean,
timerInterval?: number,
gecognizeCallback?: (e: QRCode) => void,
}
type Point = {
x: number;
y: number;
}
type OverlayPosition = {
top: number,
left: number,
width: number,
height: number,
}
const RelativeWrapperDiv = styled.div<QRReaderProps>`
position: relative;
width : ${(props) => props.width}px;
height: ${(props) => props.height}px;
`;
const VideoArea = styled.video`
position: absolute;
z-index : -100;
`;
const OverlayDiv = styled.div<OverlayPosition>`
position: absolute;
border: 1px solid #F00;
top : ${(props) => props.top}px;
left : ${(props) => props.left}px;
width : ${(props) => props.width}px;
height: ${(props) => props.height}px;
`;
const QRReader: React.FC<QRReaderProps> = (props) => {
const [overlay, setOverlay] = useState({ top:0, left: 0, width: 0, height: 0 });
const video = useRef(null as HTMLVideoElement);
const timerId = useRef(null);
const drawRect = (topLeft: Point, bottomRight: Point) => {
setOverlay({
top: topLeft.y < bottomRight.y ? topLeft.y : bottomRight.y,
left: topLeft.x < bottomRight.x ? topLeft.x :bottomRight.x,
width: Math.abs(bottomRight.x - topLeft.x),
height: Math.abs(bottomRight.y - topLeft.y),
});
};
useEffect(() => {
(async() => {
if (props.pause) {
video.current.pause();
clearInterval(timerId.current);
timerId.current = null;
return;
}
const { width, height } = props;
const constraints = {
audio: false,
video: {
facingMode: 'environment',
width,
height,
}};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
video.current.srcObject = stream;
video.current.play();
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d');
if (!timerId.current) {
timerId.current = setInterval(() => {
context.drawImage(video.current, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
const qr = jsqr(imageData.data, imageData.width, imageData.height);
if (qr) {
console.log(qr.data);
if (props.showQRFrame) {
drawRect(qr.location.topLeftCorner, qr.location.bottomRightCorner);
}
if (props.gecognizeCallback) props.gecognizeCallback(qr);
}
}, props.timerInterval);
}
return () => clearInterval(timerId.current);
})();
}, [props]);
return (
<RelativeWrapperDiv {...props}>
<VideoArea ref={video}></VideoArea>
<OverlayDiv {...overlay}></OverlayDiv>
</RelativeWrapperDiv>
);
}
// propsのデフォルト値を設定
QRReader.defaultProps = {
width: 500,
height: 500,
pause: false,
showQRFrame: true,
timerInterval: 300,
};
export default QRReader;
利用方法
- コンポーネントのサイズと(width, height)と、読み取り状態(pause)に初期値をセットします。
const [qrParam, setQRParam] = useState({
width: 500,
height: 500,
pause: true,
});
-
onRecognizeCode()
で認識されたQRコードを取得し、読み取りを停止します。
const onRecognizeCode = (e: QRCode) => {
setCode(e.data);
if (stopOnRecognize) {
setQRParam( e => { return {...e, pause: true}; });
}
}`
-
toggleVideoStream()
で停止と再開を切り替えます
const toggleVideoStream = () => {
setQRParam( e => { return {...e, pause: !e.pause}; });
}
import React, { useState } from 'react';
import QRReader, { QRCode } from './QRReader';
function App() {
const [stopOnRecognize, setStopOnRecognize] = React.useState(true);
const [qrParam, setQRParam] = useState({
width: 500,
height: 500,
pause: true,
});
const [code, setCode] = useState('');
const onRecognizeCode = (e: QRCode) => {
setCode(e.data);
if (stopOnRecognize) {
setQRParam( e => { return {...e, pause: true}; });
}
}
const toggleVideoStream = () => {
setQRParam( e => { return {...e, pause: !e.pause}; });
}
return (
<div className="App">
<QRReader {...qrParam} onRecognizeCode={onRecognizeCode} />
<div>
<label>
<input type="radio" name="rdo" value="0" onChange={(e) => setStopOnRecognize(e.target.value === "0")} checked={stopOnRecognize} />認識時に自動停止
</label>
<label>
<input type="radio" name="rdo" value="1" onChange={(e) => setStopOnRecognize(e.target.value === "0")} checked={!stopOnRecognize} />認識時も処理継続
</label>
<button onClick={toggleVideoStream}>{(qrParam.pause? '再開': '停止')}</button>
<p>QRコード:{code}</p>
</div>
</div>
);
}
export default App;