react-native-vision-camera
react-native-vision-camera copied to clipboard
🐛 App crashes when switching front/back camera while recording a video.
What were you trying to do?
While recording a video when I switches the camera from front to back or vice versa quickly and doing it many times it crashes.
Reproduceable Code
import React, { useEffect, useCallback, useRef, useState } from "react";
import {
StyleSheet,
View,
TouchableOpacity,
Linking,
Platform,
AppState,
Modal,
} from "react-native";
import { Button, ButtonWithImage, Text } from "../components";
import { useIsFocused } from "@react-navigation/core";
import { ColorConst, ImageConst } from "../constants";
import { Camera, useCameraDevices } from "react-native-vision-camera";
import { SizeClass } from "../utils/AppTheme";
import { FONT_FAMILY, APP_FONTS } from "../utils/FontsUtils";
import Timer from "./Timer";
import { useDispatch } from "react-redux";
import { timerReset } from "../redux/Timer/TimerSlice";
import { SafeAreaView } from "react-native-safe-area-context";
import { deleteFile } from "../utils/Utility";
export default function VidiAskCamera(props) {
const {
showBackCamera,
setShowBackCamera,
isRecording,
setIsRecording,
recordedVideo,
setRecordedVideo,
navigation,
setVideoPath,
replyTo,
} = props;
const devices =
showBackCamera === true
? useCameraDevices("wide-angle-camera")
: useCameraDevices();
const isFocused = useIsFocused();
const cameraRef = useRef();
const [cameraPermission, setCameraPermission] = useState();
const [micPermission, setMicPermission] = useState();
const [flashMessageModalVisible, setFlashMessageModalVisible] =
useState(false);
const [onCameraFlip, setOnCameraFlip] = useState(false);
const device = showBackCamera === true ? devices.back : devices.front;
const appStateVar = useRef(AppState.currentState);
const dispatch = useDispatch();
const reqPer = async () => {
try {
const newCameraPermission = await Camera.requestCameraPermission();
const newMicrophonePermission =
await Camera.requestMicrophonePermission();
setCameraPermission(newCameraPermission);
setMicPermission(newMicrophonePermission);
} catch (error) {}
};
// initially checking for permissions then requesting a new one if its not allowed.
useEffect(() => {
reqPer();
}, []);
// this is to handle when video is interupted, stopping the camera and then initializing the camera to default config....
useEffect(() => {
if (isRecording) {
const subscription = AppState.addEventListener(
"change",
async (nextAppState) => {
if (
appStateVar.current.match(/inactive|background/) &&
nextAppState === "active"
) {
setCameraConfigToInitialWhenStopped();
}
// to handle if we are going out of the app...
else if (appStateVar.current.match(/inactive|background/)) {
setCameraConfigToInitialWhenStopped();
} else if (appStateVar.current === "background") {
setCameraConfigToInitialWhenStopped();
}
appStateVar.current = nextAppState;
}
);
return () => {
subscription.remove();
};
}
}, [isRecording]);
const setCameraConfigToInitialWhenStopped = () => {
setFlashMessageModalVisible(true);
setVideoPath(null);
resetTimer();
setIsRecording(false);
};
// if permission is not determinned then move to settings app to allow for permissions.
const openAppSetting = useCallback(async () => {
// Open the custom settings if the app has one
// alert("app setting");
await Linking.openSettings();
}, []);
// functionality when user pressed record button.
const onRecordingStartButtonPressed = async () => {
if (device != null) {
try {
if (cameraRef.current) {
await cameraRef.current.startRecording({
// flash: "on",
onRecordingFinished: (video) => {
if (
appStateVar.current === "background" ||
appStateVar.current === "inactive"
) {
cameraRef?.current?.stopRecording();
resetTimer();
setRecordedVideo(null);
deleteFile(video?.path);
} else {
console.log(video);
setVideoPath(video?.path);
setRecordedVideo(video);
}
},
onRecordingError: (error) => {
if (Platform.OS === "android") {
setIsRecording(false);
cameraRef?.current?.stopRecording();
resetTimer();
} else {
setRecordedVideo(null);
}
console.log(
"ON RECORDING ERROR VIDIASKCAMERA LINE NO 174:",
error
);
// onRecordingStoppedButtonPressed();
},
});
}
} catch (error) {
console.log("on Video Start Recording error:", error);
}
}
};
// functionality when stops recording.
const onRecordingStoppedButtonPressed = async () => {
if (device != null) {
try {
if (cameraRef.current) await cameraRef.current.stopRecording();
} catch (error) {
console.log("On video Stopped recording error", error);
}
}
};
const onCameraFlipButtonPressed = async () => {
if (!onCameraFlip) {
await setOnCameraFlip(true);
try {
if (device != null) {
if (cameraRef.current && isRecording)
await cameraRef.current.pauseRecording();
if (cameraRef.current) await setShowBackCamera(!showBackCamera);
if (cameraRef.current && isRecording)
await cameraRef.current.resumeRecording();
}
} catch (error) {
console.log("On Camera Flip error:", error);
}
await setOnCameraFlip(false);
}
};
const resetTimer = () => {
dispatch(timerReset());
};
// rendering camera option like front/back etc
const renderCameraOptions = () => {
return (
<View style={styles.cameraOptionsContainer}>
<ButtonWithImage
icon={ImageConst.swapCamera}
IconStyle={styles.swapIcon}
onPress={() => {
onCameraFlipButtonPressed();
}}
/>
</View>
);
};
// rendering record camera button
const renderCameraRecordingButton = () => {
return (
<View style={styles.recordButtonContainer}>
<View style={styles.recordButtonWhiteContainer}>
{/* handling to separate actions because it take state to update some time thats why. */}
{!isRecording ? (
<TouchableOpacity
style={styles.recordButtonRedContainer(true)}
onPress={() => {
if (device != null) {
onRecordingStartButtonPressed();
setIsRecording(true);
}
}}
></TouchableOpacity>
) : (
<TouchableOpacity
style={styles.recordButtonRedContainer(false)}
onPress={() => {
if (device != null) {
onRecordingStoppedButtonPressed();
setIsRecording(false);
}
}}
></TouchableOpacity>
)}
</View>
<Text
style={styles.recordTextStyle}
fontFamily={FONT_FAMILY.MEDIUM}
font={APP_FONTS.UBUNTU}
>
{isRecording ? "RECORDING..." : "RECORD"}
</Text>
</View>
);
};
const renderTimerView = () => {
return (
<Timer
recordedVideo={recordedVideo}
isRecording={isRecording}
setIsRecording={setIsRecording}
onRecordingStoppedButtonPressed={onRecordingStoppedButtonPressed}
/>
);
};
// rendering the actual camera view to record viedeos....
const renderCameraView = () => {
return (
<View style={{ flex: 1 }}>
{renderTimerView()}
<Camera
ref={cameraRef}
video={true}
audio={true}
device={device}
isActive={isFocused}
style={{ flex: 1 }}
/>
{isRecording && Platform.OS === "android"
? null
: renderCameraOptions()}
{renderCameraRecordingButton()}
</View>
);
};
const renderOpenSettingsView = () => {
return (
<View style={{ flex: 1 }}>
{cameraPermission === "denied" && micPermission === "denied" ? (
<View style={styles.permissionDeniedContainer}>
<Text>
You have denied permissions to use camera. To continue further,
open the settings and allow the necessary permissions
</Text>
<Button
title="Open Settings"
onPress={openAppSetting}
style={styles.permissionDeniedButton}
textStyle={{ color: ColorConst.whiteColor }}
/>
</View>
) : (
<></>
)}
</View>
);
};
const renderFlashMessageComp = () => {
return (
<View style={styles.centeredView}>
<SafeAreaView>
<Modal
animationType="slide"
transparent={true}
visible={flashMessageModalVisible}
onRequestClose={() => {
Alert.alert("Modal has been closed.");
setModalVisible(!modalVisible);
}}
>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<View
style={{ width: SizeClass.getScreenWidthFromPercentage(60) }}
>
<Text style={styles.modalText}>
Your recording was interrupted, please try again.
</Text>
</View>
<View>
<TouchableOpacity
style={[styles.button, styles.buttonClose]}
onPress={() =>
setFlashMessageModalVisible(!flashMessageModalVisible)
}
>
<Text style={styles.textStyle}>Dismiss</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
</View>
);
};
// if (device == null) return <View></View>;
// if (cameraPermission === "denied" && micPermission === "denied")
// renderOpenSettingsView();
return (
<View style={styles.cameraContainer}>
{cameraPermission === "authorized" && micPermission === "authorized"
? device != null && renderCameraView()
: renderOpenSettingsView()}
{renderFlashMessageComp()}
</View>
);
}
const styles = StyleSheet.create({
cameraContainer: {
flex: 1,
},
cameraOptionsContainer: {
alignItems: "center",
position: "absolute",
top: SizeClass.getScreenHeightFromPercentage(8),
right: SizeClass.getScreenWidthFromPercentage(3),
},
swapIcon: {
width: SizeClass.getScreenWidthFromPercentage(8),
height: SizeClass.getScreenWidthFromPercentage(8),
},
recordButtonContainer: {
position: "absolute",
bottom: SizeClass.getScreenHeightFromPercentage(6),
alignSelf: "center",
alignItems: "center",
},
recordButtonWhiteContainer: {
borderWidth: SizeClass.getScreenWidthFromPercentage(1.2),
borderRadius: SizeClass.getScreenWidthFromPercentage(9),
width: SizeClass.getScreenWidthFromPercentage(18),
height: SizeClass.getScreenWidthFromPercentage(18),
borderColor: ColorConst.white,
width: SizeClass.getScreenWidthFromPercentage(18),
height: SizeClass.getScreenWidthFromPercentage(18),
justifyContent: "center",
},
recordButtonRedContainer: (isRecording) => ({
backgroundColor: ColorConst.redColor,
borderRadius: isRecording
? SizeClass.getScreenWidthFromPercentage(2)
: SizeClass.getScreenWidthFromPercentage(7),
width: isRecording
? SizeClass.getScreenWidthFromPercentage(9)
: SizeClass.getScreenWidthFromPercentage(14),
height: isRecording
? SizeClass.getScreenWidthFromPercentage(9)
: SizeClass.getScreenWidthFromPercentage(14),
position: "absolute",
alignSelf: "center",
}),
recordTextStyle: {
color: ColorConst.white,
fontSize: SizeClass.scaleFont(10),
marginTop: SizeClass.DEFAULT_MARGIN,
},
permissionDeniedButton: {
backgroundColor: ColorConst.black,
width: SizeClass.getScreenWidthFromPercentage(25),
height: SizeClass.getScreenWidthFromPercentage(15),
fontSize: SizeClass.scaleFont(15),
marginVertical: SizeClass.getScreenWidthFromPercentage(10),
borderRadius: SizeClass.scaleFont(12),
},
permissionDeniedContainer: {
flex: 1,
backgroundColor: ColorConst.bgColor,
alignItems: "center",
justifyContent: "center",
},
centeredView: {
flex: 1,
position: "absolute",
alignSelf: "center",
top: 2 * SizeClass.LARGE_MARGIN,
},
modalView: {
width: SizeClass.getScreenWidthFromPercentage(90),
backgroundColor: ColorConst.flashMessageBackgroundColor,
borderRadius: SizeClass.DEFAULT_MARGIN,
padding: SizeClass.LARGE_MARGIN,
flexDirection: "row",
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
button: {
padding: SizeClass.SMALL_MARGIN,
elevation: 2,
},
buttonClose: {},
textStyle: {
color: ColorConst.themeColor,
},
modalText: {
color: ColorConst.white,
},
});
What happened instead?
The app crashes with nil value found in let timestamp = CMSyncConvertTime(CMSampleBufferGetPresentationTimeStamp(sampleBuffer), from: audioCaptureSession.masterClock!, to: captureSession.masterClock!) which is line no 204 of file CameraView+RecordVideo inside the function captureOutput
Relevant log output
VisionCamera/CameraView+RecordVideo.swift:204: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Device
iPhone 12
VisionCamera Version
2.13.2
Additional information
- [ ] I am using Expo
- [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.
Hi, thanks for the report. Is audioCaptureSession.masterClock
nil or is it captureSession.masterClock
?
Hi mrousavy thanks for quick response. "to: captureSession.masterClock!" is nil
@usaidather Hello, I am having the same issue, did you find a solution yet?
Hello, I am also having the same issue
Does https://github.com/mrousavy/react-native-vision-camera/pull/1302 fix your issue?
Does #1302 fix your issue?
I was have the same issue, the PR works
i am also facing same issue