diff --git a/package.json b/package.json index 88204fc..fcfe3d1 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,22 @@ { "name": "koder-react", - "version": "1.9.0", + "version": "1.10.0", "homepage": "./", "private": true, "description": "QR/bar code scanner for the Browser", "author": "Pavel Maslov", "license": "MIT", - "devDependencies": { + "dependencies": { "react": "^17.0.2", "react-dom": "^17.0.2", - "react-scripts": "5.0.0", - "react-app-rewired": "^2.1.11", - "prop-types": "^15.8.0", + "react-scripts": "5.0.1", + "react-app-rewired": "^2.2.1", + "prop-types": "^15.8.1", + "react-usestateref": "^1.0.8", "upnqr": "1.1.0", - "@pathcheck/dcc-sdk": "^0.0.23", + "@pathcheck/dcc-sdk": "^0.0.27", "buffer": "^6.0.3", - "util": "^0.12.4", + "util": "^0.12.5", "stream-browserify": "^3.0.0", "process": "^0.11.10", "http-server": "^0.13.0" diff --git a/src/components/scan.js b/src/components/scan.js index 85fd51a..7b428d0 100644 --- a/src/components/scan.js +++ b/src/components/scan.js @@ -1,6 +1,6 @@ -import React from "react"; -import PropTypes from 'prop-types'; -import {beep, WORKER_TYPE} from "../helpers"; +import React, {useEffect} from "react"; +import useState from 'react-usestateref'; +import {beep as beepNow, WORKER_TYPE} from "../helpers"; import {CODE_TYPE} from "../transformers/base"; import {Upnqr} from "../transformers/upnqr"; import {Covid19} from "../transformers/covid19"; @@ -17,6 +17,11 @@ const CANVAS_SIZE = { HEIGHT: 430 }; +const CAPTURE_OPTIONS = { + audio: false, + video: {facingMode: "environment"} +} + const sw = CANVAS_SIZE.WIDTH; const sh = CANVAS_SIZE.HEIGHT; const dw = sw; @@ -26,58 +31,63 @@ const dy = 0; let sx = 0; let sy = 0; -let fps = 0; - const crossHairSvg = "M77.125 148.02567c0-3.5774 2.73862-6.27567 6.37076-6.27567H119V117H84.0192C66.50812 117 52 130.77595 52 148.02567V183h25.125v-34.97433zM237.37338 117H202v24.75h35.18494c3.63161 0 6.69006 2.69775 6.69006 6.27567V183H269v-34.97433C269 130.77595 254.88446 117 237.37338 117zM243.875 285.4587c0 3.5774-2.73863 6.27567-6.37076 6.27567H202V317h35.50424C255.01532 317 269 302.70842 269 285.4587V251h-25.125v34.4587zM83.49576 291.73438c-3.63213 0-6.37076-2.69776-6.37076-6.27568V251H52v34.4587C52 302.70842 66.50812 317 84.0192 317H119v-25.26563H83.49576z"; const crossHairWidth = 217, crossHairHeight = 200, x0 = 53, y0 = 117; -class Scan extends React.Component { - constructor(props) { - super(props); - this.video = document.createElement("video"); - this.video.onplaying = () => { - sx = (this.video.videoWidth - CANVAS_SIZE.WIDTH) / 2; - sy = (this.video.videoHeight - CANVAS_SIZE.HEIGHT) / 2; - }; - this.state = { - btnText: BTN_TXT.START, - scanning: false, - fpsOn: this.props.fps, - bw: this.props.bw, - crosshair: this.props.crosshair, - resultOpen: false, - worker: this.props.worker, - transformToggle: true, - rawCode: "", - codeType: CODE_TYPE.RAW, - beep: this.props.beep - }; - - this.decodeQR = this.props.decode; - this.scanRate = this.props.scanRate; - this.upnqr = this.props.upnqr; - this.covid19 = this.props.covid19; - - this.qrworker = null; - this.oldTime = 0; - } - - initWorker = () => { - this.qrworker = new Worker(this.state.worker + "Worker.js"); +export default function Scan({ + beep = true, + decode = true, + worker = WORKER_TYPE.WASM, + scanRate = 250, + bw = false, + crosshair = true, + upnqr = false, + covid19 = false +}) { + + // Component state + const [btnText, setBtnText] = useState(BTN_TXT.START); + const [scanning, setScanning] = useState(false); + + const [bwOn, setBwOn, bwRef] = useState(bw); + const [crosshairOn, setCrosshairOn, crosshairRef] = useState(crosshair); + + const [resultOpen, setResultOpen] = useState(false); + const [transformToggle, setTransformToggle] = useState(true); + const [rawCode, setRawCode] = useState(); + const [codeType, setCodeType] = useState(); + const [beepOn, setBeepOn] = useState(beep); + + const [video] = useState(document.createElement("video")); + const [barcode, setBarcode] = useState(); + const [milliseconds, setMilliseconds] = useState(); + + // Constants + let qrworker = null; + let canvasElement = null; + let canvas = null; + let oldTime = 0; + + video.onplaying = () => { + sx = (video.videoWidth - CANVAS_SIZE.WIDTH) / 2; + sy = (video.videoHeight - CANVAS_SIZE.HEIGHT) / 2; + }; - this.qrworker.onmessage = async ev => { + const initWorker = () => { + qrworker = new Worker(worker + "Worker.js"); + qrworker.onmessage = async ev => { if (ev.data != null) { - this.qrworker.terminate(); + qrworker.terminate(); const result = ev.data; - this.stopScan(); + await stopScan(); let res = result.data; - const milliseconds = ev.data.ms; + const millis = ev.data.ms; const rawCode = res; let codeType = CODE_TYPE.RAW; // Transform raw to UPNQR - if (this.upnqr) { + if (upnqr) { const transformer = new Upnqr(); if (transformer.identified(res)) { codeType = transformer.codeType(); @@ -86,7 +96,7 @@ class Scan extends React.Component { } // Transform raw to COVID19 certificate - if (this.covid19) { + if (covid19) { const transformer = new Covid19(); if (transformer.identified(res)) { codeType = transformer.codeType(); @@ -94,101 +104,67 @@ class Scan extends React.Component { } } - this.setState({barcode: res, resultOpen: true, rawCode, codeType, milliseconds}); - if (this.state.beep) beep(); + setBarcode(res); + setResultOpen(true); + setRawCode(rawCode); + setCodeType(codeType); + setMilliseconds(millis); + if (beepOn) beepNow(); } }; }; - startScan = () => { - this.initWorker(); - this.fpsTimestamp = new Date(); - - this.setState({ - scanning: true, - btnText: BTN_TXT.STOP, - barcode: "", - resultOpen: false, - transformToggle: true, - rawCode: "", - codeType: CODE_TYPE.RAW - }); - navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: "environment" } }).then(stream => { - this.video.srcObject = stream; - this.video.setAttribute("playsinline", "true"); - this.video.play(); - requestAnimationFrame(this.tick); - }).catch(err => { - this.stopScan(); + const startScan = async () => { + initWorker(); + canvasElement = document.getElementById("canvas"); + canvas = canvasElement.getContext("2d", {willReadFrequently: true}); + + setBtnText(BTN_TXT.STOP); + setBarcode(null); + setResultOpen(false); + setTransformToggle(true); + setRawCode(null); + setCodeType(CODE_TYPE.RAW); + + try { + video.srcObject = await navigator.mediaDevices.getUserMedia(CAPTURE_OPTIONS); + video.setAttribute("playsinline", "true"); + await video.play(); + setScanning(true); + + requestAnimationFrame(tick); + } catch (err) { + stopScan().then(); + console.log("stopped by the user"); alert(err); - }); - }; - - initializeAudio = () => { - window.AudioContext = window.AudioContext || window.webkitAudioContext; - if (window.AudioContext) { - window.audioContext = new window.AudioContext(); } - const fixAudioContext = function (e) { - if (window.audioContext) { - // Create empty buffer - const buffer = window.audioContext.createBuffer(1, 1, 22050); - const source = window.audioContext.createBufferSource(); - source.buffer = buffer; - // Connect to output (speakers) - source.connect(window.audioContext.destination); - // Play sound - if (source.start) { - source.start(0); - } else if (source.play) { - source.play(0); - } else if (source.noteOn) { - source.noteOn(0); - } - } - // Remove events - document.removeEventListener('touchstart', fixAudioContext); - document.removeEventListener('touchend', fixAudioContext); - }; - // iOS 6-8 - document.addEventListener('touchstart', fixAudioContext); - // iOS 9 - document.addEventListener('touchend', fixAudioContext); - } + }; - stopScan = () => { - this.setState({ - scanning: false, - btnText: BTN_TXT.START, - boxShadow: "0 4px 8px 0 rgba(0, 0, 0, .2), 0 6px 20px 0 rgba(0, 0, 0, .19)" - }); - this.video.pause(); - if (this.video.srcObject) { - this.video.srcObject.getVideoTracks().forEach(track => track.stop()); - this.video.srcObject = null; + const stopScan = async () => { + setScanning(false); + setBtnText(BTN_TXT.START); + await video.pause(); + if (video.srcObject) { + console.log("stopping video...") + video.srcObject.getVideoTracks().forEach(track => track.stop()); + video.srcObject = null; } }; - tick = (time) => { - if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { - if (this.state.fpsOn) { - fps = 1000 / (time - this.fpsTimestamp); - this.fpsTimestamp = time; - } - - this.canvas.drawImage(this.video, sx, sy, sw, sh, dx, dy, dw, dh); + const tick = (time) => { + if (video.readyState === video.HAVE_ENOUGH_DATA) { + canvas.drawImage(video, sx, sy, sw, sh, dx, dy, dw, dh); - if (this.state.bw) this.monochromize(); - if (this.state.crosshair) this.drawCrosshair(); - if (this.state.fpsOn) this.drawFPS(fps); - if (this.state.scanning) requestAnimationFrame(this.tick); - if (this.decodeQR) this.recogniseQRcode(time); + if (bwRef.current) monochromize(); + if (crosshairRef.current) drawCrosshair(); + if (scanning) requestAnimationFrame(tick); + if (decode) recogniseQRcode(time); } - else if (this.state.scanning) requestAnimationFrame(this.tick); + requestAnimationFrame(tick); }; - monochromize = () => { - let imgd = this.canvas.getImageData(0, 0, this.canvasElement.width, this.canvasElement.height); + const monochromize = () => { + let imgd = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height); let pix = imgd.data; for (let i = 0; i < pix.length; i += 4) { let gray = pix[i] * 0.3 + pix[i + 1] * 0.59 + pix[i + 2] * 0.11; @@ -196,186 +172,131 @@ class Scan extends React.Component { pix[i + 1] = gray; pix[i + 2] = gray; } - this.canvas.putImageData(imgd, 0, 0); + canvas.putImageData(imgd, 0, 0); }; - drawCrosshair = () => { - this.canvas.fillStyle = "rgba(255,255,255,0.4)"; + const drawCrosshair = () => { + canvas.fillStyle = "rgba(255,255,255,0.4)"; const shape = new Path2D(crossHairSvg); - this.canvas.fill(shape); + canvas.fill(shape); }; - recogniseQRcode = (time) => { - if (time - this.oldTime > this.scanRate) { - this.oldTime = time; + const recogniseQRcode = (time) => { + if (time - oldTime > scanRate) { + oldTime = time; let imageData; - if (this.state.crosshair === true) - imageData = this.canvas.getImageData(x0, y0, crossHairWidth, crossHairHeight); + if (crosshairRef.current === true) + imageData = canvas.getImageData(x0, y0, crossHairWidth, crossHairHeight); else - imageData = this.canvas.getImageData(0, 0, this.canvasElement.width, this.canvasElement.height); - this.qrworker.postMessage({width: imageData.width, height: imageData.height}); - this.qrworker.postMessage(imageData, [imageData.data.buffer]); + imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height); + qrworker.postMessage({width: imageData.width, height: imageData.height}); + qrworker.postMessage(imageData, [imageData.data.buffer]); } }; - drawFPS = (fps) => { - this.canvas.font = "normal 16pt Arial"; - this.canvas.fillStyle = "#f8ff4c"; - this.canvas.fillText(Math.round(fps) + " fps", 10, CANVAS_SIZE.HEIGHT- 16); - }; - - componentDidMount() { - this.canvasElement = document.getElementById("canvas"); - this.canvas = this.canvasElement.getContext("2d", {willReadFrequently: true}); - this.canvasElement.width = CANVAS_SIZE.WIDTH; - this.canvasElement.height = CANVAS_SIZE.HEIGHT; - } - - onBtnClickHandler = (e) => { - e.preventDefault(); - if (this.state.scanning) this.stopScan(); else this.startScan(); - }; - - onCrossHairClickHandler = (e) => { + const onBtnClickHandler = async (e) => { e.preventDefault(); - this.setState({crosshair: !this.state.crosshair}); + if (scanning) await stopScan(); else await startScan(); }; - onFPSClickHandler = (e) => { + const onCrossHairClickHandler = async (e) => { e.preventDefault(); - this.setState({fpsOn: !this.state.fpsOn}); + setCrosshairOn(!crosshairOn); }; - onBWClickHandler = (e) => { + const onBWClickHandler = async (e) => { e.preventDefault(); - this.setState({bw: !this.state.bw}); + setBwOn(!bwOn); }; - onBeepClickHandler = (e) => { + const onBeepClickHandler = async (e) => { e.preventDefault(); - this.setState({beep: !this.state.beep}); + setBeepOn(!beepOn); }; - startStyle = () => { + const startStyle = () => { const style = {width: 64, textAlign: "center"}; - if (this.state.scanning) return { backgroundColor: "red", ...style }; - else return { backgroundColor: "", ...style }; + if (scanning) return {backgroundColor: "red", ...style}; + else return {backgroundColor: "", ...style}; }; - fpsStyle = () => { - if (this.state.fpsOn) return { backgroundColor: "green" }; - else return { backgroundColor: "" }; + const xHairStyle = () => { + if (crosshairOn) return {backgroundColor: "green"}; + else return {backgroundColor: ""}; }; - xHairStyle = () => { - if (this.state.crosshair) return { backgroundColor: "green" }; - else return { backgroundColor: "" }; + const bwStyle = () => { + if (bwOn) return {backgroundColor: "green"}; + else return {backgroundColor: ""}; }; - bwStyle = () => { - if (this.state.bw) return { backgroundColor: "green" }; - else return { backgroundColor: "" }; + const beepStyle = () => { + if (beepOn) return {backgroundColor: "green"}; + else return {backgroundColor: ""}; }; - beepStyle = () => { - if (this.state.beep) return { backgroundColor: "green" }; - else return { backgroundColor: "" }; - }; - - transformToggleStyle = () => { - if (this.state.transformToggle) return { backgroundColor: "green", padding: 12 }; + const transformToggleStyle = () => { + if (transformToggle) return {backgroundColor: "green", padding: 12}; else return {backgroundColor: "", padding: 12}; } - render() { - return ( -