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

🐛 App crashes when switching front/back camera while recording a video.

Open usaidather opened this issue 2 years ago • 3 comments

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

usaidather avatar Apr 20 '22 11:04 usaidather

Hi, thanks for the report. Is audioCaptureSession.masterClock nil or is it captureSession.masterClock?

mrousavy avatar Apr 20 '22 11:04 mrousavy

Hi mrousavy thanks for quick response. "to: captureSession.masterClock!" is nil

usaidather avatar Apr 20 '22 11:04 usaidather

@usaidather Hello, I am having the same issue, did you find a solution yet?

jacquesikot avatar Jul 21 '22 15:07 jacquesikot

Hello, I am also having the same issue

meetkkvelu avatar Oct 21 '22 09:10 meetkkvelu

Does https://github.com/mrousavy/react-native-vision-camera/pull/1302 fix your issue?

mrousavy avatar Oct 24 '22 12:10 mrousavy

Does #1302 fix your issue?

I was have the same issue, the PR works

ridvanaltun avatar Jan 18 '23 01:01 ridvanaltun

i am also facing same issue

meetkkvelu avatar Sep 15 '23 06:09 meetkkvelu