zxing-js / browser

ZXing for JS's browser layer with decoding implementations for browser.
MIT License
222 stars 44 forks source link

how correct stop video in react hook #19

Open PaloJeDobryClovek opened 3 years ago

PaloJeDobryClovek commented 3 years ago

Hello, I have in react component, when is render I want run scanner, if not stop scanning barcode.

import React, {useRef, useEffect} from 'react';
import {BrowserMultiFormatReader, BarcodeFormat} from '@zxing/browser';
import DecodeHintType from "@zxing/library/cjs/core/DecodeHintType";
import {makeStyles} from "@material-ui/core/styles";

const useStyles = makeStyles(theme => ({
    video: {
        width: '100%',
        height: '100%',
        maxHeight: '50vh',
        objectFit: 'cover',
    },
}));

export default function ScannerZxing({callback}) {
    const classes = useStyles();
    const video = useRef();
    useEffect(() => {
            const hints = new Map();
            const formats = [BarcodeFormat.QR_CODE, BarcodeFormat.CODE_128, BarcodeFormat.ITF];
            hints.set(DecodeHintType.POSSIBLE_FORMATS, formats);
            const codeReader = new BrowserMultiFormatReader(hints, {
                delayBetweenScanSuccess: 2000,
                delayBetweenScanAttempts: 600,
            });
            var controls = null;
            controls = codeReader
                .decodeFromVideoDevice(undefined, video.current, function (result, error) {
                    if (typeof result !== "undefined") {
                        callback(result); // redirect on different page and not render this
                    }
                })
            console.log(controls);

            return function cleanup() {
                console.log('clean');
                //controls.stop();
            };
    });

    return (
        <video
            className={classes.video}
            ref={video}
        ></video>
    );
}

but it now works correctly and after scan and render different page scanner still work, could you please write example in react?

odahcam commented 3 years ago

Yeah, will write in a few days. Please ping me here if it takes too long.

PaloJeDobryClovek commented 3 years ago

You will made my day when you write example :)

odahcam commented 3 years ago

Just to give a tip before we move on (I'm a little busy this week), you can add controls to the decodeFromVideoDevice params so you have a strong reference to the controls variable inside the callback scope:

                .decodeFromVideoDevice(undefined, video.current, function (result, error, controls) {
                    if (typeof result !== "undefined") {
                        controls.stop(); // stops the scanning
                        callback(result); // redirect on different page and not render this
                    }
                })

You can also make use of decodeOnceFromVideoDevice:

                .decodeOnceFromVideoDevice(undefined, video.current, function (result, error) {
                    if (typeof result !== "undefined") {
                        callback(result); // redirect on different page and not render this
                    }

                    // the scanning proccess stops by itself after first successful decode
                })

I'll try to create some working example soon, thanks for being patient!

lancechentw commented 3 years ago

Try

            const controlsPromise = codeReader
                .decodeFromVideoDevice(undefined, video.current, function (result, error) {
                    if (typeof result !== "undefined") {
                        callback(result); // redirect on different page and not render this
                    }
                })

            return function cleanup() {
                console.log('clean');
                controlsPromise.then(controls => controls.stop());
            };
lancechentw commented 3 years ago

I ended up doing this

    const mountedRef = useRef(true);

    useEffect(() => {
            mountedRef.current = true;
            ...
            codeReader
                .decodeFromVideoDevice(undefined, video.current, function (result, error, controls) {
                    if (mountedRef.current === false) {
                        controls.stop();
                        return;
                    }

                    if (typeof result !== "undefined") {
                        callback(result); // redirect on different page and not render this
                    }
                })

            return function cleanup() {
                console.log('clean');
                mountedRef.current = false;
            };
odahcam commented 3 years ago

You could do something like:

    const mountedRef = useRef(true);

    useEffect(() => {
            mountedRef.current = true;
            ...
            codeReader
                .decodeFromVideoDevice(undefined, video.current, function (result, error, controls) {
                    if (mountedRef.current === false) {
                        controls.stop();
                        return;
                    }

                    if (typeof result !== "undefined") {
                        controls.stop(); // you only needed this to stop the scanner
                        callback(result); // redirect on different page and not render this
                    }
                })

            return function cleanup() {
                console.log('clean');
                mountedRef.current = false;
            };
lancechentw commented 3 years ago

@odahcam you're right, the first controls.stop() is to break the loop when a user does not do a scan and just head to some where else, and the second controls.stop() is to break the loop when a user does do a scan. I was so into how to deal with the first case, so I left out the second one. Thanks!

PaloJeDobryClovek commented 3 years ago

Hi, is possible start again stopped IScannerControls? Becauouse I dont see any method to use. Or just process pause loop ? I have use case...scan code (pause scanner)...send request to server, if its correct redirect, if not continue in scanning. And I dont know how handle it correctly.

odahcam commented 3 years ago

There's no way yet. It's actually very requested so I will create something about it soon. For now you can use controls.stop() and then call codeReader.decodeFromVideoDevice(...) again; I would suggest you create a wrapper function like startScanner() around the codeReader.decodeFrom...() call so you don't have to repeat it every time.

antarsaha9 commented 3 years ago

This is my workaround to stop video after scanning done without redirecting to different page.

function decodeOnce(selectedDeviceId) {
    codeReader.decodeOnceFromVideoDevice(selectedDeviceId, 'video')
      .then((result) => {
        console.log(result)
        setCode(result.text);

      }).catch((err) => {
        console.error(err)

      }).then(() => {
        video.current.srcObject.getTracks()[0].stop();
        setTimeout(() => {
          props.done(code); // Tell parent component to stop rendering
        }, 1000);
      });
  }
mmalomo commented 2 years ago

I try everything it says here and in other topics and nothing works 100%. It works the first time, maybe the second time and then it doesn't work anymore until you refresh the page. Navigation handled by a react router, for example in NextJs, does not refresh the page so the camera is not released.

If I find a way that works 100% I'll come back with the solution

tuul-wq commented 2 years ago

@mmalomo I had a similar issue only in Safari. Using qrScanner.current.stop() disabled scanning process but didn't affect camera's light on my laptop. Fortunately I spotted a difference in streams ids that I got from navigator.mediaDevices.getUserMedia({ video: true }) and videoRef.current.srcObject. Thus I decided to save stream from getUserMedia and use it to stop all the video tracks.

<script type="text/javascript">
    window.addEventListener('load', async () => {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });

      const codeReader = new ZXingBrowser.BrowserQRCodeReader();
      const previewElem = document.querySelector('#video');

      // undefined - takes 1st available camera
      try {
        const controls = await codeReader.decodeFromVideoDevice(undefined, previewElem, (result) => {
          console.log('scan ...');
        });

        // stop after 1.5 sec
        setTimeout(() => {
          stream.getVideoTracks().forEach(track => track.stop());
          controls.stop();
        }, 1500);
      } catch (error) {
        console.warn(error);
      }
    });
  </script>

In React I do this to stop scanning + camera's light.

  streamRef.current.getVideoTracks().forEach((track) => track.stop());
  qrScannerControls.current.stop();