import { Dimensions, Face, faceBoundsAdjustToView, scanFaces, sortFormatsByResolution, } from '@mat2718/vision-camera-face-detector'; import React, { FC, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { Button, Platform, StyleProp, StyleSheet, TouchableOpacity, useWindowDimensions, View, ViewStyle, } from 'react-native'; import {runOnJS} from 'react-native-reanimated'; import { Camera, CameraProps, Frame, PhotoFile, TakePhotoOptions, useCameraDevices, useFrameProcessor, } from 'react-native-vision-camera'; type Props = { /** * If true, the bounding box will be drawn around the face detected. */ enableBoundingBox?: boolean; // enableAutoCapture?: boolean; /** * If true, the bounding box will be drawn around all faces detected. */ boundingBoxMultipleFaces?: boolean; /** * The color of the bounding box. */ boundingBoxColor?: string; /** * The width of the bounding box. */ boundingBoxWidth?: number; /** * The corner radius of the bounding box. */ boundingBoxCornerRadius?: number; /** * The vertical padding of the bounding box. */ boundingBoxVerticalPadding?: number; /** * The horizontal padding of the bounding box. */ boundingBoxHorizontalPadding?: number; /** * The options to use when taking a photo. */ takePhotoOptions?: TakePhotoOptions; /** * The style of the component. */ style?: StyleProp; // onFacesDetected?: (faces: Face[]) => void; /** * The component to use as the photo capture button. */ photoCaptureButton?: React.ReactNode; /** * pass the photo to the parent component * @param photoBase64 * @returns */ photoCaptureOnPress?: (photoBase64: PhotoFile) => void; /** * The style of the photo capture button. */ photoCaptureButtonStyle?: StyleProp; /** * If true, the photo skip button will be displayed. */ photoSkipButtonEnabled?: boolean; /** * The component to use as the photo skip button. */ photoSkipButton?: React.ReactNode; /** * callback function to skip the photo * @returns */ photoSkipOnPress?: () => void; /** * The style of the photo skip button. */ photoSkipButtonStyle?: StyleProp; /** * The component to use as the torch button. */ torchButton?: React.ReactNode; /** * all options for the camera */ cameraProps?: CameraProps; }; const FaceDetector: FC> = ({ enableBoundingBox, boundingBoxMultipleFaces, boundingBoxColor, boundingBoxWidth, boundingBoxCornerRadius, boundingBoxVerticalPadding, boundingBoxHorizontalPadding, takePhotoOptions, style, photoCaptureButton, photoCaptureOnPress, photoCaptureButtonStyle, photoSkipButton, photoSkipOnPress, photoSkipButtonStyle, photoSkipButtonEnabled, // torchButton, // camera options cameraProps, }) => { //***************************************************************************************** // setting up the state //***************************************************************************************** // Permissions const [hasPermission, setHasPermission] = React.useState(false); // camera states const devices = useCameraDevices(); const direction: 'front' | 'back' = 'front'; const device = devices[direction]; const camera = useRef(null); const [faces, setFaces] = useState([]); const {height: screenHeight, width: screenWidth} = useWindowDimensions(); const landscapeMode = screenWidth > screenHeight; const [frameDimensions, setFrameDimensions] = useState(); const [isActive, setIsActive] = useState(true); // photoCaptured is used to prevent attemptPhotoCapture() from being called again before it's completed const [photoCaptured, setPhotoCaptured] = useState(false); //***************************************************************************************** // Comp Logic //***************************************************************************************** /* A cleanup function that is called when the component is unmounted. */ useEffect(() => { return () => { setIsActive(false); }; }, []); // which format should we use const formats = useMemo( () => device?.formats.sort(sortFormatsByResolution), [device?.formats], ); //figure our what happens if it is undefined? const [format, setFormat] = useState( formats && formats.length > 0 ? formats[0] : undefined, ); /* Setting the frame dimensions and faces. */ const handleScan = useCallback( (frame: Frame, newFaces: Face[]) => { const isRotated = !landscapeMode; setFrameDimensions( isRotated ? { width: frame.height, height: frame.width, } : { width: frame.width, height: frame.height, }, ); setFaces(newFaces); }, [landscapeMode], ); /* Setting the format to the first format in the formats array. */ useEffect(() => { setFormat(formats && formats.length > 0 ? formats[0] : undefined); // eslint-disable-next-line react-hooks/exhaustive-deps }, [device]); /* Using the useFrameProcessor hook to process the video frames. */ const frameProcessor = useFrameProcessor( frame => { 'worklet'; const scannedFaces = scanFaces(frame); runOnJS(handleScan)(frame, scannedFaces); }, [handleScan], ); /* Using the useMemo hook to create a style object. */ const boundingStyle = useMemo>( () => ({ position: 'absolute', top: 0, left: 0, width: screenWidth, height: screenHeight, }), [screenWidth, screenHeight], ); useEffect(() => { (async () => { const status = await Camera.requestCameraPermission(); setHasPermission(status === 'authorized'); })(); }, []); /** * If the camera is defined, take a photo, if the photo is defined, read the file and return the * base64 string, if the photo is not defined, return an error, if the camera is not defined, return * an error, if the photo has already been captured, return an error, if no face is detected, return * an error. * @param {Face[]} [faceBounds] - Face[] * @returns A promise that resolves with a base64 string of the photo taken. */ const attemptPhotoCapture = async (faceBounds?: Face[]) => { if (faceBounds && faceBounds.length > 0) { if (!photoCaptured) { setPhotoCaptured(true); if (camera?.current) { const photo = await camera.current.takePhoto(takePhotoOptions); if (photo && photoCaptureOnPress) { try { photoCaptureOnPress(photo); return Promise.resolve(photo); } catch (err: any) { console.error(err.message); setPhotoCaptured(false); return Promise.reject(err); } } else { setPhotoCaptured(false); return Promise.reject('PhotoCaptureOnPress or photo is undefined'); } } else { setPhotoCaptured(false); return Promise.reject('Camera is undefined'); } } else { setPhotoCaptured(false); return Promise.reject('Photo already captured'); } } else { setPhotoCaptured(false); return Promise.reject('No face detected'); } }; //***************************************************************************************** // stylesheet //***************************************************************************************** const styles = StyleSheet.create({ boundingBox: { borderRadius: boundingBoxCornerRadius, borderWidth: boundingBoxWidth, borderColor: boundingBoxColor, position: 'absolute', }, fixToText: { flexDirection: 'row', justifyContent: 'space-between', }, }); //******************************************************************** // Components //******************************************************************** return device != null && hasPermission ? ( {frameDimensions && (() => { if (enableBoundingBox) { const mirrored = Platform.OS === 'android' && direction === 'front'; const {adjustRect} = faceBoundsAdjustToView( frameDimensions, { width: landscapeMode ? screenHeight : screenWidth, height: landscapeMode ? screenWidth : screenHeight, }, landscapeMode, boundingBoxVerticalPadding, boundingBoxHorizontalPadding, ); if (boundingBoxMultipleFaces) { /* Returning a view for each face detected. */ return faces ? faces.map((i, index) => { const {left, ...others} = adjustRect(i.bounds); return ( ); }) : undefined; } else { /* This is checking if there are any faces detected. If there are, it will return the first face detected. If there are no faces detected, it will return undefined. */ if (faces && faces.length > 0) { const {left, ...others} = adjustRect(faces[0].bounds); return ( ); } else { return undefined; } } } else { return undefined; } })()} {photoCaptureButton || photoSkipButton ? ( {photoCaptureButton ? ( attemptPhotoCapture(faces)}> {photoCaptureButton} ) : (