mrousavy / react-native-vision-camera

šŸ“ø A powerful, high-performance React Native Camera library.
https://react-native-vision-camera.com
MIT License
7.18k stars 1.05k forks source link

šŸ› Android Crash on video recording without using frame processor/react-native-worklets-core #3085

Closed amosaxe closed 1 month ago

amosaxe commented 1 month ago

What's happening?

I wanted to take a video, but it crashes after video is recorded and previews start. It crashes anywhere during the preview screen or gets stuck. I have tried on two devices, oneplus 6T and motorolla g60. This only happens with back camera.

Reproduceable Code

import { useFocusEffect, useNavigation } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
    ActivityIndicator,
    Dimensions,
    Image,
    Platform,
    StyleSheet,
    TouchableOpacity,
    View,
} from 'react-native';
import Video from 'react-native-video';
import {
    Camera,
    useCameraDevice,
    useCameraFormat,
    useCameraPermission,
    useMicrophonePermission,
} from 'react-native-vision-camera';
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome';
import {
    faCameraRotate,
    faMoon,
    faBolt,
    faChevronLeft,
} from '@fortawesome/free-solid-svg-icons';
import { PressableOpacity } from 'react-native-pressable-opacity';
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets';
import { CaptureButton } from './CaptureButton';
import Reanimated, {
    Extrapolate,
    interpolate,
    useAnimatedGestureHandler,
    useAnimatedProps,
    useSharedValue,
} from 'react-native-reanimated';
import {
    PinchGestureHandler,
    TapGestureHandler,
} from 'react-native-gesture-handler';
import { dynamicSize } from '../../utils/responsive';
import {
    getMediaDataObjectFromCropper,
    getMediaDataObjectFromVisionCamera,
} from '../../utils/mediaFormatter';
import { CheckIcon } from 'react-native-heroicons/solid';
import { colors } from '../../styles/colors';
import ImagePicker from 'react-native-image-crop-picker';

const CONTROL_BUTTON_SIZE = 40;
const CONTENT_SPACING = 15;
const MAX_ZOOM_FACTOR = 10;
const SCALE_FULL_ZOOM = 3;
const CAPTURE_BUTTON_SIZE = 78;
const BORDER_WIDTH = CAPTURE_BUTTON_SIZE * 0.1;

export const SCREEN_WIDTH = Dimensions.get('window').width;
export const SCREEN_HEIGHT = Platform.select({
    android:
        Dimensions.get('screen').height -
        StaticSafeAreaInsets.safeAreaInsetsBottom,
    ios: Dimensions.get('window').height,
});

const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera);
Reanimated.addWhitelistedNativeProps({
    zoom: true,
});

const CameraScreen = props => {
    const {
        mediaType = 'any',
        navigateTo = 'OnboardingScreen',
        isCroppingEnabled = false,
        height = SCREEN_WIDTH,
        width = SCREEN_WIDTH,
        cropperCircleOverlay = true,
    } = props?.route?.params;
    const navigation = useNavigation();
    const { hasPermission, requestPermission } = useCameraPermission();
    const {
        hasPermission: microphonePermission,
        requestPermission: requestMicrophonePermission,
    } = useMicrophonePermission();

    const [isActive, setIsActive] = useState(false);
    const [isRecording, setIsRecording] = useState(false);
    const [cameraPosition, setCameraPosition] = useState('back');
    const [targetFps, setTargetFps] = useState(60);
    const [photo, setPhoto] = useState();
    const [video, setVideo] = useState();
    const [enableHdr, setEnableHdr] = useState(false);
    const [flash, setFlash] = useState('off');
    const [enableNightMode, setEnableNightMode] = useState(false);
    const isPressingButton = useSharedValue(false);
    const zoom = useSharedValue(1);
    const [isCameraInitialized, setIsCameraInitialized] = useState(false);
    const [mediaObj, setMediaObj] = useState(null);
    const screenAspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH;

    const device = useCameraDevice(cameraPosition, {
        physicalDevices: ['ultra-wide-angle-camera'],
    });

    const format = useCameraFormat(device, [
        { fps: targetFps },
        { videoAspectRatio: screenAspectRatio },
        { videoResolution: 'max' },
        { photoAspectRatio: screenAspectRatio },
        { photoResolution: 'max' },
    ]);

    const fps = Math.min(format?.maxFps ?? 1, targetFps);
    const camera = useRef(null);
    const supportsFlash = device?.hasFlash ?? false;
    const supportsHdr = format?.supportsPhotoHdr;
    const videoHdr = format?.supportsVideoHdr && enableHdr;
    const photoHdr = format?.supportsPhotoHdr && enableHdr && !videoHdr;
    const canToggleNightMode = device?.supportsLowLightBoost ?? false;
    const minZoom = device?.minZoom ?? 1;
    const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR);

    const cameraAnimatedProps = useAnimatedProps(() => {
        const z = Math.max(Math.min(zoom.value, maxZoom), minZoom);
        return {
            zoom: z,
        };
    }, [maxZoom, minZoom, zoom]);

    useFocusEffect(
        useCallback(() => {
            setIsActive(true);
            return () => {
                setIsActive(false);
            };
        }, []),
    );

    useEffect(() => {
        if (!hasPermission) {
            requestPermission();
        }

        if (!microphonePermission) {
            requestMicrophonePermission();
        }
    }, [hasPermission, microphonePermission]);

    useEffect(() => {
        // Reset zoom to it's default everytime the `device` changes.
        zoom.value = device?.neutralZoom ?? 1;
    }, [zoom, device]);

    const onInitialized = useCallback(() => {
        console.log('Camera initialized!');
        setIsCameraInitialized(true);
    }, []);

    const onPhotoCaptured = async picture => {
        if (picture) {
            const mediaObj = getMediaDataObjectFromVisionCamera({
                ...picture,
                mediaType: 'image/jpeg',
            });

            console.log('in onPhotoCaptured');

            if (isCroppingEnabled) {
                await ImagePicker.openCropper({
                    path: mediaObj.uri,
                    width: width,
                    height: height,
                    cropperCircleOverlay: cropperCircleOverlay,
                    cropperActiveWidgetColor: colors.CHERRY_RED,
                    cropperToolbarColor: colors.BLACK,
                    cropperToolbarWidgetColor: colors.WHITE,
                    cropperStatusBarColor: colors.BLACK,
                })
                    .then(image => {
                        const cropedImage =
                            getMediaDataObjectFromCropper(image);
                        console.log('cropedImage', cropedImage);
                        setPhoto(cropedImage);
                        setMediaObj(cropedImage);
                    })
                    .catch(err => {});
            } else {
                console.log('mediaObj', mediaObj);
                setPhoto(mediaObj);
                setMediaObj(mediaObj);
            }
        }
    };

    const onVideoCaptured = media => {
        if (media) {
            console.log('in onVideoCaptured', media);

            const mediaObj = getMediaDataObjectFromVisionCamera({
                ...media,
                mediaType: 'video/mov',
            });
            console.log('in onVideoCaptured mediaObj', mediaObj);
            setVideo(mediaObj);
            setMediaObj(mediaObj);
        }
    };

    const onGoBack = () => {
        setPhoto(undefined);
        setVideo(undefined);
    };

    const onFlipCameraPressed = useCallback(() => {
        setCameraPosition(p => (p === 'back' ? 'front' : 'back'));
    }, []);

    const onFlashPressed = useCallback(() => {
        setFlash(f => (f === 'off' ? 'on' : 'off'));
    }, []);

    const onDoubleTap = useCallback(() => {
        onFlipCameraPressed();
    }, [onFlipCameraPressed]);

    const setIsPressingButton = useCallback(
        _isPressingButton => {
            isPressingButton.value = _isPressingButton;
        },
        [isPressingButton],
    );

    const onPinchGesture = useAnimatedGestureHandler({
        onStart: (_, context) => {
            context.startZoom = zoom.value;
        },
        onActive: (event, context) => {
            // we're trying to map the scale gesture to a linear zoom here
            const startZoom = context.startZoom ?? 0;
            const scale = interpolate(
                event.scale,
                [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM],
                [-1, 0, 1],
                Extrapolate.CLAMP,
            );
            zoom.value = interpolate(
                scale,
                [-1, 0, 1],
                [minZoom, startZoom, maxZoom],
                Extrapolate.CLAMP,
            );
        },
    });

    const onFocusTap = useCallback(
        ({ nativeEvent: event }) => {
            if (!device?.supportsFocus) return;
            camera.current?.focus({
                x: event.locationX,
                y: event.locationY,
            });
        },
        [device?.supportsFocus],
    );

    const onSaveMedia = () => {
        navigation?.navigate(navigateTo, { media: mediaObj });
    };

    const onNavigateBack = () => {
        navigation?.goBack();
    };

    if (!hasPermission || !microphonePermission) {
        return <ActivityIndicator />;
    }

    if (!device) {
        return <Text>Camera device not found</Text>;
    }

    return (
        <View style={{ flex: 1 }}>
            <PinchGestureHandler
                onGestureEvent={onPinchGesture}
                enabled={isActive && !photo && !video}>
                <Reanimated.View
                    onTouchEnd={onFocusTap}
                    style={[
                        StyleSheet.absoluteFill,
                        (video?.uri || photo?.uri) && { opacity: 0 },
                    ]}>
                    <TapGestureHandler onEnded={onDoubleTap} numberOfTaps={2}>
                        <ReanimatedCamera
                            ref={camera}
                            style={StyleSheet.absoluteFill}
                            device={device}
                            isActive={isActive && !photo && !video}
                            photo={mediaType == 'any' || mediaType == 'image'}
                            video={mediaType == 'any' || mediaType == 'video'}
                            audio
                            photoHdr={photoHdr}
                            videoHdr={videoHdr}
                            format={format}
                            fps={fps}
                            photoQualityBalance="quality"
                            onInitialized={onInitialized}
                            enableZoomGesture={false}
                            orientation={'portrait'}
                            animatedProps={cameraAnimatedProps}
                        />
                    </TapGestureHandler>
                </Reanimated.View>
            </PinchGestureHandler>

            {video?.uri && (
                <View style={styles.imagePreview}>
                    <Video
                        style={StyleSheet.absoluteFill}
                        source={{
                            uri: video?.uri,
                        }}
                        repeat
                    />
                    <View style={styles.rightButtonRow}>
                        <View>
                            <PressableOpacity
                                style={styles.button}
                                onPress={onGoBack}
                                disabledOpacity={0.4}>
                                <FontAwesomeIcon
                                    icon={faChevronLeft}
                                    color="white"
                                    size={20}
                                />
                            </PressableOpacity>
                        </View>
                    </View>
                </View>
            )}

            {photo?.uri && (
                <View style={styles.imagePreview}>
                    {photo?.uri && (
                        <Image
                            source={{ uri: photo?.uri }}
                            // style={StyleSheet.absoluteFill}
                            resizeMode="cover"
                            style={{
                                width: width,
                                height: height,
                            }}
                        />
                    )}
                    {console.log('photo?.uri', photo?.uri)}
                    <View style={styles.rightButtonRow}>
                        <View>
                            <PressableOpacity
                                style={styles.button}
                                onPress={onGoBack}
                                disabledOpacity={0.4}>
                                <FontAwesomeIcon
                                    icon={faChevronLeft}
                                    color="white"
                                    size={20}
                                />
                            </PressableOpacity>
                        </View>
                    </View>
                </View>
            )}

            {(photo?.uri || video?.uri) && (
                <View style={styles.checkContainer}>
                    <PressableOpacity
                        onPress={onSaveMedia}
                        style={styles.checkIcon}>
                        <CheckIcon
                            color={colors.BLACK}
                            strokeWidth={2}
                            size={30}
                        />
                    </PressableOpacity>
                </View>
            )}

            {!photo?.uri && !video?.uri && (
                <>
                    <View style={styles.rightButtonRow}>
                        <View>
                            <PressableOpacity
                                style={styles.button}
                                onPress={onNavigateBack}
                                disabledOpacity={0.4}>
                                <FontAwesomeIcon
                                    icon={faChevronLeft}
                                    color="white"
                                    size={20}
                                />
                            </PressableOpacity>
                        </View>

                        <View>
                            <PressableOpacity
                                style={styles.button}
                                onPress={onFlipCameraPressed}
                                disabledOpacity={0.4}>
                                <FontAwesomeIcon
                                    icon={faCameraRotate}
                                    color="white"
                                    size={24}
                                />
                            </PressableOpacity>
                            {supportsFlash && (
                                <PressableOpacity
                                    style={[
                                        styles.button,
                                        {
                                            backgroundColor:
                                                flash == 'off'
                                                    ? 'rgba(140, 140, 140, 0.3)'
                                                    : 'white',
                                        },
                                    ]}
                                    onPress={onFlashPressed}
                                    disabledOpacity={0.4}>
                                    <FontAwesomeIcon
                                        icon={faBolt}
                                        color={
                                            flash == 'off' ? 'white' : 'black'
                                        }
                                        size={24}
                                    />
                                </PressableOpacity>
                            )}

                            {supportsHdr && (
                                <PressableOpacity
                                    style={[
                                        styles.button,
                                        {
                                            backgroundColor: !enableHdr
                                                ? 'rgba(140, 140, 140, 0.3)'
                                                : 'white',
                                        },
                                    ]}
                                    onPress={() => setEnableHdr(h => !h)}>
                                    <Text
                                        style={{
                                            color: !enableHdr
                                                ? 'white'
                                                : 'black',
                                        }}>
                                        HDR
                                    </Text>
                                </PressableOpacity>
                            )}
                            {canToggleNightMode && (
                                <PressableOpacity
                                    style={[
                                        styles.button,
                                        {
                                            backgroundColor: !enableNightMode
                                                ? 'rgba(140, 140, 140, 0.3)'
                                                : colors.WHITE,
                                        },
                                    ]}
                                    onPress={() =>
                                        setEnableNightMode(!enableNightMode)
                                    }
                                    disabledOpacity={0.4}>
                                    <FontAwesomeIcon
                                        icon={faMoon}
                                        color={
                                            !enableNightMode
                                                ? colors.WHITE
                                                : colors.BLACK
                                        }
                                        size={24}
                                    />
                                </PressableOpacity>
                            )}
                        </View>
                    </View>
                    {mediaType == 'image' ? (
                        <TouchableOpacity
                            style={styles.buttonContainer}
                            onPress={async () => {
                                console.log('in onPress');
                                if (camera?.current == null) return;
                                const photo = await camera?.current?.takePhoto({
                                    flash: flash,
                                    enableShutterSound: false,
                                });
                                // console.log('photo', photo);
                                onPhotoCaptured(photo);
                            }}>
                            <View style={styles.captureButton} />
                        </TouchableOpacity>
                    ) : (
                        <View style={styles.captureButtonContainer}>
                            <CaptureButton
                                style={styles.captureButton2}
                                camera={camera}
                                onPhotoCaptured={onPhotoCaptured}
                                mediaType={mediaType}
                                onVideoCaptured={onVideoCaptured}
                                cameraZoom={zoom}
                                minZoom={minZoom}
                                maxZoom={maxZoom}
                                flash={supportsFlash ? flash : 'off'}
                                enabled={isCameraInitialized && isActive}
                                setIsPressingButton={setIsPressingButton}
                            />
                        </View>
                    )}
                </>
            )}
        </View>
    );
};

const styles = StyleSheet.create({
    button: {
        marginBottom: CONTENT_SPACING,
        width: CONTROL_BUTTON_SIZE,
        height: CONTROL_BUTTON_SIZE,
        borderRadius: CONTROL_BUTTON_SIZE / 2,
        backgroundColor: 'rgba(140, 140, 140, 0.3)',
        justifyContent: 'center',
        alignItems: 'center',
    },
    rightButtonRow: {
        position: 'absolute',
        top: 20,
        flexDirection: 'row',
        justifyContent: 'space-between',
        width: '100%',
        paddingHorizontal: 20,
    },
    captureButtonContainer: {
        flex: 1,
        position: 'absolute',
        bottom: dynamicSize(25),
        width: SCREEN_WIDTH,
        justifyContent: 'center',
        alignItems: 'center',
    },
    captureButton: {
        width: CAPTURE_BUTTON_SIZE,
        height: CAPTURE_BUTTON_SIZE,
        borderRadius: CAPTURE_BUTTON_SIZE / 2,
        borderWidth: BORDER_WIDTH,
        borderColor: 'white',
        backgroundColor: colors.BLUE_DARK,
    },
    captureButton2: {
        width: CAPTURE_BUTTON_SIZE,
        height: CAPTURE_BUTTON_SIZE,
        // borderRadius: CAPTURE_BUTTON_SIZE / 2,
        // borderWidth: BORDER_WIDTH,
        // borderColor: 'white',
        // backgroundColor: colors.BLUE_DARK,
    },
    buttonContainer: {
        position: 'absolute',
        bottom: dynamicSize(25),
        justifyContent: 'center',
        alignItems: 'center',
        flex: 1,
        width: SCREEN_WIDTH,
    },
    checkIcon: {
        backgroundColor: colors.WHITE,
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: dynamicSize(30),
        height: dynamicSize(50),
        width: dynamicSize(50),
    },
    checkContainer: {
        position: 'absolute',
        right: dynamicSize(30),
        bottom: dynamicSize(30),
    },
    imagePreview: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default CameraScreen;

import React, { useCallback, useRef } from 'react';
import { Dimensions, Platform, StyleSheet, View } from 'react-native';
import {
    PanGestureHandler,
    State,
    TapGestureHandler,
} from 'react-native-gesture-handler';
import Reanimated, {
    cancelAnimation,
    Easing,
    Extrapolate,
    interpolate,
    useAnimatedStyle,
    withSpring,
    withTiming,
    useAnimatedGestureHandler,
    useSharedValue,
    withRepeat,
} from 'react-native-reanimated';
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets';
import { colors } from '../../styles/colors';

const CAPTURE_BUTTON_SIZE = 78;
export const SCREEN_WIDTH = Dimensions.get('window').width;
export const SCREEN_HEIGHT = Platform.select({
    android:
        Dimensions.get('screen').height -
        StaticSafeAreaInsets.safeAreaInsetsBottom,
    ios: Dimensions.get('window').height,
});

const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH];
const PAN_GESTURE_HANDLER_ACTIVE_Y = [-2, 2];

const START_RECORDING_DELAY = 200;
const BORDER_WIDTH = CAPTURE_BUTTON_SIZE * 0.1;

const _CaptureButton = ({
    camera,
    onPhotoCaptured,
    onVideoCaptured,
    minZoom,
    maxZoom,
    cameraZoom,
    flash,
    enabled,
    setIsPressingButton,
    style,
    ...props
}) => {
    const pressDownDate = useRef(undefined);
    const isRecording = useRef(false);
    const recordingProgress = useSharedValue(0);
    const isPressingButton = useSharedValue(false);

    //#region Camera Capture
    const takePhoto = useCallback(async () => {
        try {
            if (camera?.current == null) throw new Error('Camera ref is null!');

            console.log('Taking photo...');
            const photo = await camera?.current?.takePhoto({
                flash: flash,
                enableShutterSound: false,
            });
            onVideoCaptured(undefined);
            console.log('photot', photo?.path);
            onPhotoCaptured(photo);
        } catch (e) {
            console.error('Failed to take photo!', e);
        }
    }, [camera, flash, onPhotoCaptured, onVideoCaptured]);

    const onStoppedRecording = useCallback(() => {
        isRecording.current = false;
        cancelAnimation(recordingProgress);
        console.log('stopped recording video!');
    }, [recordingProgress]);
    const stopRecording = useCallback(async () => {
        try {
            if (camera.current == null) throw new Error('Camera ref is null!');

            console.log('calling stopRecording()...');
            await camera.current.stopRecording();
            console.log('called stopRecording()!');
        } catch (e) {
            console.error('failed to stop recording!', e);
        }
    }, [camera]);
    const startRecording = useCallback(() => {
        try {
            if (camera.current == null) throw new Error('Camera ref is null!');

            console.log('calling startRecording()...');
            camera.current.startRecording({
                flash: flash,
                onRecordingError: error => {
                    console.error('Recording failed!', error);
                    onStoppedRecording();
                },
                onRecordingFinished: video => {
                    console.log(
                        `Recording successfully finished! ${video.path}`,
                    );
                    onPhotoCaptured(undefined);
                    onVideoCaptured(video);
                    onStoppedRecording();
                },
            });
            // TODO: wait until startRecording returns to actually find out if the recording has successfully started

            isRecording.current = true;
        } catch (e) {
            console.error('failed to start recording!', e, 'camera');
        }
    }, [camera, flash, onVideoCaptured, onPhotoCaptured, onStoppedRecording]);
    //#endregion

    //#region Tap handler
    const tapHandler = useRef();
    const onHandlerStateChanged = useCallback(
        async ({ nativeEvent: event }) => {
            // This is the gesture handler for the circular "shutter" button.
            // Once the finger touches the button (State.BEGAN), a photo is being taken and "capture mode" is entered. (disabled tab bar)
            // Also, we set `pressDownDate` to the time of the press down event, and start a 200ms timeout. If the `pressDownDate` hasn't changed
            // after the 200ms, the user is still holding down the "shutter" button. In that case, we start recording.
            //
            // Once the finger releases the button (State.END/FAILED/CANCELLED), we leave "capture mode" (enable tab bar) and check the `pressDownDate`,
            // if `pressDownDate` was less than 200ms ago, we know that the intention of the user is to take a photo. We check the `takePhotoPromise` if
            // there already is an ongoing (or already resolved) takePhoto() call (remember that we called takePhoto() when the user pressed down), and
            // if yes, use that. If no, we just try calling takePhoto() again
            console.debug(`state: ${Object.keys(State)[event.state]}`);
            switch (event.state) {
                case State.BEGAN: {
                    // enter "recording mode"
                    recordingProgress.value = 0;
                    isPressingButton.value = true;
                    const now = new Date();
                    pressDownDate.current = now;
                    setTimeout(() => {
                        if (pressDownDate.current === now) {
                            // user is still pressing down after 200ms, so his intention is to create a video
                            startRecording();
                        }
                    }, START_RECORDING_DELAY);
                    setIsPressingButton(true);
                    return;
                }
                case State.END:
                case State.FAILED:
                case State.CANCELLED: {
                    // exit "recording mode"
                    try {
                        if (pressDownDate.current == null)
                            throw new Error(
                                'PressDownDate ref .current was null!',
                            );
                        const now = new Date();
                        const diff =
                            now.getTime() - pressDownDate.current.getTime();
                        pressDownDate.current = undefined;
                        if (diff < START_RECORDING_DELAY) {
                            // user has released the button within 200ms, so his intention is to take a single picture.
                            await takePhoto();
                        } else {
                            // user has held the button for more than 200ms, so he has been recording this entire time.
                            await stopRecording();
                        }
                    } finally {
                        setTimeout(() => {
                            isPressingButton.value = false;
                            setIsPressingButton(false);
                        }, 500);
                    }
                    return;
                }
                default:
                    break;
            }
        },
        [
            isPressingButton,
            recordingProgress,
            setIsPressingButton,
            startRecording,
            stopRecording,
            takePhoto,
        ],
    );
    //#endregion
    //#region Pan handler
    const panHandler = useRef();
    const onPanGestureEvent = useAnimatedGestureHandler({
        onStart: (event, context) => {
            context.startY = event.absoluteY;
            const yForFullZoom = context.startY * 0.7;
            const offsetYForFullZoom = context.startY - yForFullZoom;

            // extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
            context.offsetY = interpolate(
                cameraZoom.value,
                [minZoom, maxZoom],
                [0, offsetYForFullZoom],
                Extrapolate.CLAMP,
            );
        },
        onActive: (event, context) => {
            const offset = context.offsetY ?? 0;
            const startY = context.startY ?? SCREEN_HEIGHT;
            const yForFullZoom = startY * 0.7;

            cameraZoom.value = interpolate(
                event.absoluteY - offset,
                [yForFullZoom, startY],
                [maxZoom, minZoom],
                Extrapolate.CLAMP,
            );
        },
    });
    //#endregion

    const shadowStyle = useAnimatedStyle(
        () => ({
            transform: [
                {
                    scale: withSpring(isPressingButton.value ? 1 : 0, {
                        mass: 1,
                        damping: 35,
                        stiffness: 300,
                    }),
                },
            ],
        }),
        [isPressingButton],
    );
    const buttonStyle = useAnimatedStyle(() => {
        let scale;
        if (enabled) {
            if (isPressingButton.value) {
                scale = withRepeat(
                    withSpring(1, {
                        stiffness: 100,
                        damping: 1000,
                    }),
                    -1,
                    true,
                );
            } else {
                scale = withSpring(0.9, {
                    stiffness: 500,
                    damping: 300,
                });
            }
        } else {
            scale = withSpring(0.6, {
                stiffness: 500,
                damping: 300,
            });
        }

        return {
            opacity: withTiming(enabled ? 1 : 0.3, {
                duration: 100,
                easing: Easing.linear,
            }),
            transform: [
                {
                    scale: scale,
                },
            ],
        };
    }, [enabled, isPressingButton]);

    return (
        <TapGestureHandler
            enabled={enabled}
            ref={tapHandler}
            onHandlerStateChange={onHandlerStateChanged}
            shouldCancelWhenOutside={false}
            maxDurationMs={99999999} // <-- this prevents the TapGestureHandler from going to State.FAILED when the user moves his finger outside of the child view (to zoom)
            simultaneousHandlers={panHandler}>
            <Reanimated.View {...props} style={[buttonStyle, style]}>
                <PanGestureHandler
                    enabled={enabled}
                    ref={panHandler}
                    failOffsetX={PAN_GESTURE_HANDLER_FAIL_X}
                    activeOffsetY={PAN_GESTURE_HANDLER_ACTIVE_Y}
                    onGestureEvent={onPanGestureEvent}
                    simultaneousHandlers={tapHandler}>
                    <Reanimated.View style={styles.flex}>
                        <Reanimated.View style={[styles.shadow, shadowStyle]} />
                        <View style={styles.button} />
                    </Reanimated.View>
                </PanGestureHandler>
            </Reanimated.View>
        </TapGestureHandler>
    );
};

export const CaptureButton = React.memo(_CaptureButton);

const styles = StyleSheet.create({
    flex: {
        flex: 1,
    },
    shadow: {
        position: 'absolute',
        width: CAPTURE_BUTTON_SIZE,
        height: CAPTURE_BUTTON_SIZE,
        borderRadius: CAPTURE_BUTTON_SIZE / 2,
        backgroundColor: '#e34077',
    },
    button: {
        width: CAPTURE_BUTTON_SIZE,
        height: CAPTURE_BUTTON_SIZE,
        borderRadius: CAPTURE_BUTTON_SIZE / 2,
        borderWidth: BORDER_WIDTH,
        borderColor: 'white',
        // backgroundColor: colors.CHERRY_RED,
    },
});

Relevant log output

FATAL EXCEPTION: main
Process: com.froker, PID: 27753
java.lang.OutOfMemoryError: Failed to allocate a 32 byte allocation with 16056 free bytes and 15KB until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC.
    at android.view.DisplayEventReceiver.dispatchVsync(DisplayEventReceiver.java:260)
    at android.os.MessageQueue.nativePollOnce(Native Method)
    at android.os.MessageQueue.next(MessageQueue.java:342)
    at android.os.Looper.loopOnce(Looper.java:182)
    at android.os.Looper.loop(Looper.java:357)
    at android.app.ActivityThread.main(ActivityThread.java:8090)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1026)

FATAL EXCEPTION: Binder:27753_1
Process: com.froker, PID: 27753
java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying to throw an exception; no stack trace available

FATAL EXCEPTION: OkHttp Dispatcher
Process: com.froker, PID: 27753
java.lang.OutOfMemoryError: OutOfMemoryError thrown while trying to throw OutOfMemoryError; no stack trace available

FATAL EXCEPTION: Crashlytics Exception Handler1
Process: com.froker, PID: 27753
java.lang.OutOfMemoryError: Failed to allocate a 32 byte allocation with 848 free bytes and 848B until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC.
    at dalvik.system.VMStack.getThreadStackTrace(Native Method)
    at java.lang.Thread.getStackTrace(Thread.java:1841)
    at java.lang.Thread.getAllStackTraces(Thread.java:1909)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsReportDataCapture.populateThreadsList(CrashlyticsReportDataCapture.java:343)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsReportDataCapture.populateExecutionData(CrashlyticsReportDataCapture.java:314)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsReportDataCapture.populateEventApplicationData(CrashlyticsReportDataCapture.java:261)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsReportDataCapture.captureEventData(CrashlyticsReportDataCapture.java:112)
    at com.google.firebase.crashlytics.internal.common.SessionReportingCoordinator.persistEvent(SessionReportingCoordinator.java:334)
    at com.google.firebase.crashlytics.internal.common.SessionReportingCoordinator.persistFatalEvent(SessionReportingCoordinator.java:131)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsController$2.call(CrashlyticsController.java:214)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsController$2.call(CrashlyticsController.java:199)
    at com.google.firebase.crashlytics.internal.common.CrashlyticsBackgroundWorker$3.then(CrashlyticsBackgroundWorker.java:105)
    at com.google.android.gms.tasks.zze.run(com.google.android.gms:play-services-tasks@@18.1.0:1)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
    at com.google.firebase.crashlytics.internal.common.ExecutorUtils$1$1.onRun(ExecutorUtils.java:67)
    at com.google.firebase.crashlytics.internal.common.BackgroundPriorityRunnable.run(BackgroundPriorityRunnable.java:27)
    at java.lang.Thread.run(Thread.java:1012)

Camera Device

{
  "formats": [],
  "sensorOrientation": "landscape-left",
  "hardwareLevel": "full",
  "maxZoom": 8,
  "minZoom": 1,
  "maxExposure": 12,
  "supportsLowLightBoost": false,
  "neutralZoom": 1,
  "physicalDevices": [
    "wide-angle-camera"
  ],
  "supportsFocus": true,
  "supportsRawCapture": false,
  "isMultiCam": false,
  "minFocusDistance": 10,
  "minExposure": -12,
  "name": "0 (BACK) androidx.camera.camera2",
  "hasFlash": true,
  "hasTorch": true,
  "position": "back",
  "id": "0"
}

Device

motorolla g60

VisionCamera Version

4.3.2

Can you reproduce this issue in the VisionCamera Example app?

No, I cannot reproduce the issue in the Example app

Additional information

maintenance-hans[bot] commented 1 month ago

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.

amosaxe commented 1 month ago

I even tried the exact same code of example app by migrating(copy pasting) on my app and it still crashes, but if I run the example app it works fine.

I have only removed the frame processor while migrating the example app code.

I also looked into android studio logs(logcat). The above logs i had attached from flipper crash reporter

in the logcat i feel this is the main issue:

Unable to update properties for view tag 65 com.facebook.react.uimanager.IllegalViewOperationException: ViewManager for tag 65 could not be found.

this error I saw that is an open issue and was marked as close as well after v4. Any suggestions what to do?

amosaxe commented 1 month ago

update: finally it works, to have a complete copy I even added react-native-worklets-core and a frame processor and now it works, not sure what was the reason, still eager to know so keeping this open

Dingenis commented 1 month ago

You are running out of memory, that is what the log is trying to tell you. So it might be that you have a memory leak somewhere, you could try to profile it using the Android Studio Memory Profiler. I don't think it's related to this library, otherwise the example app would also crash for that very reason. Maybe you could try to find the source of the memory usage?

amosaxe commented 1 month ago

Yep, I honestly dont blame the library, and will put more details after profiling. Will test once if example also crash if i remove the worklets and update here.

mrousavy commented 1 month ago

Hey - to be honest this does not sound like a VisionCamera issue, if it works fine in Example but crashes with your code (and the Camera props remain mostly the same) it is very likely something else.

Maybe some other library is allocating a ton of stuff, maybe you have an endless loop allocating JSI Values - I don't know but it doesn't sound VisionCamera related.

mrousavy commented 1 month ago

Let me know what you find out after profiling - worst case we can just reopen this issue :)