1import { CodedError } from 'expo-modules-core'; 2import * as React from 'react'; 3import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; 4import createElement from 'react-native-web/dist/exports/createElement'; 5 6import { 7 CameraCapturedPicture, 8 CameraNativeProps, 9 CameraPictureOptions, 10 CameraType, 11} from './Camera.types'; 12import CameraManager from './ExponentCameraManager.web'; 13import { capture } from './WebCameraUtils'; 14import { PictureSizes } from './WebConstants'; 15import { useWebCameraStream } from './useWebCameraStream'; 16import { useWebQRScanner } from './useWebQRScanner'; 17 18export interface ExponentCameraRef { 19 getAvailablePictureSizes: (ratio: string) => Promise<string[]>; 20 takePicture: (options: CameraPictureOptions) => Promise<CameraCapturedPicture>; 21 resumePreview: () => Promise<void>; 22 pausePreview: () => Promise<void>; 23} 24 25const ExponentCamera = React.forwardRef( 26 ( 27 { type, pictureSize, poster, ...props }: CameraNativeProps & { children?: React.ReactNode }, 28 ref: React.Ref<ExponentCameraRef> 29 ) => { 30 const video = React.useRef<HTMLVideoElement | null>(null); 31 32 const native = useWebCameraStream(video, type as CameraType, props, { 33 onCameraReady() { 34 if (props.onCameraReady) { 35 props.onCameraReady(); 36 } 37 }, 38 onMountError: props.onMountError, 39 }); 40 41 const isQRScannerEnabled = React.useMemo<boolean>(() => { 42 return !!( 43 props.barCodeScannerSettings?.barCodeTypes?.includes('qr') && !!props.onBarCodeScanned 44 ); 45 }, [props.barCodeScannerSettings?.barCodeTypes, props.onBarCodeScanned]); 46 47 useWebQRScanner(video, { 48 interval: props.barCodeScannerSettings?.interval, 49 isEnabled: isQRScannerEnabled, 50 captureOptions: { scale: 1, isImageMirror: native.type === CameraType.front }, 51 onScanned(event) { 52 if (props.onBarCodeScanned) { 53 props.onBarCodeScanned(event); 54 } 55 }, 56 // onError: props.onMountError, 57 }); 58 59 // const [pause, setPaused] 60 61 React.useImperativeHandle( 62 ref, 63 () => ({ 64 async getAvailablePictureSizes(ratio: string): Promise<string[]> { 65 return PictureSizes; 66 }, 67 async takePicture(options: CameraPictureOptions): Promise<CameraCapturedPicture> { 68 if (!video.current || video.current?.readyState !== video.current?.HAVE_ENOUGH_DATA) { 69 throw new CodedError( 70 'ERR_CAMERA_NOT_READY', 71 'HTMLVideoElement does not have enough camera data to construct an image yet.' 72 ); 73 } 74 const settings = native.mediaTrackSettings; 75 if (!settings) { 76 throw new CodedError('ERR_CAMERA_NOT_READY', 'MediaStream is not ready yet.'); 77 } 78 79 return capture(video.current, settings, { 80 ...options, 81 // This will always be defined, the option gets added to a queue in the upper-level. We should replace the original so it isn't called twice. 82 onPictureSaved(picture) { 83 if (options.onPictureSaved) { 84 options.onPictureSaved(picture); 85 } 86 if (props.onPictureSaved) { 87 props.onPictureSaved({ nativeEvent: { data: picture, id: -1 } }); 88 } 89 }, 90 }); 91 }, 92 async resumePreview(): Promise<void> { 93 if (video.current) { 94 video.current.play(); 95 } 96 }, 97 async pausePreview(): Promise<void> { 98 if (video.current) { 99 video.current.pause(); 100 } 101 }, 102 }), 103 [native.mediaTrackSettings, props.onPictureSaved] 104 ); 105 106 // TODO(Bacon): Create a universal prop, on native the microphone is only used when recording videos. 107 // Because we don't support recording video in the browser we don't need the user to give microphone permissions. 108 const isMuted = true; 109 110 const style = React.useMemo<StyleProp<ViewStyle>>(() => { 111 const isFrontFacingCamera = native.type === CameraManager.Type.front; 112 return [ 113 StyleSheet.absoluteFill, 114 styles.video, 115 { 116 // Flip the camera 117 transform: isFrontFacingCamera ? [{ scaleX: -1 }] : undefined, 118 }, 119 ]; 120 }, [native.type]); 121 122 return ( 123 <View pointerEvents="box-none" style={[styles.videoWrapper, props.style]}> 124 <Video 125 autoPlay 126 playsInline 127 muted={isMuted} 128 poster={poster} 129 // webkitPlaysinline 130 pointerEvents={props.pointerEvents} 131 ref={video} 132 style={style} 133 /> 134 {props.children} 135 </View> 136 ); 137 } 138); 139 140export default ExponentCamera; 141 142const Video = React.forwardRef( 143 ( 144 props: React.ComponentProps<typeof View> & { 145 autoPlay?: boolean; 146 playsInline?: boolean; 147 muted?: boolean; 148 poster?: string; 149 }, 150 ref: React.Ref<HTMLVideoElement> 151 ) => createElement('video', { ...props, ref }) 152); 153 154const styles = StyleSheet.create({ 155 videoWrapper: { 156 flex: 1, 157 alignItems: 'stretch', 158 }, 159 video: { 160 width: '100%', 161 height: '100%', 162 objectFit: 'cover', 163 }, 164}); 165