From bc386dc97d9f22a458bc6570c825b3d475111463 Mon Sep 17 00:00:00 2001 From: beerosagos Date: Mon, 21 Oct 2024 16:51:07 +0200 Subject: [PATCH] frontend/qrcodescanner: improve qrscanner component 7c0b2f6 introduced a 300ms wait between the QrScanner creation and its start to fix a bug on re-renders of the component, but it seems not to be enough on certain devices, which still show the bug. It would be nice to change the 300ms fixed wait to a better way to know when the scanner is ready to be started but we could not find it. This commit improves the general structure of the component, to limit race conditions across re-renders and seems to improve the behavior and fix the bug with the tested devices. --- frontends/web/src/hooks/qrcodescanner.ts | 107 ++++++++++++++--------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/frontends/web/src/hooks/qrcodescanner.ts b/frontends/web/src/hooks/qrcodescanner.ts index 0a1ad98fdd..5fb6a65a47 100644 --- a/frontends/web/src/hooks/qrcodescanner.ts +++ b/frontends/web/src/hooks/qrcodescanner.ts @@ -34,61 +34,82 @@ export const useQRScanner = ( const { t } = useTranslation(); const [initErrorMessage, setInitErrorMessage] = useState(); const scanner = useRef(null); - - useEffect(() => { - if (videoRef.current && !scanner.current) { - scanner.current = new QrScanner( - videoRef.current, - result => { - scanner.current?.stop(); - onResult(result); - }, { - onDecodeError: err => { - const errorString = err.toString(); - if (err && !errorString.includes('No QR code found')) { - onError(err); - } - }, - highlightScanRegion: true, - highlightCodeOutline: true, - calculateScanRegion: (v) => { - const videoWidth = v.videoWidth; - const videoHeight = v.videoHeight; - const factor = 0.5; - const size = Math.floor(Math.min(videoWidth, videoHeight) * factor); - return { - x: (videoWidth - size) / 2, - y: (videoHeight - size) / 2, - width: size, - height: size - }; - } - } - ); - } - return () => { - scanner.current?.stop(); - scanner.current?.destroy(); - scanner.current = null; - }; - }, [onError, onResult, videoRef]); + // loading is set to true while the scanner is being created/started/stopped/destroyed, + // this allows to sync across re-renders. + const loading = useRef(false); useEffect(() => { (async () => { + if (!videoRef.current) { + return; + } + + while (loading.current) { + await new Promise(r => setTimeout(r, 100)); + } try { - await new Promise(r => setTimeout(r, 300)); - if (scanner.current) { - await scanner.current.start(); - if (onStart) { - onStart(); + loading.current = true; + scanner.current = new QrScanner( + videoRef.current, + result => { + scanner.current?.stop(); + onResult(result); + }, { + onDecodeError: err => { + const errorString = err.toString(); + if (err && !errorString.includes('No QR code found')) { + onError(err); + } + }, + highlightScanRegion: true, + highlightCodeOutline: true, + calculateScanRegion: (v) => { + const videoWidth = v.videoWidth; + const videoHeight = v.videoHeight; + const factor = 0.5; + const size = Math.floor(Math.min(videoWidth, videoHeight) * factor); + return { + x: (videoWidth - size) / 2, + y: (videoHeight - size) / 2, + width: size, + height: size + }; + } } + ); + // Somehow, the new QrScanner may return before it is ready to be started. + // We don't have a way to know when it is ready, but this 300ms wait seems + // to work well enough. + await new Promise(r => setTimeout(r, 300)); + await scanner.current?.start(); + loading.current = false; + if (onStart) { + onStart(); } } catch (error: any) { const stringifiedError = error.toString(); + loading.current = false; const cameraNotFound = stringifiedError === 'Camera not found.'; setInitErrorMessage(cameraNotFound ? t('send.scanQRNoCameraMessage') : stringifiedError); + onError(error); } })(); + + return () => { + (async() => { + while (loading.current) { + await new Promise(r => setTimeout(r, 100)); + } + if (scanner.current) { + loading.current = true; + await scanner.current?.pause(true); + await scanner.current?.stop(); + await scanner.current?.destroy(); + scanner.current = null; + loading.current = false; + } + })(); + }; }, [videoRef, onStart, onResult, onError, t]); return { initErrorMessage };