🐛 App unable to go to sleep once camera loads
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
- [ ] I am using Expo
- [ ] I have enabled Frame Processors (react-native-worklets-core)
- [X] I have read the Troubleshooting Guide
- [X] I agree to follow this project's Code of Conduct
- [X] I searched for similar issues in this repository and found none.
Guten Tag, Hans here.
[!NOTE] New features, bugfixes, updates and other improvements are all handled mostly by
@mrousavyin his free time. To support@mrousavy, please consider 💖 sponsoring him on GitHub 💖. Sponsored issues will be prioritized.
@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.
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.
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.
@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.
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?
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.
+1 on this. I commented out the line:
UIApplication.shared.isIdleTimerDisabled = isActive
is it fixed? i still face the issue