Stop scanner while starting?
Pre
I am using the API of Html5Qrcode with React to create the scanner component and use it inside a Modal. Upon opening the Modal, the scanner is initialized and the start() method is called, which causes the scanner to start loading the camera. Once loaded, the background can be clicked to close the Modal, which will cause the scanner component to unmount and therefore call the stop() method of the API.
Issue
I want the user to be able to close the scanner Modal even if the process of starting has not been completed yet. Using the stop() function for that doesn't seem to be possible, because the scanner isn't in SCANNING state yet as the camera is loading.
Question
What would be the most appropriate way to close the scanner while the start() method hasn't finished yet?
Thank you for your time
I am facing the same issue in a similar use-case: Multi-page application, where 1 page is the "scanner" page. So navigating to that specific page initializes/starts the camera and navigating away stops it. Now user behavior can be very random, so it can happen that a user navigates to the "scanner" page and then immediately navigates away, basically attempting to stop the scanner before it fully initialized.
To me it seems like a bug because the .then() of the initialization promise (start() method) is actually called before the scanner is fully loaded (eg before isScanning is set to true).
This should be adjusted or there should be another callback param available to use for the "fully initialized" event.
So not sure if it helps, but I ended up making it so that for the entire process of loading up the scanner, the entire page gets disabled and nothing can be clicked. So only once the scanner has been loaded, it will be possible to call stop() on it.
For me the .then() of start() seems to be getting called appropriately enough, and therefore such solution can be possible. I confirm that isScanning should rather be avoided for now.
Thank you for the suggestion. For my case I don't think it would work (also it sounds a bit hackish :) ), because you cannot block navigation, the user can always navigate at least via browser buttons like "Back".
The problem I face is to define when the scanning can be stopped. The callback of start() is too early because it's not fully loaded yet. Of course I could put something like setTimeout(stopFunc, 1000), but that is also a bit hacky.
@mezei well, if the user is clicking back so the camera initialization will stop. the web browser has no choice to wait until the device is ready so it's up to the developer to limit the events while the device is initializing, if not so for sure inconstitency will appear...
@ROBERT-MCDOWELL No. It's up to the library to provide correct callbacks and not get stuck in between states.
I prepared an example so people can get a better grasp of the issue: https://glatar.hu/scanner-stop-bug/
In this example, hash navigating to basically anything other than "root" will attempt to start the scan, while navigating to "root" attempt to stop it. If the user just navigates slowly, it all works. If however, you navigate to the scan path, then very quickly navigate back, the following happens:
stop()is called, butscanneris still initializing so throws an error, non issuestart().then()callback is called, so supposedly we can now callstop(), right? No. Some random video error will be thrown - this is the issuescanneris now (usually) stuck in a state where you can'tclear(),stop()norstart()it again, which is a pretty bad state
(Note: the isScanning flag was not used because that's also unreliable, as mentioned above.)
Feel free to inspect the code of that url, it's all in a single file.
So to sum it up this seems like an issue with the library.
@mezei when hardware control is involved with javascript be sure that the behavior of your device won't be the same as another device, even if it's exactly the same type and model of device. control hardware device from software is painful, and you will never have a solution working for all devices, unless you restrict as much as possible the events that create troubles. concerning the "back" from the browser, even this process differs from browsers and systems and also browser settings (cache, no cache etc...). so the only way on my side to make it work on almost all devices is to lock the whole UI until hardware intialization is finished
I would rather say it works exactly the same on most devices. I tested this on browsers Safari, Chrome and Firefox. The bahaviour is exactly the same on Macbook Pro, iPad Mini, iPhone 12. Same goes for Android devices and my Windows 11 machine.
Also (perhaps related to this issue), on mobile devices rises another problem:
What if the device is rotated to the side and we need to reload the scanner (because unless you reload it, the dimensions of qrBox will be misunderstood by the scanner). It is as simple as attching an orientationchange handler to window, and simply stopping and starting the scanner again. Yet the same question apperas: What if the user rotates the device while the scanner is still loading?
I would rather say it works exactly the same on most devices. I tested this on browsers Safari, Chrome and Firefox. The bahaviour is exactly the same on Macbook Pro, iPad Mini, iPhone 12. Same goes for Android devices and my Windows 11 machine. ... it's simple not possible, you think it react the same which is not when hardware is involved. it's simply logical. take a car as an example the same car will look to react the same but never ever exactly the same.
about rotation, personnaly I disabled it. if you really want it so just put an evenlistener waiting the end of initialization (no choice) and the decide what to do. Again, any kind of hardware initialization should have the device waiting for the end of init or for sure bad things will happen.
Hi @ROBERT-MCDOWELL , I am also facing the same issue. Looks like library is internally using video tag and video.play() is asynchronous which takes time to resolve. Your setTimeout suggestion is also not working for me as it throws me error when I navigate very fast The play() request was interrupted because the media was removed from the document
Any solution for same?
My source code looks like this:
import React, { useEffect } from 'react';
import { Html5Qrcode } from 'html5-qrcode';
import styles from './QRScanner.scss';
const onScanSuccess = (decodedText: string) => {
window.Toast.show(decodedText);
};
type QrDimensions = {
width: number;
height: number;
};
type QRScannerProps = {
onScanSuccess?: () => {};
onScanFailed?: () => {} | undefined;
fps: number | undefined;
qrBoxDimensions?: number | QrDimensions;
aspectRatio?: number | undefined;
disableFlip?: boolean | undefined;
};
const getQRBoxDimensions = (
viewfinderWidth: number,
viewfinderHeight: number
) => {
const minEdgePercentage = 0.7;
const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
const qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
return {
width: qrboxSize,
height: qrboxSize,
};
};
QRComponent.defaultProps = {
onScanSuccess,
fps: 100,
qrBoxDimensions: getQRBoxDimensions,
};
export default function QRComponent({
fps,
qrBoxDimensions,
aspectRatio,
disableFlip,
onScanSuccess,
onScanFailed,
}: QRScannerProps) {
useEffect(() => {
const html5QrCode = new Html5Qrcode('reader');
const oldRegion = document.getElementById('qr-shaded-region');
// const videoTracker = document.querySelector('video');
if (oldRegion) {
oldRegion.remove();
}
const handleClickAdvanced = async () => {
await html5QrCode.start(
{ facingMode: 'environment' },
{
fps,
qrbox: qrBoxDimensions,
aspectRatio,
disableFlip,
},
onScanSuccess,
onScanFailed
);
};
handleClickAdvanced();
const handleStop = async () => {
if (html5QrCode.isScanning) {
await html5QrCode.stop();
html5QrCode.clear();
}
};
return () => {
handleStop();
};
}, [
fps,
qrBoxDimensions,
aspectRatio,
disableFlip,
onScanSuccess,
onScanFailed,
]);
return (
<>
<div id="reader" className={styles.scanner}>
<video muted />
</div>
</>
);
}
just disable the UI before everything is set.
just disable the UI before everything is set.
Personally, this is exactly what I ended up doing, but there is a huge flaw... Sometimes the camera keeps on loading forever when opening the scanner. It just keeps on loading and returns no errors. But I still need the user to be able to exit out of the page.
At the end my solution was to use a real hand-held barcode scanner. Works like a charm :)
@ROBERT-MCDOWELL or @MM1132 can you guys share the sample code to disable UI until camera is loaded. I am new to react and not able to figure out at what point of time I need to freeze UI. I am relying on isScanning flag but seems like they are also setting it asynchronously
Update: I have disabled mouse click event and added popstate eventListener to freeze browser back button until library is loaded and it works like a charm :)
sorry I don't use react... only pure JS
@anmol212
Create a component with a div and some ID like barcode-scanner. Inside of that same component, use the scanner API to transform the created div element into a scanner.
Since the isScanning does not work properly, we need to abuse the callbacks of start() and stop() functions. Essentially you can define your own state... something as of scannerStarted, and then just call setScannerStarted(true) inside the then() of the start() function.
All this works well, but there is still a problem... how to let the parent component know whether your scnner has loaded up or not. For this, I used React's forwardRef, to sort of "export" the scannerStarted boolean... making it visible to the parent component.
This is not the recommended way to handle the situation by React docs, but I found it to work the best in the given situation.
Hey @MM1132, Thanks for your reply. But I have done this with slightly different approach, I have placed two event listeners and I am relying on isScanning, it does work properly but since camera is opening asynchronously so it does take time to set to true until camera starts working. I have placed isScanning inside my event listeners and I don't need to manage separate state inside my component.
here is the sample code for event listeners:
function disableMouseClick(event: MouseEvent) {
if (!html5QrCode.isScanning) {
event.stopPropagation();
event.preventDefault();
}
}
function disableBrowserBackButton() {
if (!html5QrCode.isScanning) {
window.history.pushState(null, document.title, window.location.href);
}
}
document.addEventListener('click', disableMouseClick, true);
window.addEventListener('popstate', disableBrowserBackButton);
P.S. don't forget to remove eventListeners in useEffect unmount callback 😄
Hey @MM1132, Thanks for your reply. But I have done this with slightly different approach, I have placed two event listeners and I am relying on
isScanning, it does work properly but since camera is opening asynchronously so it does take time to set to true until camera starts working. I have placed isScanning inside my event listeners and I don't need to manage separate state inside my component.here is the sample code for event listeners:
function disableMouseClick(event: MouseEvent) { if (!html5QrCode.isScanning) { event.stopPropagation(); event.preventDefault(); } } function disableBrowserBackButton() { if (!html5QrCode.isScanning) { window.history.pushState(null, document.title, window.location.href); } } document.addEventListener('click', disableMouseClick, true); window.addEventListener('popstate', disableBrowserBackButton);P.S. don't forget to remove eventListeners in useEffect unmount callback 😄
can you show me the full code, please?
@ImamAlfariziSyahputra Here you go:
import React, { useEffect } from 'react';
import { Html5Qrcode } from 'html5-qrcode';
import Header from 'components/shared/Header';
import styles from './QRScanner.scss';
export enum CameraFacingMode {
Front = 'user',
Back = 'environment',
}
const onScanSuccess = (decodedText: string) => {
window.Toast.show(decodedText);
};
type QrDimensions = {
width: number;
height: number;
};
type QRScannerProps = {
placeholderText?: string;
facingMode?: CameraFacingMode;
onScanSuccess?: () => {};
onScanFailed?: () => {};
fps: number | undefined;
qrBoxDimensions?: number | QrDimensions;
aspectRatio?: number;
disableFlip?: boolean;
};
// With this we are setting the QR box dimensions to always be 70% of the smaller edge of the video stream so it works on both mobile and PC platforms.
const getQRBoxDimensions = (
viewfinderWidth: number,
viewfinderHeight: number
) => {
const minEdgePercentage = 0.7;
const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
const qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
return {
width: qrboxSize,
height: qrboxSize,
};
};
QRScanner.defaultProps = {
facingMode: CameraFacingMode.Back,
fps: 100,
qrBoxDimensions: getQRBoxDimensions,
onScanSuccess,
};
const configureQrShadedRegion = () => {
const html5QrCode = new Html5Qrcode('qr-web-reader');
const oldRegion = document.getElementById('qr-shaded-region');
if (oldRegion) {
oldRegion.remove();
}
return html5QrCode;
};
export default function QRScanner({
placeholderText,
facingMode,
fps,
qrBoxDimensions,
aspectRatio,
disableFlip,
onScanSuccess,
onScanFailed,
}: QRScannerProps) {
useEffect(() => {
const html5QrCode = configureQrShadedRegion();
window.history.pushState(null, document.title, window.location.href);
function disableMouseClick(event: MouseEvent) {
if (!html5QrCode.isScanning) {
event.stopPropagation();
event.preventDefault();
}
}
function disableBrowserBackButton() {
if (!html5QrCode.isScanning) {
window.history.pushState(null, document.title, window.location.href);
}
}
const startQRScanner = async () => {
await html5QrCode.start(
{ facingMode },
{
fps,
qrbox: qrBoxDimensions,
aspectRatio,
disableFlip,
},
onScanSuccess,
onScanFailed
);
};
const stopQRScanner = async () => {
if (html5QrCode.isScanning) {
await html5QrCode.stop();
html5QrCode.clear();
}
};
/*
Since camera hardware takes time to load, so we have added two event listeners which will disable
browser back button until the camera is loaded so that we will not face any issues while stopping the
camera once component is unmounted.
*/
document.addEventListener('click', disableMouseClick, true);
window.addEventListener('popstate', disableBrowserBackButton);
startQRScanner();
return () => {
stopQRScanner();
document.removeEventListener('click', disableMouseClick, true);
window.removeEventListener('popstate', disableBrowserBackButton);
};
}, [
facingMode,
fps,
qrBoxDimensions,
aspectRatio,
disableFlip,
onScanSuccess,
onScanFailed,
]);
return (
<>
{placeholderText && (
<Header h1 className={styles.placeholder}>
{placeholderText}
</Label>
)}
<div id="qr-web-reader" className={styles.scanner} />
</>
);
}
@ImamAlfariziSyahputra Here you go:
import React, { useEffect } from 'react'; import { Html5Qrcode } from 'html5-qrcode'; import Header from 'components/shared/Header'; import styles from './QRScanner.scss'; export enum CameraFacingMode { Front = 'user', Back = 'environment', } const onScanSuccess = (decodedText: string) => { window.Toast.show(decodedText); }; type QrDimensions = { width: number; height: number; }; type QRScannerProps = { placeholderText?: string; facingMode?: CameraFacingMode; onScanSuccess?: () => {}; onScanFailed?: () => {}; fps: number | undefined; qrBoxDimensions?: number | QrDimensions; aspectRatio?: number; disableFlip?: boolean; }; // With this we are setting the QR box dimensions to always be 70% of the smaller edge of the video stream so it works on both mobile and PC platforms. const getQRBoxDimensions = ( viewfinderWidth: number, viewfinderHeight: number ) => { const minEdgePercentage = 0.7; const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); const qrboxSize = Math.floor(minEdgeSize * minEdgePercentage); return { width: qrboxSize, height: qrboxSize, }; }; QRScanner.defaultProps = { facingMode: CameraFacingMode.Back, fps: 100, qrBoxDimensions: getQRBoxDimensions, onScanSuccess, }; const configureQrShadedRegion = () => { const html5QrCode = new Html5Qrcode('qr-web-reader'); const oldRegion = document.getElementById('qr-shaded-region'); if (oldRegion) { oldRegion.remove(); } return html5QrCode; }; export default function QRScanner({ placeholderText, facingMode, fps, qrBoxDimensions, aspectRatio, disableFlip, onScanSuccess, onScanFailed, }: QRScannerProps) { useEffect(() => { const html5QrCode = configureQrShadedRegion(); window.history.pushState(null, document.title, window.location.href); function disableMouseClick(event: MouseEvent) { if (!html5QrCode.isScanning) { event.stopPropagation(); event.preventDefault(); } } function disableBrowserBackButton() { if (!html5QrCode.isScanning) { window.history.pushState(null, document.title, window.location.href); } } const startQRScanner = async () => { await html5QrCode.start( { facingMode }, { fps, qrbox: qrBoxDimensions, aspectRatio, disableFlip, }, onScanSuccess, onScanFailed ); }; const stopQRScanner = async () => { if (html5QrCode.isScanning) { await html5QrCode.stop(); html5QrCode.clear(); } }; /* Since camera hardware takes time to load, so we have added two event listeners which will disable browser back button until the camera is loaded so that we will not face any issues while stopping the camera once component is unmounted. */ document.addEventListener('click', disableMouseClick, true); window.addEventListener('popstate', disableBrowserBackButton); startQRScanner(); return () => { stopQRScanner(); document.removeEventListener('click', disableMouseClick, true); window.removeEventListener('popstate', disableBrowserBackButton); }; }, [ facingMode, fps, qrBoxDimensions, aspectRatio, disableFlip, onScanSuccess, onScanFailed, ]); return ( <> {placeholderText && ( <Header h1 className={styles.placeholder}> {placeholderText} </Label> )} <div id="qr-web-reader" className={styles.scanner} /> </> ); }
thanks @anmol242