import React, { useState, useRef } from 'react'; import classNames from 'classnames'; import { Flex, Loader, View } from '@aws-amplify/ui-react'; import { FaceMatchState } from '../service'; import { useLivenessActor, useLivenessSelector, createLivenessSelector, useMediaStreamInVideo, UseMediaStreamInVideo, } from '../hooks'; import { ErrorDisplayText, HintDisplayText, StreamDisplayText, } from '../displayText'; import { CancelButton, Hint, RecordingIcon, Overlay, selectErrorState, MatchIndicator, } from '../shared'; import { LivenessClassNames } from '../types/classNames'; import { CheckScreenComponents, FaceLivenessErrorModal, renderErrorModal, } from '../shared/FaceLivenessErrorModal'; export const selectVideoConstraints = createLivenessSelector( (state) => state.context.videoAssociatedParams?.videoConstraints ); export const selectVideoStream = createLivenessSelector( (state) => state.context.videoAssociatedParams?.videoMediaStream ); export const selectFaceMatchPercentage = createLivenessSelector( (state) => state.context.faceMatchAssociatedParams!.faceMatchPercentage ); export const selectFaceMatchState = createLivenessSelector( (state) => state.context.faceMatchAssociatedParams!.faceMatchState ); export interface LivenessCameraModuleProps { isMobileScreen: boolean; isRecordingStopped: boolean; streamDisplayText: Required; hintDisplayText: Required; errorDisplayText: Required; components?: CheckScreenComponents; testId?: string; } const centeredLoader = ( ); /** * For now we want to memoize the HOC for MatchIndicator because to optimize renders * The LivenessCameraModule still needs to be optimized for re-renders and at that time * we should be able to remove this memoization */ const MemoizedMatchIndicator = React.memo(MatchIndicator); export const LivenessCameraModule = ( props: LivenessCameraModuleProps ): JSX.Element => { const { isMobileScreen, isRecordingStopped, streamDisplayText, hintDisplayText, errorDisplayText, components: customComponents, testId, } = props; const { cancelLivenessCheckText, recordingIndicatorText } = streamDisplayText; const { ErrorView = FaceLivenessErrorModal } = customComponents ?? {}; const [state, send] = useLivenessActor(); const videoStream = useLivenessSelector(selectVideoStream); const videoConstraints = useLivenessSelector(selectVideoConstraints); const faceMatchPercentage = useLivenessSelector(selectFaceMatchPercentage); const faceMatchState = useLivenessSelector(selectFaceMatchState); const errorState = useLivenessSelector(selectErrorState); const showMatchIndicatorStates = [ FaceMatchState.TOO_FAR, FaceMatchState.CANT_IDENTIFY, FaceMatchState.FACE_IDENTIFIED, FaceMatchState.MATCHED, ]; const { videoRef, videoWidth, videoHeight } = useMediaStreamInVideo( videoStream!, videoConstraints! ); const canvasRef = useRef(null); const freshnessColorRef = useRef(null); const [isCameraReady, setIsCameraReady] = useState(false); const isCheckingCamera = state.matches('cameraCheck'); const isRecording = state.matches('recording'); const isCheckSucceeded = state.matches('checkSucceeded'); const isFlashingFreshness = state.matches({ recording: 'flashFreshnessColors', }); // Android/Firefox and iOS flip the values of width/height returned from // getUserMedia, so we'll reset these in useLayoutEffect with the videoRef // element's intrinsic videoWidth and videoHeight attributes const [mediaWidth, setMediaWidth] = useState(videoWidth); const [mediaHeight, setMediaHeight] = useState(videoHeight); const [aspectRatio, setAspectRatio] = useState(() => videoWidth && videoHeight ? videoWidth / videoHeight : 0 ); React.useLayoutEffect(() => { if (isCameraReady) { send({ type: 'SET_DOM_AND_CAMERA_DETAILS', data: { videoEl: videoRef.current, canvasEl: canvasRef.current, freshnessColorEl: freshnessColorRef.current, isMobile: isMobileScreen, }, }); } if (videoRef.current) { setMediaWidth(videoRef.current.videoWidth); setMediaHeight(videoRef.current.videoHeight); setAspectRatio( videoRef.current.videoWidth / videoRef.current.videoHeight ); } }, [send, videoRef, isCameraReady, isMobileScreen]); const handleMediaPlay = () => { setIsCameraReady(true); }; if (isCheckingCamera) { return ( {centeredLoader} ); } return ( {!isCameraReady && centeredLoader} ); };