-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Open
Labels
🐛 bugSomething isn't workingSomething isn't working
Description
What's happening?
Currently users on some certain android device get blank videos after recording a video from the camera using react-native-vision-camera.
the videos was made the log from sentry shows a valid file path and dimensions, but the video is blank
device experincing the crash:
Redmi Note 12 (6gb/128gb)
Reproduceable Code
import {
Alert,
Dimensions,
Image,
Linking,
Platform,
Pressable,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {
Camera,
CameraCaptureError,
TakePhotoOptions,
Templates,
useCameraDevice,
useCameraFormat,
useCameraPermission,
useMicrophonePermission,
} from 'react-native-vision-camera';
import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
import {useAppState} from '@react-native-community/hooks';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {runOnJS} from 'react-native-reanimated';
import {Point} from 'react-native-gesture-handler/lib/typescript/web/interfaces';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {getPermission} from '@utils/getPermission';
import AppText from '@components/app-text';
import {logThis} from '@utils/logThis';
import {luupliGreenLightColor} from '@constants/colors';
import {useRunAfterInteraction} from '@hooks/useRunAfterInteraction';
import {openLinkInDefaultApp} from '@utils/device';
import {Button} from '@components/Button';
import CircularProgressWithImage from './components/circularProgressWithImage';
import {Camera as CameraSvgIcon, CloseIcon, Upload, Video} from '@assets/svg';
import {ScreenWrapper} from '@components/ScreenWrapper';
import {
CameraIcon,
CameraSwitcherIcon,
CancelIcon,
CircledUploadIcon,
FlashIcon,
} from '@assets/svg/svgComponents';
import {colors} from '@resources/colors';
import {launchImageLibrary} from 'react-native-image-picker';
import {checkFileType, testlogger} from '@utils/helper';
import {useAddPostContext} from './Model/AddPostContextProvider';
import useMusicTrackPlayerForSingleSong from '@hooks/useMusicTrackPlayer';
import TrackPlayer from 'react-native-track-player';
import {SCREEN_HEIGHT} from '@resources/config';
import {luupliMaxVideoDuration} from '@constants/staticData';
import AlertModal from '@components/alert-modal';
import useWeeklyChallengeHandler from '@hooks/useWeeklyChallengeHandler';
import * as Sentry from '@sentry/react-native';
type PostMediaOption = 'photo' | 'video' | 'textpost';
const postMediaOptions: {title: string; type: PostMediaOption}[] = [
{
type: 'video',
title: 'Video',
},
{
type: 'photo',
title: 'Photo',
},
{
type: 'textpost',
title: 'Text',
},
];
const AppCamera = () => {
const [cameraOption, setCameraOption] =
useState<Exclude<PostMediaOption, 'textpost'>>('photo');
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>(
'back'
);
const [flash, setFlash] = useState<TakePhotoOptions['flash']>('off');
const [seconds, setSeconds] = useState(0);
const [isRecordingVideo, setIsRecordingVideo] = useState(false);
const [showGrantMicPrompt, setShowGrantMicPrompt] = useState(false);
const {removeWeeklyChallengeDetail} = useWeeklyChallengeHandler();
const [isCameraReady, setIsCameraReady] = useState(false);
const cameraRef = useRef<Camera>(null);
const device = useCameraDevice(cameraPosition);
const {
hasPermission: hasCameraPermission,
requestPermission: requestCameraPermission,
} = useCameraPermission();
const {
hasPermission: hasMicPermission,
requestPermission: requestMicPermission,
} = useMicrophonePermission();
const format = useCameraFormat(device, Templates.Snapchat);
const navigation = useNavigation();
const focused = useIsFocused();
const appState = useAppState();
const {top, bottom} = useSafeAreaInsets();
const isActive = useMemo(
() => focused && appState === 'active',
[appState, focused]
);
const {runTaskAfterInteractions} = useRunAfterInteraction();
const {setCapturedMediaUri, setCapturedMediaData, setMediaType} =
useAddPostContext();
const route = useRoute();
const {feedItemForAudio = {}, musicInfo: musicInfoForAudioReuse = {}} =
route.params ?? ({} as unknown as any);
const {playSong, pauseSong, isPlaying} = useMusicTrackPlayerForSingleSong({
songUrl: musicInfoForAudioReuse?.url,
onRepeat: true,
});
const videoTimerIntervalId = useRef<NodeJS.Timeout>(null);
const resetTrackPlayer = async () => {
await TrackPlayer.reset();
};
useEffect(() => {
const init = async () => {
try {
if (!hasCameraPermission) await requestCameraPermission();
if (!hasMicPermission) await requestMicPermission();
} catch (err) {
logThis('error when requesting mic and camera permission', err);
}
};
init();
}, [hasCameraPermission, hasMicPermission]);
let micPermStatus = Camera.getMicrophonePermissionStatus();
useEffect(() => {
if (micPermStatus === 'denied') {
setShowGrantMicPrompt(true);
} else {
setShowGrantMicPrompt(false);
}
// logThis({hasMicPermission, micPermStatus, showGrantMicPrompt});
}, [micPermStatus]);
useEffect(() => {
return () => {
resetTrackPlayer();
};
}, []);
useEffect(() => {
if (isRecordingVideo) {
videoTimerIntervalId.current = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
}
return () => clearInterval(videoTimerIntervalId.current);
}, [isRecordingVideo]);
useEffect(() => {
if (seconds >= luupliMaxVideoDuration) {
endCaptureVideo();
}
}, [seconds]);
const openAppSettings = () => {
if (Platform.OS === 'ios') {
const url = 'app-settings:';
openLinkInDefaultApp(url);
} else {
Linking.openSettings();
}
};
const goToSettingsForPermissions = async () => {
Alert.alert(
'Permissions Required',
'You need to allow camera and microphone permissions to use this feature.',
[
{
text: 'Cancel',
onPress: () => {
navigation.goBack();
},
},
{
text: 'Open Settings',
onPress: () => {
openAppSettings();
},
},
]
);
};
/** Navigate to Edit Media Screen */
const goToEditMedia = () => {
navigation.navigate('EditMediaScreen' as unknown as never);
};
/** Upload image from gallery */
const uploadImageFormGallery = async () => {
try {
const newFiles = await launchImageLibrary({
mediaType: device == null ? 'mixed' : cameraOption,
presentationStyle: 'currentContext',
quality: 1,
videoQuality: 'high',
assetRepresentationMode: 'current',
});
const selectedImage = newFiles.assets?.[0];
let imageUrlPath = selectedImage.uri;
setCapturedMediaUri(imageUrlPath);
setCapturedMediaData(selectedImage);
let dataType = checkFileType(selectedImage.uri);
setMediaType(dataType);
goToEditMedia();
} catch (error) {
console.log(error);
}
};
const snapPhoto = async () => {
if (cameraRef.current) {
try {
const data = await cameraRef.current.takePhoto({
flash,
});
setCapturedMediaUri(
Platform.OS === 'ios' ? data.path : `file://${data.path}`
);
let dataType = checkFileType(data.path);
setMediaType(dataType);
setCapturedMediaData({
...data,
path: Platform.OS === 'ios' ? data.path : `file://${data.path}`,
});
goToEditMedia();
} catch (error) {
console.error('Failed to capture picture:', error);
}
}
};
const resetAndClearProgressTimer = () => {
setSeconds(0);
clearInterval(videoTimerIntervalId.current);
};
const handleVideoError = (err: CameraCaptureError) => {
switch (err.code) {
case 'capture/recording-canceled':
setIsRecordingVideo(false);
resetAndClearProgressTimer();
break;
case 'capture/focus-canceled':
setIsRecordingVideo(false);
Sentry.captureMessage(JSON.stringify(err, null, 3), 'error');
break;
default:
break;
}
};
const captureVideo = async () => {
if (cameraRef.current && isCameraReady) {
setIsRecordingVideo(true);
try {
cameraRef.current.startRecording({
flash: flash === 'on' ? 'on' : 'off',
videoCodec: 'h264', // Use H.264 codec for MP4
fileType: 'mp4', // Explicitly set file type to MP4
onRecordingFinished: video => {
Sentry.captureMessage(JSON.stringify(video, null, 3), 'error');
// const response = {...video, path: `file://${video?.path}`};
// setCapturedMediaUri(`file://${video?.path}`); // Update the capturedMediaUri state
setCapturedMediaUri(video?.path); // Update the capturedMediaUri state
// setCapturedMediaUri(
// Platform.OS === 'ios' ? video?.path : `file://${video?.path}`
// ); // Update the capturedMediaUri state
// setCapturedMediaData({
// ...video,
// path:
// Platform.OS === 'ios' ? video?.path : `file://${video?.path}`,
// });
setCapturedMediaData(video);
setMediaType('Video');
setIsRecordingVideo(false);
// logThis('Video recorded:', video);
},
onRecordingError: error => {
handleVideoError(error);
logThis('error when recording Video', error);
},
});
} catch (error) {
console.log('Failed to record video:', error);
}
}
};
const endCaptureVideo = async () => {
if (cameraRef.current) {
try {
await cameraRef.current.stopRecording();
resetAndClearProgressTimer();
} catch (error) {
logThis('Failed to stop video recording:', error);
}
}
setIsRecordingVideo(false);
if (isPlaying) {
pauseSong();
await TrackPlayer.seekTo(0);
}
goToEditMedia();
};
/** Focus gesture to focus camera to wherever the user taps in the screen */
const focus = useCallback((point: Point) => {
const c = cameraRef.current;
if (c == null) return;
c.focus(point);
}, []);
const gesture = Gesture.Tap().onEnd(({x, y}) => {
runOnJS(focus)({x, y});
});
const onCancelPress = async () => {
if (isRecordingVideo) {
await cameraRef.current.cancelRecording();
} else {
removeWeeklyChallengeDetail();
navigation.goBack();
}
};
if (!hasCameraPermission) {
return (
<View
style={[
StyleSheet.absoluteFill,
{
backgroundColor: luupliGreenLightColor,
justifyContent: 'center',
alignItems: 'center',
},
]}
>
<AppText align={'center'}>
Camera App requires Microphone and Camera permission.
</AppText>
<Button
label="Grant Permission"
marginTop={24}
labelTextType={'button'}
onPress={async () => {
await goToSettingsForPermissions();
}}
/>
</View>
);
}
Sentry.captureMessage(
JSON.stringify(device, (k, v) => (k === 'formats' ? [] : v), 2),
'error'
);
if (device == null) {
return (
<View
style={[
StyleSheet.absoluteFill,
{
backgroundColor: colors.luupli_light_green_100,
justifyContent: 'center',
alignItems: 'center',
},
]}
>
<View
style={{
position: 'absolute',
top: top + 30,
left: 30,
}}
>
<TouchableOpacity hitSlop={10} onPress={navigation.goBack}>
<CancelIcon
width={20}
height={20}
strokeWidth={1}
strokeColor={colors.luupli_black}
/>
</TouchableOpacity>
</View>
<Image
source={require('@assets/gifs/errorGif.gif')}
style={{width: 200, height: 150}}
/>
<View style={{display: 'flex', flexDirection: 'row'}}>
<CameraIcon strokeColor={colors.luupli_green_500} />
<AppText align={'center'}>No camera device Available.</AppText>
</View>
<Button
label="Upload A text"
marginTop={24}
onPress={() => {
navigation.navigate('TextPreviewScreen' as unknown as never);
}}
/>
<View
style={{
position: 'absolute',
bottom: 30,
left: 10,
}}
>
<TouchableOpacity onPress={uploadImageFormGallery}>
<Upload width={140} height={40} stroke={colors.primary_black} />
</TouchableOpacity>
<AppText align={'center'} color={'primary_black'}>
Upload
</AppText>
</View>
</View>
);
}
return (
<ScreenWrapper
edges={['left', 'right']}
customStyles={{
paddingTop: top,
paddingBottom: bottom,
backgroundColor: colors.luupli_light_green_100,
}}
>
<GestureDetector gesture={gesture}>
{/* <ScreenWrapper edges={['bottom']}> */}
<View
collapsable={false}
style={{
flex: 1,
// backgroundColor: 'black',
// position: 'relative',
width: '100%',
height: '100%',
}}
>
<View style={styles.topContainerStyle}>
<TouchableOpacity hitSlop={10} onPress={onCancelPress}>
<CancelIcon
width={16}
height={16}
strokeWidth={1}
strokeColor={colors.plain_white}
/>
</TouchableOpacity>
{device?.hasFlash && (
<TouchableOpacity
hitSlop={10}
onPress={() =>
setFlash(prevFlash => (prevFlash !== 'on' ? 'on' : 'off'))
}
>
<FlashIcon
fillColor={
flash === 'on' || flash === 'auto'
? colors.luupli_light_green_300
: colors.plain_white
}
/>
</TouchableOpacity>
)}
<TouchableOpacity
hitSlop={10}
onPress={() => {
setCameraPosition(prev => (prev === 'back' ? 'front' : 'back'));
}}
>
<CameraSwitcherIcon />
</TouchableOpacity>
</View>
<Camera
ref={cameraRef}
style={[
StyleSheet.absoluteFill,
{
top: 0,
height: SCREEN_HEIGHT,
},
]}
device={device}
isActive={isActive}
enableZoomGesture
format={format}
photoQualityBalance={'quality'}
photo={cameraOption === 'photo'}
video={cameraOption === 'video'}
audio={hasMicPermission && cameraOption === 'video'}
//enableBufferCompression
// audio={cameraOption === 'video'}
onError={error => {
logThis('Camera error', error);
}}
onInitialized={() => {
logThis('Camera initialized');
setIsCameraReady(true);
}}
onPreviewStarted={() => logThis('Preview started!')}
onPreviewStopped={() => logThis('Preview stopped!')}
/>
<View style={styles.bottomContainerStyle}>
{!isRecordingVideo ? (
<View style={styles.optionsContainerStyle}>
{postMediaOptions.map(option => (
<TouchableOpacity
key={option.type}
activeOpacity={0.7}
style={styles.optionItemStyle}
onPress={() => {
if (option.type === 'textpost') {
navigation.navigate(
'TextPreviewScreen' as unknown as never
);
} else {
setCameraOption(option.type);
}
}}
>
<AppText
style={{
...styles.bottomTextItemStyle,
color:
cameraOption === option.type
? colors.luupli_light_green_300
: colors.grey_200,
}}
>
{option.title}
</AppText>
</TouchableOpacity>
))}
</View>
) : null}
<View
style={[
styles.mediaSwitcherContainer,
{
justifyContent: isRecordingVideo
? 'flex-end'
: 'space-between',
},
]}
>
{!isRecordingVideo ? (
<View style={styles.uploadBtnContainer}>
<TouchableOpacity onPress={uploadImageFormGallery}>
<CircledUploadIcon />
</TouchableOpacity>
<AppText style={styles.uploadTextStyle}>Upload</AppText>
</View>
) : null}
<TouchableOpacity
style={[
styles.shutterIndicatorContainerStyle,
isRecordingVideo ? {alignItems: 'center'} : {},
]}
onPress={() => {
if (cameraOption === 'photo') {
snapPhoto();
} else {
if (isRecordingVideo) {
endCaptureVideo();
} else {
captureVideo();
}
}
}}
>
{/* <View
style={{
justifyContent: 'center',
alignItems: 'center',
width: 80,
height: 80,
padding: 20,
}}
> */}
{cameraOption === 'photo' ? (
<CameraSvgIcon style={styles.photoShutterIndicator} />
) : (
<CircularProgressWithImage
seconds={seconds}
maxSeconds={luupliMaxVideoDuration}
isRecordingVideo={isRecordingVideo}
imageSourceComponent={
<Video style={styles.videoShutterIndicator} />
}
/>
)}
{/* </View> */}
</TouchableOpacity>
</View>
</View>
</View>
{/* </ScreenWrapper> */}
</GestureDetector>
{!hasMicPermission && showGrantMicPrompt && (
<AlertModal
isVisible={showGrantMicPrompt}
noTitle="Ignore"
yesTitle="Open Settings"
title="Ohh hi!"
description={`Luups are more fun with sound 🎙️👌🏾 \nAllow microphone access in settings to add sound`}
onPressYes={goToSettingsForPermissions}
onPressNo={() => setShowGrantMicPrompt(false)}
/>
)}
</ScreenWrapper>
);
};
export default AppCamera;
const styles = StyleSheet.create({
topContainerStyle: {
position: 'absolute',
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
zIndex: 2,
top: 20,
paddingHorizontal: 20,
},
bottomTextItemStyle: {
color: colors.luupli_white,
fontFamily: 'Inter',
fontSize: 17,
},
bottomContainerStyle: {
position: 'absolute',
width: '100%',
bottom: 0,
paddingBottom: 25,
zIndex: 2,
},
optionsContainerStyle: {
flexDirection: 'row',
backgroundColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
gap: 14,
},
optionItemStyle: {
backgroundColor: 'rgba(0,0,0,0.3)',
justifyContent: 'center',
alignItems: 'center',
height: 30,
paddingHorizontal: 8,
borderRadius: 10,
fontFamily: 'Inter',
fontSize: 17,
},
mediaSwitcherContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 20,
width: '65%',
marginHorizontal: 10,
},
shutterIndicatorContainerStyle: {
position: 'relative',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'flex-end',
width: 120,
height: 120,
padding: 20,
},
photoShutterIndicator: {
position: 'absolute',
width: 80,
height: 80,
borderRadius: 80 / 2,
},
videoShutterIndicator: {
position: 'absolute',
width: 80,
height: 80,
borderRadius: 80 / 2,
},
flex2Style: {
// flex: 2,
justifyContent: 'center',
alignItems: 'center',
},
uploadBtnContainer: {
display: 'flex',
alignItems: 'center',
},
uploadTextStyle: {
color: colors.luupli_white,
fontFamily: 'Inter',
fontSize: 15,
marginTop: 5,
},
});Relevant log output
log is not available. This the data after user finished recording,
{
"height": 1080,
"width": 1920,
"duration": 4.58,
"path": "/data/user/0/com.dev.luupli/cache/mrousavy3823717656105929792.mp4"
}Camera Device
{
"formats": [],
"sensorOrientation": "landscape-left",
"hardwareLevel": "full",
"maxZoom": 10,
"minZoom": 1,
"maxExposure": 24,
"supportsLowLightBoost": false,
"neutralZoom": 1,
"physicalDevices": [
"wide-angle-camera"
],
"supportsFocus": true,
"supportsRawCapture": false,
"isMultiCam": false,
"minFocusDistance": 0,
"minExposure": -24,
"name": "0 (BACK) androidx.camera.camera2",
"hasFlash": true,
"hasTorch": true,
"position": "back",
"id": "0"
}Device
Redmi Note 12(Android)
VisionCamera Version
^4.7.2/4.7.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)
- I have read the Troubleshooting Guide
- I agree to follow this project's Code of Conduct
- I searched for similar issues in this repository and found none.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
🐛 bugSomething isn't workingSomething isn't working