zxing-js / browser

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

NextJS - stop() not stopping decodeFromConstraints #128

Open Snolandia opened 9 months ago

Snolandia commented 9 months ago

Stop doesnt seem to do anything. Calling it or not calling it has no effect and the callback continuously gets called.

Code Below

const controlsRef: MutableRefObject<IScannerControls | null | undefined> = useRef(undefined);

useEffect(()=>{
const codeReader = new BrowserQRCodeReader(undefined, { delayBetweenScanAttempts:props.qrReaderProps.scanDelay, }); codeReader.decodeFromConstraints({ video:props.qrReaderProps.constraints }, props.qrReaderProps.videoId, (result, error,controls) => {onResult(result,error,controls,codeReader)}) .then((controls: IScannerControls) => { controlsRef.current = controls; }) .catch((error: Error) => { if(props.qrReaderProps.onResult){ onResult(null, error, controlsRef.current, codeReader); } }); return () => { BrowserCodeReader.releaseAllStreams() controlsRef.current?.stop(); }; },[])

JamDev0 commented 7 months ago

So, first, the useEffect clean up:

return () => {
  BrowserCodeReader.releaseAllStreams()
  controlsRef.current?.stop();
};

dosen't have access to the controlsRef.current defined inside of itself, so the controlsRef.current?.stop() will not work.

Secondly, useEffect is called twice by default in React, so the codeReader.decodeFromConstraints is being called twice and the controlsRef.current?.stop() is only stopping one instance of it, the previous one.

Snolandia commented 7 months ago

Yes, this creates a memory leak which is quite annoying. I figured out a work around for it but ended up having to modify the codereader a bit.

gregg-cbs commented 2 months ago

I have noticed this too - im not using nextjs. We are having issues with battery on mobile because this scanner is constantly running. We are having to write a few hacks to get this thing to stop.

Essentially we want to pause the reader and play the reader to save battery but it seems to be quite difficult to do.

If anyone has ideas or solutions please let me know :)

gregg-cbs commented 2 months ago

After a significant amount of testing, this seems to destroy the video on all devices.

  let scanDelay: number = 250;
  let qrCodeResult: string | undefined = '';
  let videoRef: HTMLVideoElement;
  let codeReader: BrowserQRCodeReader;
  let videoControl: IScannerControls;
  let isVideoRunning = false;

  function init() {
    stop();

    codeReader = new BrowserQRCodeReader(undefined, {
      delayBetweenScanAttempts: scanDelay
    });
  }

  function stop() {
    if (videoControl) {
      videoControl.stop();
    }
    if (videoRef) {
      videoRef.pause()
    }
    BrowserQRCodeReader.cleanVideoSource(videoRef);
    BrowserQRCodeReader.releaseAllStreams();
    isVideoRunning = false;
  }

  function start() {
    isVideoRunning = true;
    qrCodeResult = "";

    codeReader
    .decodeFromConstraints({ 
      video: {
        aspectRatio: 1,
        facingMode: 'environment'
      }
    }, videoRef, (result)=> {
      if (result?.getText()) {
        qrCodeResult = result.getText();
      }
    })
    .then((controls: IScannerControls) => {
      videoControl = controls
    })
    .catch((error: Error) => {
      console.error('Error decoding QR Code:', error);
    });
  }
Snolandia commented 2 months ago

@gregg-cbs

I'm fairly sure I ended up resolving this with being able to stop start. My use case was also for mobile devices, being able to scan one code, do something, then restart the scanner. I'm away from my computer for the weekend though and I'll try to remember Monday to find what I did. Feel free to @ me if I don't reply to this again by end of Monday as I probably just got distracted.

Snolandia commented 2 months ago

@gregg-cbs This is what I used in React/NextJS.

// A small test page
const qrResult:OnResultFunction = (result, error,controls,codereader) => {
// your function here
}

var testProps:QrReaderProps = {
        onResult:(result, error, codeReader) => {
            qrResult(result,error, codeReader)
        },
        videoId: 'video',
        scanDelay:100,
        constraints:{ facingMode:  "environment"  },
        videoContainerStyle:{height:'auto',width:'auto'},
        videoStyle:{height:'auto',width:'auto'},
        containerStyle:{height:'auto',width:'auto'}
    }

const [cameraOn, setCameraOn] = useState<boolean>(true)

return (
            <div style={{position:'relative'}}>
                <button onClick={()=>{setCameraOn(!cameraOn)}}>
                    Turn Scanner On/Off
                </button>
                {cameraOn&&
                <QrReaderT qrReaderProps={testProps}/>
    }
//The modified qrReader Component

`'use client'

import * as React from 'react';

import { MutableRefObject, useEffect, useRef, useState } from 'react';
import { BrowserCodeReader, BrowserQRCodeReader, IScannerControls } from '@zxing/browser';

import { Result, Exception } from '@zxing/library';

export type QrReaderProps = {
  /**
   * Media track constraints object, to specify which camera and capabilities to use
   */
  constraints: MediaTrackConstraints;
  /**
   * Called when an error occurs.
   */
  onResult?: OnResultFunction;
  /**
   * Property that represents the view finder component
   */
  ViewFinder?: (props: any) => React.ReactElement<any, any> | null;
  /**
   * Property that represents the scan period
   */
  scanDelay?: number;
  /**
   * Property that represents the ID of the video element
   */
  videoId?: string;
  /**
   * Property that represents an optional className to modify styles
   */
  className?: string;
  /**
   * Property that represents a style for the container
   */
  containerStyle?: any;
  /**
   * Property that represents a style for the video container
   */
  videoContainerStyle?: any;
  /**
   * Property that represents a style for the video
   */
  videoStyle?: any;
};

export type OnResultFunction = (
  /**
   * The QR values extracted by Zxing
   */
  result?: Result | undefined | null,
  /**
   * The name of the exceptions thrown while reading the QR
   */
  error?: Error | undefined | null,

  controls?: IScannerControls | undefined | null,
  /**
   * The instance of the QR browser reader
   */
  codeReader?: BrowserQRCodeReader
) => void;

export type UseQrReaderHookProps = {
  /**
   * Media constraints object, to specify which camera and capabilities to use
   */
  constraints?: MediaTrackConstraints;
  /**
   * Callback for retrieving the result
   */
  onResult?: OnResultFunction;
  /**
   * Property that represents the scan period
   */
  scanDelay?: number;
  /**
   * Property that represents the ID of the video element
   */
  videoId?: string;

  controlsRef:MutableRefObject<IScannerControls | null | undefined>
};

export type UseQrReaderHook = (props: UseQrReaderHookProps) => void;

export const isMediaDevicesSupported = () => {
    const isMediaDevicesSupported =
      typeof navigator !== 'undefined' && !!navigator.mediaDevices;

    if (!isMediaDevicesSupported) {
      console.warn(
        `[ReactQrReader]: MediaDevices API has no support for your browser. You can fix this by running "npm i webrtc-adapter"`
      );
    }

    return isMediaDevicesSupported;
  };

  export const isValidType = (value: any, name: string, type: string) => {
    const isValid = typeof value === type;

    if (!isValid) {
      console.warn(
        `[ReactQrReader]: Expected "${name}" to be a of type "${type}".`
      );
    }

    return isValid;
  };

export const styles: any = {
    container: {
      width: '100%',
      paddingTop: '100%',
      overflow: 'hidden',
      position: 'relative',
    },
    video: {
      top: 0,
      left: 0,
      width: '100%',
      height: '100%',
      display: 'block',
      overflow: 'hidden',
      position: 'absolute',
      transform: undefined,
    },
  };

export function QrReaderT(props:{qrReaderProps:QrReaderProps}){

    // const controlsRef: MutableRefObject<IScannerControls | null | undefined> = useRef(undefined);
    var controlsRef:IScannerControls | null | undefined = undefined

    const setUp = useRef(true)

    const codeReader = new BrowserQRCodeReader(undefined, {
      delayBetweenScanAttempts:props.qrReaderProps.scanDelay,
    });

    const [process, setProcess] = useState(true);

    const decodeCallback =((result: Result | undefined, error: Exception | undefined,controls: IScannerControls, codeReader:BrowserQRCodeReader)=>{
        if (isValidType(props.qrReaderProps.onResult, 'onResult', 'function')) {
          if (controlsRef === null) {
              throw new Error('Component is unmounted');
          }
          if(props.qrReaderProps.onResult){
            props.qrReaderProps.onResult(result, error,controls, codeReader);
          }
        }
    })

    const codeReaderSetup = ()=>{
      codeReader.decodeFromConstraints({ video:props.qrReaderProps.constraints }, props.qrReaderProps.videoId, (result, error,controls) => {decodeCallback(result,error,controls,codeReader)})
      .then((controls: IScannerControls) => {
          if (controlsRef === undefined) {
            console.log('set Controls')
            controlsRef = controls;
          }
        })
      .catch((error: Error) => {
        if (isValidType(props.qrReaderProps.onResult, 'onResult', 'function')) {
          if(props.qrReaderProps.onResult){
            console.log('error catch')
            props.qrReaderProps.onResult(null, error, controlsRef, codeReader);
          }
        }
      });
    }

    useEffect(()=>{  
        console.log("UseEffect : qrReaderT")
      if(setUp.current){

      if (!isMediaDevicesSupported() && isValidType(props.qrReaderProps.onResult, 'onResult', 'function')) {
        const message = 'MediaDevices API has no support for your browser. You can fix this by running "npm i webrtc-adapter"';
          if(props.qrReaderProps.onResult){
            props.qrReaderProps.onResult(null, new Error(message),null, codeReader);
          }
          console.log("No Media Devices")
      }

      if (isValidType(props.qrReaderProps.constraints, 'constraints', 'object')) {
        console.log('is valid type')
        codeReader.decodeFromConstraints({ video:props.qrReaderProps.constraints }, props.qrReaderProps.videoId, (result, error,controls) => {decodeCallback(result,error,controls,codeReader)})
      .then((controls: IScannerControls) => {
          if (controlsRef === undefined) {
            console.log('set Controls')
            controlsRef = controls;
          }
        })
      .catch((error: Error) => {
        if (isValidType(props.qrReaderProps.onResult, 'onResult', 'function')) {
          if(props.qrReaderProps.onResult){
            console.log('error catch')
            props.qrReaderProps.onResult(null, error, controlsRef, codeReader);
          }
        }
      }); 
      }
    }
    setUp.current = false;
      return () => {if (controlsRef === undefined) {
          console.log("Scanner Cleanup did not go as planned")
        } else {
          console.log("QR Scanner is being cleaned up")
          BrowserCodeReader.releaseAllStreams()
          controlsRef?.stop()
        }
      };
    },[])

    return (
      <section className={props.qrReaderProps.className} style={props.qrReaderProps.containerStyle}>
        <div
          style={{
            ...styles.container,
            ...props.qrReaderProps.videoContainerStyle,
          }}
        >
          {!!props.qrReaderProps.ViewFinder && <props.qrReaderProps.ViewFinder/>}
          <video
            muted
            id={props.qrReaderProps.videoId}
            style={{
              ...styles.video,
              ...props.qrReaderProps.videoStyle,
              transform: props.qrReaderProps.constraints?.facingMode === 'user' && 'scaleX(-1)',
            }}
          />
        </div>
      </section>
    );
  };