Files
Schnappix/node_modules/expo-camera/src/ExpoCamera.web.tsx
T

177 lines
5.2 KiB
TypeScript

import { CodedError } from 'expo-modules-core';
import {
type PropsWithChildren,
useRef,
useMemo,
useImperativeHandle,
type ComponentProps,
type Ref,
} from 'react';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import createElement from 'react-native-web/dist/exports/createElement';
import {
CameraNativeProps,
CameraCapturedPicture,
CameraPictureOptions,
CameraType,
} from './Camera.types';
import CameraManager from './ExpoCameraManager.web';
import { capture } from './web/WebCameraUtils';
import { PictureSizes } from './web/WebConstants';
import { useWebCameraStream } from './web/useWebCameraStream';
import { useWebQRScanner } from './web/useWebQRScanner';
export interface ExponentCameraRef {
getAvailablePictureSizes: (ratio: string) => Promise<string[]>;
takePicture: (options: CameraPictureOptions) => Promise<CameraCapturedPicture>;
resumePreview: () => Promise<void>;
pausePreview: () => Promise<void>;
}
const ExponentCamera = ({
facing,
poster,
ref,
...props
}: PropsWithChildren<CameraNativeProps>) => {
const video = useRef<HTMLVideoElement | null>(null);
const native = useWebCameraStream(video, facing as CameraType, props, {
onCameraReady() {
if (props.onCameraReady) {
props.onCameraReady();
}
},
onMountError: props.onMountError,
});
const isQRScannerEnabled = useMemo<boolean>(() => {
return Boolean(
props.barcodeScannerSettings?.barcodeTypes?.includes('qr') && !!props.onBarcodeScanned
);
}, [props.barcodeScannerSettings?.barcodeTypes, props.onBarcodeScanned]);
useWebQRScanner(video, {
interval: 300,
isEnabled: isQRScannerEnabled,
captureOptions: { scale: 1, isImageMirror: native.type === 'front' },
onScanned(event) {
if (props.onBarcodeScanned) {
props.onBarcodeScanned(event);
}
},
});
useImperativeHandle(
ref,
() => ({
async getAvailablePictureSizes(): Promise<string[]> {
return PictureSizes;
},
async takePicture(options: CameraPictureOptions): Promise<CameraCapturedPicture> {
if (!video.current || video.current?.readyState !== video.current?.HAVE_ENOUGH_DATA) {
throw new CodedError(
'ERR_CAMERA_NOT_READY',
'HTMLVideoElement does not have enough camera data to construct an image yet.'
);
}
const settings = native.mediaTrackSettings;
if (!settings) {
throw new CodedError('ERR_CAMERA_NOT_READY', 'MediaStream is not ready yet.');
}
return capture(video.current, settings, {
...options,
// 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.
onPictureSaved(picture) {
if (options.onPictureSaved) {
options.onPictureSaved(picture);
}
if (props.onPictureSaved) {
props.onPictureSaved({ nativeEvent: { data: picture, id: -1 } });
}
},
});
},
async resumePreview(): Promise<void> {
if (video.current) {
video.current.play();
}
},
async pausePreview(): Promise<void> {
if (video.current) {
video.current.pause();
}
},
async stopRecording(): Promise<void> {
console.warn('stopRecording is not supported on web.');
},
async record(): Promise<{ uri: string }> {
console.warn('record is not supported on web.');
return { uri: '' };
},
async toggleRecording() {
console.warn('toggleRecording is not supported on web.');
},
async launchModernScanner() {
console.warn('launchModernScanner is not supported on web.');
},
async getAvailableLenses() {
console.warn('getAvailableLenses is not supported on web.');
return [];
},
}),
[native.mediaTrackSettings, props.onPictureSaved]
);
// TODO(Bacon): Create a universal prop, on native the microphone is only used when recording videos.
// Because we don't support recording video in the browser we don't need the user to give microphone permissions.
const isMuted = true;
const style = useMemo<StyleProp<ViewStyle>>(() => {
const isFrontFacingCamera = native.type === CameraManager.Type.front;
return [
StyleSheet.absoluteFill,
styles.video,
{
pointerEvents: props.pointerEvents,
// Flip the camera
transform: isFrontFacingCamera ? [{ scaleX: -1 }] : undefined,
},
];
}, [props.pointerEvents, native.type]);
return (
<View style={[styles.videoWrapper, props.style]}>
<Video autoPlay playsInline muted={isMuted} poster={poster} ref={video} style={style} />
{props.children}
</View>
);
};
export default ExponentCamera;
const Video = (
props: ComponentProps<typeof View> & {
autoPlay?: boolean;
playsInline?: boolean;
muted?: boolean;
poster?: string;
ref: Ref<HTMLVideoElement>;
}
) => createElement('video', { ...props });
const styles = StyleSheet.create({
videoWrapper: {
flex: 1,
alignItems: 'stretch',
pointerEvents: 'box-none',
},
video: {
width: '100%',
height: '100%',
objectFit: 'cover',
},
});