react-native-vision-camera icon indicating copy to clipboard operation
react-native-vision-camera copied to clipboard

🐛 App unable to go to sleep once camera loads

Open FoundersApproach opened this issue 1 year ago • 9 comments

What's happening?

Once camera library loads in one screen and going back to previous screen in stack navigation then app unable to goes to sleep. May be camera reference object is unable to let app sleep ?

I did some object reference clearance in component unmount but no luck. Please help!

Reproduceable Code

import React, { useState, useEffect, useCallback } from 'react';
import { View, Text, TouchableOpacity, Alert, Platform, StyleSheet, PermissionsAndroid, Image, AppState } from 'react-native';
import {Camera, useCameraDevices, useCameraDevice,useCameraFormat, Templates, useCodeScanner } from 'react-native-vision-camera';
import TextRecognition from '@react-native-ml-kit/text-recognition';
import AppConstants from '../module/constantVairable'
import { getCameraFace, getCurrentOrientation, getDeviceHeight, getDeviceWidth, getSafeAreaInsetBottom, getSafeAreaInsetTop, getTorchOn, isOrientationPortrait, setCameraRef, setScanLog, verifyGTINChecksum } from '../api/api';
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import ImageResizer from '@bam.tech/react-native-image-resizer';
import { RFValue } from 'react-native-responsive-fontsize';
import RNFS from 'react-native-fs';
import Animated, { runOnJS } from 'react-native-reanimated';


let cameraRef = null
let capturePhotoInteval = false
let focusPointTimer = false

const ScannerFunction = (props) => {
    const device = useCameraDevice(getCameraFace());
    const [permissionGranted, setPermissionGranted] = useState(false);
    const [isInitialized, setIsInitialized] = useState(false);
    const [torch, setTorch] = useState(false);
    const [restartCamera, setRestartCamera] = useState(false);
    const [cameraActive, setCameraActive] = useState(true);

    const [tapLocation, setTapLocation] = useState(null);


    useEffect(() => {
        // let appStateListener = AppState.addEventListener('change', appStateChangeListener);
        if (Platform.OS === 'android') {
          askCameraPermission();
        }
        else {
          setPermissionGranted(true)
          if (device && device.hasTorch && getTorchOn()) {
            setTorch(true)
          }
        }
        startCaptureTimer(2)
        
        return () => {
          cameraRef = null
          console.log("ScannerFunction unmount>>>>")
          setCameraActive(false)
          setTorch(false)
          setPermissionGranted(false)
          stopCaptureTimer()
          setCameraRef(null)
        }
    }, []);

    const startCaptureTimer = (sec=1) => {
      // console.log("startCaptureTimer>>>>")
      if (capturePhotoInteval) {
        clearInterval(capturePhotoInteval);
      }

      capturePhotoInteval = setInterval(() => {
        // console.log("capturePhotoInteval")
        capturePhoto()
      }, 2 * 1000);
    }

    const stopCaptureTimer = () => {
      if (capturePhotoInteval) {
        // console.log("clear interval")
        clearInterval(capturePhotoInteval);
      }
    }

    const format1 = useCameraFormat(device, [
      { photoResolution: "max" },
    ])

    const askCameraPermission = async () => {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.CAMERA,
        {
          title: "Camera Permission",
          message: "This app needs access to your camera ",
        }
      );
      if (granted === PermissionsAndroid.RESULTS.GRANTED) {
        console.log("You can use the camera");
        setPermissionGranted(true)
      } else {
        console.log("Camera permission denied");
      }
    }

    const parseText = (line, wholeText="", photoUrl="") => {
    }

    const capturePhoto = async () => {
      
        try {
          if (props.is_active) {
            if (cameraRef) {

              const photo = await cameraRef.takePhoto({
                qualityPrioritization: "quality",
                flash: "off",
                enableAutoStabilization: true,
                enableShutterSound: false
              });

              let photoUrl = "file://"+photo.path
  
              stopCaptureTimer()
              const result = await TextRecognition.recognize(photoUrl);
              
              for (let block of result.blocks) {
                
              
                for (let line of block.lines) {
                  setScanLog(line.text)
                  parseText(line.text, result.text, photoUrl)
                }
              }
  
              startCaptureTimer(2)
              
            }
            else {
              // Alert.alert("Camera not found")
            }
          }
          
        } catch (error) {
          setCameraActive(false)
          setCameraRef(null)
          cameraRef = null
          props.onError(error)
          console.log("Error>>>>>>>>>>>>", error.message)
        }
        
    }

    const focus = useCallback((point: Point) => {
      const c = cameraRef
      if (c == null) return
      c.focus(point)
      setTapLocation({x: point.x - RFValue(35), y: point.y - RFValue(35)})
      if (focusPointTimer) {
        clearTimeout(focusPointTimer)
      }
      focusPointTimer = setTimeout(() => {
        setTapLocation(null)
      }, 2 * 1000)
    }, [])

    const gesture = Gesture.Tap()
    .onEnd(({ x, y }) => {
      runOnJS(focus)({ x, y })
    })

    const _onError = (error) => {
    }

    return(
      <View style={{flex: 1, overflow: "hidden", backgroundColor: "black"}}>
      {/* <GestureDetector gesture={_gesture}> */}
      {
        device && device.hasTorch ? (
          <TouchableOpacity 
              onPress={() => setTorch(!torch)}
              style={{position: 'absolute', zIndex: 999 ,
              top: isOrientationPortrait() ? RFValue(10) : null, 
              bottom: isOrientationPortrait() ? null : (getSafeAreaInsetTop() + getSafeAreaInsetBottom() + RFValue(10)),
              right: RFValue(10), 
              height: RFValue(35), 
              width: RFValue(35), 
              transform: [{rotate: isOrientationPortrait() ? '0deg' : '-270deg'}],
              borderRadius: RFValue(5) ,backgroundColor: "#ffffff99", 
              alignItems: 'center', justifyContent: 'center'}}>
              <Image resizeMode='contain' style={{height: '65%', width: '65%'}} 
                  source={torch ? Theme.icons.ic_camera_light_off : Theme.icons.ic_camera_light_on}></Image>
          </TouchableOpacity> 
        ) : null
      }
      
      {/* this.onStopped = this.onStopped.bind(this)
    this.onError = this.onError.bind(this) */}

      {
        device && permissionGranted && props.is_active ? (
          <View style={{flex: 1}}>
          <GestureDetector gesture={gesture}>
          <Camera
            onError={(error)=>props.onError(error)}
          orientation={getCurrentOrientation()}
          torch={torch ? "on" : "off"}
          onInitialized={() => {
            setIsInitialized(true);
            setCameraRef(cameraRef)
          }}
              style={
                isInitialized
                  ? {
                      position: "absolute",
                      top: 0,
                      left: 0,
                      right: 0,
                      bottom: 0
                    }
                  : {
                      width: 0,
                      height: 0,
                    }
               }
              device={device}
              isActive={cameraActive}
              photo={true}
              ref={(ref) => cameraRef = ref}
              
            />
            
            </GestureDetector>
            {
              tapLocation && (
                <Animated.View
                  style={[
                    styles.indicator,
                    {
                      transform: [
                        { translateX: tapLocation.x - 15 },
                        { translateY: tapLocation.y - 15 },
                      ],
                    },
                  ]}
                />
              )
            }
            

            </View>
        ) : null
      }
      </View>
    )
}
export default ScannerFunction;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'transparent',
  },
  indicator: {
    width: RFValue(70),
    height: RFValue(70),
    borderRadius: RFValue(70),
    position: 'absolute',
    borderWidth: 4,
    borderColor: Theme.colors.appThemeColor,
  },
});

Relevant log output

This issue seems different so no log helps

Camera Device

<Camera
            onError={(error)=>props.onError(error)}
          orientation={getCurrentOrientation()}
          torch={torch ? "on" : "off"}
          onInitialized={() => {
            setIsInitialized(true);
            setCameraRef(cameraRef)
          }}
              style={
                isInitialized
                  ? {
                      position: "absolute",
                      top: 0,
                      left: 0,
                      right: 0,
                      bottom: 0
                    }
                  : {
                      width: 0,
                      height: 0,
                    }
               }
              device={device}
              isActive={cameraActive}
              photo={true}
              ref={(ref) => cameraRef = ref}
              
            />

Device

iPhone 14 plus (ios 17.1.1)

VisionCamera Version

"react-native-vision-camera": "^3.9.2"

Can you reproduce this issue in the VisionCamera Example app?

Yes, I can reproduce the same issue in the Example app here

Additional information

FoundersApproach avatar Jul 02 '24 16:07 FoundersApproach

Guten Tag, Hans here.

[!NOTE] New features, bugfixes, updates and other improvements are all handled mostly by @mrousavy in his free time. To support @mrousavy, please consider 💖 sponsoring him on GitHub 💖. Sponsored issues will be prioritized.

maintenance-hans[bot] avatar Jul 02 '24 16:07 maintenance-hans[bot]

@FoundersApproach We're seeing a similar issue on iOS 16+. On iOS <= 15 the green dot showing the camera is active never goes away. Can you try capturing the "goBack" and then using setTimeout to set the camera in-active prior to going back?

Something like this:

const delayedGoBack = useCallback(() => {
     setCameraActive(false)
     setTimeout(() => navigation.goBack(), 10)
}, [])

Note: Would need to disable back gestures in order to make sure navigating back is always captured.

kevinranks avatar Aug 01 '24 16:08 kevinranks

https://github.com/mrousavy/react-native-vision-camera/blob/77e98178f84824a0b1c76785469413b64dc96046/package/ios/React/CameraView.swift#L281

Looks like this specific line keeps the phone from going to sleep. It doesn't appear that this is implemented on Android for VC.

kevinranks avatar Aug 01 '24 18:08 kevinranks

Perhaps this should just be removed? Idle management could be left to library consumers. This would benefit Expo users, especially, which uses keep-awake to allow stacked calls to hold and release wake lock / idle timer setting.

jkaufman avatar Aug 22 '24 17:08 jkaufman

@FoundersApproach We're seeing a similar issue on iOS 16+. On iOS <= 15 the green dot showing the camera is active never goes away. Can you try capturing the "goBack" and then using setTimeout to set the camera in-active prior to going back?

Something like this:

const delayedGoBack = useCallback(() => {
     setCameraActive(false)
     setTimeout(() => navigation.goBack(), 10)
}, [])

Note: Would need to disable back gestures in order to make sure navigating back is always captured.

this approach works. I need to delay the goBack action. if both cameraActive and goBack is executed together. Then there will be some delay for the green dot to be gone. If you want the green dot to be gone asap. You need to delay the navigation like mentioned above.

ibrahim-ytl avatar Oct 09 '24 08:10 ibrahim-ytl

I'm finally migrating from react-native-camera to get rid of some old problems.. only to run into new ones =(

Leaving the green camera dot on and prevent the app from sleeping is a no-go for me. The delayedGoBack approach solves this, but only when I'm in charge of the back navigation. Do I really have to disable both the back button and the "swipe to go back" function to get this to work?

I have postponed the migration for many years and I was quite confident that vision-camera would be a really mature product by now. Everything else in vision-camera seems excellent, but this bug is a bummer. I'm hoping that I have misunderstood something or that I have misconfigured something. Is it really the case that this bug affects everyone who uses react-navigation?

msageryd avatar Dec 11 '24 09:12 msageryd

For anyone else ending up here: I tapped into the "beforeRemove" event in order to handle this delay even if the back navigation is not explicitly performed by me via goBack.

With the below code I can use the regular navigation.goBack() and the user can go back via the back-button. This should also work for the Android physical back button, but I haven't tried.

import {
  useNavigation,
  useFocusEffect,
} from '@react-navigation/native';
 useFocusEffect(
    useCallback(() => {
      const unsubscribe = navigation.addListener('beforeRemove', async e => {
        e.preventDefault();
        setIsCameraActive(false);
        await new Promise(resolve => setTimeout(resolve, 10));
        navigation.dispatch(e.data.action);
      });
      return unsubscribe;
    }, [navigation]),
  );

edit: I just noticed that this solution was already shown here: https://github.com/mrousavy/react-native-vision-camera/issues/905#issuecomment-1208571973

Only difference is the delay and useFocusEffect instead of useEffect, which makes it possible to deactivate the camera even for routes which stays mounted. I.e. deactivate camera "onBlur" instead of "unMount"

edit2: It turns out that a much more elegant solution exists: https://github.com/mrousavy/react-native-vision-camera/issues/905#issuecomment-1505718969

Simply use the value from useIsFocued as input for isActive. It works great for me.

msageryd avatar Dec 11 '24 11:12 msageryd

+1 on this. I commented out the line:

UIApplication.shared.isIdleTimerDisabled = isActive

ChristopherGabba avatar Feb 09 '25 22:02 ChristopherGabba

is it fixed? i still face the issue

milon27 avatar Sep 02 '25 14:09 milon27