mrousavy / react-native-vision-camera

📸 A powerful, high-performance React Native Camera library.
https://react-native-vision-camera.com
MIT License
7.28k stars 1.07k forks source link

🐛 How can I reduce the size of a video that is 151 MB for a 1-minute recording? The bitRate setting does not seem to be working. #3113

Open vishalyad16 opened 1 month ago

vishalyad16 commented 1 month ago

What's happening?

When recording a 1-minute video, the file size becomes as large as 151 MB. However, using compression packages often results in the video becoming blurry.

Reproduceable Code

import React, { useRef, useState, useEffect, useCallback } from 'react'
import {
    View,
    StyleSheet,
    TouchableOpacity,
    Image,
    AppState,
    BackHandler,
    Alert,
    ToastAndroid
} from 'react-native'
// components
import MyText from '../../components/atoms/MyText';
//packages
import { Camera, useCameraDevice, useCameraFormat, useCameraPermission, useMicrophonePermission } from 'react-native-vision-camera';
import FlipIcon from 'react-native-vector-icons/MaterialIcons'
import { useNavigation } from '@react-navigation/native';
import Icon from 'react-native-vector-icons/Ionicons';
// assets
import colors from '../../assets/colors/colors';
import RecordBtn from '../../assets/images/recordBtn.png';
import PauseBtn from '../../assets/images/PauseBtn.png';
import CustomButton from '../../components/atoms/Button';
import _debounce from 'lodash/debounce';
// style
import styles from './style'

const RecordVideo = ({ route }) => {
    //var
    const { setCustomUrl } = route.params;
    const backCamera = useCameraDevice('back');
    const frontCamera = useCameraDevice('front');

    const format = useCameraFormat(backCamera, [
        { videoResolution: { width: 1280, height: 720 } },
        { fps: 15 },{pixelFormat: 'native'}
        ])
        console.log("format",format);
    //hook:useRef
    const cameraRef = useRef(Camera);
    //hook:navigation
    const navigation = useNavigation()
    // Camera and microphone permissions
    const { hasPermission, requestPermission } = useCameraPermission()
    const { hasPermission: hasMicPermission, requestPermission: requestMicPermission } = useMicrophonePermission()
    useEffect(() => {
        // requestCameraPermission()
        if (Platform.OS === 'ios') {
        hasMicPermission || requestMicPermission()
        }
    }, [])
    //hook:useState
    const [isRecording, setIsRecording] = useState(false)
    const [recordDuration, setRecordDuration] = useState(0)
    const [timerId, setTimerId] = useState(null);
    const [isRecordingPaused, setIsRecordingPaused] = useState(false);
    const [isRecordingCompleted, setisRecordingCompleted] = useState(false);
    const [videoPath, setVideoPath] = useState("");
    const [cameraActive, setCameraActive] = useState(true);
    const [selectedDevice, setSelectedDevice] = useState(backCamera); // State to keep track of selected camera device
    const [isMaxDurationReached, setIsMaxDurationReached] = useState(false);
    //Function:To get camera permission
    // const requestCameraPermission = async () => {
    //     if (Platform.OS === 'android') {
    //         try {
    //             const granted = await PermissionsAndroid.request(
    //                 PermissionsAndroid.PERMISSIONS.CAMERA,
    //                 {
    //                     title: 'Camera Permission',
    //                     message: 'This app needs camera permission to record videos.',
    //                     buttonNeutral: 'Ask Me Later',
    //                     buttonNegative: 'Cancel',
    //                     buttonPositive: 'OK',
    //                 },
    //             );

    //             if (granted === PermissionsAndroid.RESULTS.GRANTED) {
    //                 console.log('Camera permission granted');
    //             } else {
    //                 console.log('Camera permission denied');
    //             }
    //         } catch (err) {
    //             console.warn(err);
    //         }
    //     }
    // };
    //var
    const onStartRecordingDebounced = _debounce(() => startRecording(), 1000, { leading: true, trailing: false });
    //Function:To start recording
    const startRecording = async () => {
        setIsRecording(true);
        setRecordDuration(0)
        try {
            // Update timer every second while recording
            const timerId = setInterval(() => {
                setRecordDuration(prevDuration => {
                    const newDuration = prevDuration + 1;
                    if (newDuration >= 300) { 
                        pauseRecording();
                        setIsMaxDurationReached(true);
                        clearInterval(timerId);
                    }
                    return newDuration;
                });
            }, 1000);
            setTimerId(timerId)
            if (isRecordingPaused) {
                await cameraRef.current.resumeRecording()
                setIsRecordingPaused(false)
            } else {
                await cameraRef.current?.startRecording({
                    flash:'on',
                    bitRate: 400000,
                    quality: 'low',
                    onRecordingFinished: video => {
                        const path = video?.path
                        setVideoPath(path)
                        clearInterval(timerId);
                        setCustomUrl(path)
                    },
                    onRecordingError: error => {
                        setIsRecording(false);
                        console.error('Recording error: ', error);
                        clearInterval(timerId);
                    }
                });
                // Store the timer ID to clear it later
                return () => clearInterval(timerId);
            }
        } catch (err) {
            setIsRecording(false);
            console.error('Failed to start recording: ', err);
        }
    };
    //Function:To stop recording
    const stopRecording = async () => {
        if (recordDuration === 0) {
            setIsRecording(false);
            setIsRecordingPaused(false);
            clearInterval(timerId);
            navigation.goBack();
            ToastAndroid.show('Recording not generated');
            return;
        }
        setisRecordingCompleted(true);
        setIsRecording(false);
        setIsRecordingPaused(false)
        try {
            const video = await cameraRef.current?.stopRecording({});
            console.log('Recorded video: ', video);
            clearInterval(timerId)
        } catch (err) {
            setIsRecording(false);
            setIsRecordingPaused(false)
            console.error('Failed to stop recording: ', err);
        }
    };
    //Function:To close the recording screen
    const handleClose = () => {
        setCustomUrl("");
        navigation.navigate('ProjectTaskTwo')
    };
    // Add event listener for hardware back button press
    useEffect(() => {
        const backAction = () => {
            setCustomUrl("");
            if (isRecording) {
                Alert.alert(
                    'Stop Recording',
                    'Please stop recording before going back.',
                    [
                        {
                            text: 'OK',
                            onPress: () => null,
                        },
                    ],
                    { cancelable: false }
                );
                return true; // Prevent default behavior (exit app)
            } else {
                handleClose(); // Call your close function when back button is pressed
                return true; // Prevent default behavior (exit app)
            }
        };

        const backHandler = BackHandler.addEventListener(
            'hardwareBackPress',
            backAction
        );

        return () => backHandler.remove(); // Remove event listener on component unmount
    }, [isRecording]);
    //Function:To pause recording
    const pauseRecording = async () => {
        try {
            await cameraRef.current.pauseRecording()
            setIsRecordingPaused(true)
            clearInterval(timerId)
        } catch (error) {
            console.log("pauseRecording error", error)
        }
    }
    //Function:To play recording
    const resumeRecording = async () => {
        try {
            await cameraRef.current.resumeRecording();
            setIsRecordingPaused(false);
            // Restart the timer with the time elapsed since pause
            const timerId = setInterval(() => {
                setRecordDuration(prevDuration => prevDuration + 1);
            }, 1000);
            setTimerId(timerId);
        } catch (error) {
            console.log("resumeRecording", error);
            setIsRecording(false);
        }
    }
    //Function:To Flip the camera when it is pressed
    const onFlipCameraPressed = useCallback(() => {
        setCameraActive(false);
    }, [cameraActive]);
    //hook:useEffect
    // useEffect(() => {
    //     if (!cameraActive) {
    //         const newSelectedDevice = selectedDevice.position === "front" ? backCamera : frontCamera;
    //         setSelectedDevice(newSelectedDevice);
    //         setCameraActive(true);
    //     }
    // }, [cameraActive, backCamera, frontCamera, selectedDevice]);
    //hook:useEffect
    useEffect(() => {
        if (!cameraActive) {
            selectedDevice.position === "front"
                ? setSelectedDevice(backCamera)
                : setSelectedDevice(frontCamera);
            setCameraActive(true);
        }
        return () => { };
    }, [cameraActive]);
    //Function:To format the recording duration into minute and seconds
    const formatTime = seconds => {
        const minutes = Math.floor(seconds / 60);
        const remainingSeconds = seconds % 60;
        return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
    }
    //Function:To handle generate uri while on pause video
    const pauseAndGenerateURI = async () => {
        if (isRecording && !isRecordingPaused) {
            await pauseRecording();
            setCustomUrl(videoPath);
        }
    };
    //Function:To delete recording video
    const deleteVideo = async () => {
        try {
            // Stop the recording if it's active
            if (isRecording) {
                await cameraRef.current?.stopRecording({});
            }
        } catch (error) {
            console.error('Failed to stop recording during delete: ', error);
        }
        // Reset the state to the initial stage
        setVideoPath("");
        setCustomUrl("")
        setIsRecording(false);
        setIsRecordingPaused(false);
        setRecordDuration(0);
        clearInterval(timerId);
        setRecordDuration(0);
    }
    //Function:To save recording video
    const saveRecording = () => {
        stopRecording();
        navigation.goBack();
    }
    //hook:useEffect
    useEffect(() => {
        const handleAppStateChange = (newAppState) => {
            if (newAppState === 'active' && isRecording) {
                console.log("Camera active");
                setCameraActive(true);
                pauseAndGenerateURI();
            } else if (newAppState === 'active' && isRecordingPaused) {
                setisRecordingCompleted(true);
                setCameraActive(true);
            } else {
                setCameraActive(true);
            }
        };
        const subscription = AppState.addEventListener('change', handleAppStateChange);
        return () => {
            if (subscription) {
                subscription.remove();
            }
        };
    }, [isRecording, isRecordingPaused]);
    return (
        <View style={styles.container}>
            <Camera
                key={selectedDevice?.id}
                ref={cameraRef}
                device={selectedDevice}
                isActive={cameraActive}
                style={StyleSheet.absoluteFill}
                video={true}
                audio={true}
                format={format}
                torch='off'
                onStarted={() => {
                    console.log('Camera streaming started');
                    setCameraActive(true);
                }}
                onStopped={() => {
                    console.log('Camera streaming stopped');
                    setCameraActive(false);
                }}
            />
            <View style={styles.mainView}>
                {!isRecording && (
                    <TouchableOpacity onPress={handleClose} style={styles.closeButtonConatiner}>
                        <Icon name="close" size={30} color={colors.white} />
                    </TouchableOpacity>
                )}
                <View style={styles.buttonView}>
                    {!isRecording && !isRecordingCompleted &&
                        (<TouchableOpacity onPress={onFlipCameraPressed} style={styles.flipButtonView}>
                            <FlipIcon name='flip-camera-android' size={30} color={colors.white} />
                        </TouchableOpacity>)}
                    {isRecording && !isRecordingCompleted && (
                        <TouchableOpacity style={styles.saveButtonView}>
                            <CustomButton textColor={colors.white} btnStyle={{ width: 85, borderRadius: 40 }} onPress={saveRecording}>Save</CustomButton>
                        </TouchableOpacity>)}
                    {isRecording && (
                        <TouchableOpacity onPress={deleteVideo} style={styles.deleteButtonView}>
                            <FlipIcon name='delete' size={30} color={colors.white} />
                        </TouchableOpacity>
                    )}
                    <View style={styles.timerBtnView}>
                        <MyText text={formatTime(recordDuration)} color={colors.white} fontsize={20} fontWeight='bold' style={{ marginBottom: 10 }} />
                        {!isRecording && !isRecordingCompleted && (<TouchableOpacity onPress={onStartRecordingDebounced}>
                            <Image
                                source={RecordBtn}
                                style={styles.imageStyle}
                                resizeMode='contain'
                            />
                        </TouchableOpacity>)}
                        {isRecording && (
                            <TouchableOpacity onPress={isRecordingPaused ? resumeRecording : pauseRecording} disabled={isMaxDurationReached}>
                                <Image
                                    source={isRecordingPaused ? RecordBtn : PauseBtn}
                                    style={[styles.imageStyle, { width: 60, height: 60 }]}
                                    resizeMode='contain'
                                />
                            </TouchableOpacity>
                        )}
                    </View>
                </View>
            </View>
        </View>
    )
}

export default RecordVideo;

Relevant log output

There is no crash while recording.

Camera Device

{
    "videoStabilizationModes": [
        "off",
        "cinematic"
    ],
    "autoFocusSystem": "contrast-detection",
    "photoWidth": 4000,
    "supportsPhotoHdr": false,
    "supportsDepthCapture": false,
    "maxISO": 6400,
    "minISO": 100,
    "minFps": 7,
    "videoWidth": 1280,
    "supportsVideoHdr": false,
    "videoHeight": 720,
    "fieldOfView": 84.9812741473671,
    "maxFps": 30,
    "photoHeight": 1800
}

Device

Realme ,OnePlus

VisionCamera Version

"react-native-vision-camera": "^4.4.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

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.

alextusinean commented 1 month ago

This is not an issue with the library. 151mb isn't that odd for a one minute video at 1280x720 15fps. If you'd like to reduce the size even more, start working on video compression algorithms.

iamvishaldev commented 1 month ago

This is not an issue with the library. 151mb isn't that odd for a one minute video at 1280x720 15fps. If you'd like to reduce the size even more, start working on video compression algorithms.

Can you send me some examples for compression algorithms and I don't want to record at 1280×720 it can record 480 also

alextusinean commented 1 month ago

This is not an issue with the library. 151mb isn't that odd for a one minute video at 1280x720 15fps. If you'd like to reduce the size even more, start working on video compression algorithms.

Can you send me some examples for compression algorithms and I don't want to record at 1280×720 it can record 480 also

I would like to but I don't know about any better than mp4. Try recording at 480 then and see if it improves.

zzz08900 commented 2 weeks ago

One thing with video compression algorithm is: Fast encoding + high quality = large file size Small file size + high quality = (really) slow encoding, and usually resulting in (very) hard to decode video files Fast encoding + small file size = lousy quality

take your pick then.

zzz08900 commented 2 weeks ago

If you think you can outsmart VCEG with a few days of work and come with a much better video compression algorithm than H264/265, well, good luck then.