react-native-vision-camera/src/Camera.tsx

576 lines
20 KiB
TypeScript
Raw Normal View History

2021-02-19 08:23:54 -07:00
import React from 'react';
import { requireNativeComponent, NativeModules, ViewProps, NativeSyntheticEvent, findNodeHandle, NativeMethods, Platform } from 'react-native';
import type { CameraPhotoCodec, CameraVideoCodec } from './CameraCodec';
import type { ColorSpace, CameraDeviceFormat, CameraDevice } from './CameraDevice';
import type { ErrorWithCause } from './CameraError';
import { CameraCaptureError, CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError';
import type { CameraPreset } from './CameraPreset';
import type { CodeType, Code } from './Code';
import type { PhotoFile, TakePhotoOptions } from './PhotoFile';
import type { Point } from './Point';
import type { TakeSnapshotOptions } from './Snapshot';
import type { RecordVideoOptions, VideoFile } from './VideoFile';
2021-02-19 08:07:53 -07:00
//#region Types
type Modify<T, R> = Omit<T, keyof R> & R;
2021-03-08 10:32:20 -07:00
interface CameraFormatProps {
2021-02-19 08:07:53 -07:00
/**
* Automatically selects a camera format which best matches the given preset
*/
preset?: CameraPreset;
/**
* Specify the frames per second this camera should use. Make sure the given `format` includes a frame rate range with the given `fps`.
*/
fps?: never;
/**
* Enables or disables HDR on this camera device. Make sure the given `format` supports HDR mode.
*/
hdr?: never;
/**
* Enables or disables low-light boost on this camera device. Make sure the given `format` supports low-light boost.
*/
lowLightBoost?: never;
/**
* Specifies the color space to use for this camera device. Make sure the given `format` contains the given `colorSpace`.
*/
colorSpace?: never;
/**
* Selects a given format.
*/
format?: never;
2021-03-08 10:32:20 -07:00
}
2021-02-19 08:07:53 -07:00
type CameraPresetProps = Modify<
CameraFormatProps,
{
preset?: never;
fps?: number;
hdr?: boolean;
lowLightBoost?: boolean;
colorSpace?: ColorSpace;
format?: CameraDeviceFormat;
}
>;
2021-03-08 10:32:20 -07:00
interface CameraScannerPropsNever {
2021-02-19 08:07:53 -07:00
/**
* Specify the code types this camera can scan.
*/
scannableCodes?: never;
/**
* Called when one or multiple codes have been scanned.
*/
onCodeScanned?: never;
2021-03-08 10:32:20 -07:00
}
2021-02-19 08:07:53 -07:00
export type CameraScannerProps = Modify<
CameraScannerPropsNever,
{
scannableCodes: CodeType[];
onCodeScanned: (codes: Code[]) => void;
}
>;
2021-03-08 10:32:20 -07:00
export interface CameraDeviceProps {
2021-02-19 08:07:53 -07:00
// Properties
/**
2021-03-08 10:51:53 -07:00
* The Camera Device to use.
*
* See the [Camera Devices](https://cuvent.github.io/react-native-vision-camera/docs/devices) section in the documentation for more information about Camera Devices.
*
* @example
2021-03-09 04:02:10 -07:00
* ```tsx
2021-03-08 10:51:53 -07:00
* const devices = useCameraDevices('wide-angle-camera')
* const device = devices.back
*
* return (
* <Camera
* device={device}
* isActive={true}
* style={StyleSheet.absoluteFill}
* />
* )
* ```
2021-02-19 08:07:53 -07:00
*/
device: CameraDevice;
/**
* Also captures data from depth-perception sensors. (e.g. disparity maps)
*
* @default false
*/
enableDepthData?: boolean;
/**
* A boolean specifying whether the photo render pipeline is prepared for portrait effects matte delivery.
*
* When enabling this, you must also set `enableDepthData` to `true`.
*
* @platform iOS 12.0+
* @default false
*/
enablePortraitEffectsMatteDelivery?: boolean;
/**
* Indicates whether the photo render pipeline should be configured to deliver high resolution still images
*
* @default false
*/
enableHighResolutionCapture?: boolean;
2021-03-08 10:32:20 -07:00
}
2021-02-19 08:07:53 -07:00
2021-03-08 10:32:20 -07:00
export interface CameraDynamicProps {
2021-02-19 08:07:53 -07:00
/**
2021-03-08 10:51:53 -07:00
* Whether the Camera should actively stream video frames, or not. See the [documentation about the `isActive` prop](https://cuvent.github.io/react-native-vision-camera/docs/devices#the-isactive-prop) for more information.
*
* This can be compared to a Video component, where `isActive` specifies whether the video is paused or not.
*
* > Note: If you fully unmount the `<Camera>` component instead of using `isActive={false}`, the Camera will take a bit longer to start again. In return, it will use less resources since the Camera will be completely destroyed when unmounted.
2021-02-19 08:07:53 -07:00
*/
isActive: boolean;
/**
* Set the current torch mode.
*
* Note: The torch is only available on `"back"` cameras, and isn't supported by every phone.
*
* @default "off"
*/
2021-02-19 08:23:54 -07:00
torch?: 'off' | 'on';
2021-02-19 08:07:53 -07:00
/**
* Specifies the zoom factor of the current camera, in percent. (`0.0` - `1.0`)
*
2021-03-08 10:16:45 -07:00
* **Note:** Linearly increasing this value always appears logarithmic to the user.
*
2021-02-19 08:07:53 -07:00
* @default 0.0
*/
zoom?: number;
/**
2021-03-08 10:51:53 -07:00
* Enables or disables the native pinch to zoom gesture.
*
* If you want to implement a custom zoom gesture, see [the Zooming with Reanimated documentation](https://cuvent.github.io/react-native-vision-camera/docs/animated).
2021-02-19 08:07:53 -07:00
*
* @default false
*/
enableZoomGesture?: boolean;
2021-03-08 10:32:20 -07:00
}
2021-02-19 08:07:53 -07:00
2021-03-08 10:32:20 -07:00
export interface CameraEventProps {
2021-02-19 08:07:53 -07:00
/**
* Called when any kind of runtime error occured.
*/
onError?: (error: CameraRuntimeError) => void;
/**
* Called when the camera was successfully initialized.
*/
onInitialized?: () => void;
2021-03-08 10:32:20 -07:00
}
2021-02-19 08:07:53 -07:00
export type CameraProps = (CameraPresetProps | CameraFormatProps) &
(CameraScannerPropsNever | CameraScannerProps) &
CameraDeviceProps &
CameraDynamicProps &
CameraEventProps &
ViewProps;
2021-02-19 08:23:54 -07:00
export type CameraPermissionStatus = 'authorized' | 'not-determined' | 'denied' | 'restricted';
export type CameraPermissionRequestResult = 'authorized' | 'denied';
2021-02-19 08:07:53 -07:00
interface OnErrorEvent {
code: string;
message: string;
cause?: ErrorWithCause;
}
interface OnCodeScannedEvent {
codes: Code[];
}
//#endregion
// NativeModules automatically resolves 'CameraView' to 'CameraViewModule'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const CameraModule = NativeModules.CameraView;
2021-02-19 08:23:54 -07:00
if (CameraModule == null) console.error("Camera: Native Module 'CameraView' was null! Did you run pod install?");
2021-02-19 08:07:53 -07:00
interface CameraState {
/**
* The actual native ID for the camera device.
*/
cameraId?: string;
}
type RefType = React.Component<CameraProps> & Readonly<NativeMethods>;
/**
* ### A powerful `<Camera>` component.
*
2021-03-08 10:51:53 -07:00
* Read the [VisionCamera documentation](https://cuvent.github.io/react-native-vision-camera/) for more information.
*
* The `<Camera>` component's most important (and therefore _required_) properties are:
*
2021-03-08 10:51:53 -07:00
* * {@linkcode CameraDeviceProps.device | device}: Specifies the {@linkcode CameraDevice} to use. Get a {@linkcode CameraDevice} by using the {@linkcode useCameraDevices | useCameraDevices()} hook, or manually by using the {@linkcode Camera.getAvailableCameraDevices Camera.getAvailableCameraDevices()} function.
* * {@linkcode CameraDynamicProps.isActive | isActive}: A boolean value that specifies whether the Camera should actively stream video frames or not. This can be compared to a Video component, where `isActive` specifies whether the video is paused or not. If you fully unmount the `<Camera>` component instead of using `isActive={false}`, the Camera will take a bit longer to start again.
*
* @example
2021-03-09 04:02:10 -07:00
* ```tsx
* function App() {
* const devices = useCameraDevices('wide-angle-camera')
* const device = devices.back
*
* if (device == null) return <LoadingView />
* return (
* <Camera
* style={StyleSheet.absoluteFill}
* device={device}
* isActive={true}
* />
* )
* }
* ```
*
* @component
2021-02-19 08:07:53 -07:00
*/
export class Camera extends React.PureComponent<CameraProps, CameraState> {
2021-03-07 06:23:14 -07:00
/**
* @internal
*/
2021-02-19 08:23:54 -07:00
static displayName = 'Camera';
2021-03-07 06:23:14 -07:00
/**
* @internal
*/
2021-02-19 08:07:53 -07:00
displayName = Camera.displayName;
private readonly ref: React.RefObject<RefType>;
2021-03-07 06:23:14 -07:00
/**
* @internal
*/
2021-02-19 08:07:53 -07:00
constructor(props: CameraProps) {
super(props);
this.state = { cameraId: undefined };
this.onInitialized = this.onInitialized.bind(this);
this.onError = this.onError.bind(this);
this.onCodeScanned = this.onCodeScanned.bind(this);
this.ref = React.createRef<RefType>();
}
private get handle(): number | null {
const nodeHandle = findNodeHandle(this.ref.current);
2021-02-19 08:23:54 -07:00
if (nodeHandle == null) console.error('Camera: findNodeHandle(ref) returned null! Does the Camera view exist in the native view tree?');
2021-02-19 08:07:53 -07:00
return nodeHandle;
}
//#region View-specific functions (UIViewManager)
/**
* Take a single photo and write it's content to a temporary file.
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraCaptureError} When any kind of error occured while capturing the photo. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
2021-03-08 10:30:23 -07:00
* @example
2021-03-09 04:02:10 -07:00
* ```ts
2021-03-08 10:30:23 -07:00
* const photo = await camera.current.takePhoto({
* qualityPrioritization: 'quality',
* flash: 'on',
* enableAutoRedEyeReduction: true
* })
* ```
2021-02-19 08:07:53 -07:00
*/
public async takePhoto(options?: TakePhotoOptions): Promise<PhotoFile> {
try {
return await CameraModule.takePhoto(this.handle, options ?? {});
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Take a snapshot of the current preview view.
*
2021-03-08 10:51:53 -07:00
* This can be used as an alternative to {@linkcode Camera.takePhoto | takePhoto()} if speed is more important than quality
*
* @throws {@linkcode CameraCaptureError} When any kind of error occured while taking a snapshot. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*
* @platform Android
2021-03-08 10:30:23 -07:00
* @example
2021-03-09 04:02:10 -07:00
* ```ts
2021-03-08 10:30:23 -07:00
* const photo = await camera.current.takeSnapshot({
* quality: 85,
* skipMetadata: true
* })
* ```
2021-02-19 08:07:53 -07:00
*/
public async takeSnapshot(options?: TakeSnapshotOptions): Promise<PhotoFile> {
2021-02-19 08:23:54 -07:00
if (Platform.OS !== 'android')
throw new CameraCaptureError('capture/capture-type-not-supported', `'takeSnapshot()' is not available on ${Platform.OS}!`);
2021-02-19 08:07:53 -07:00
try {
return await CameraModule.takeSnapshot(this.handle, options ?? {});
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Start a new video recording.
*
* Records in the following formats:
* * **iOS**: QuickTime (`.mov`)
* * **Android**: MPEG4 (`.mp4`)
*
* @blocking This function is synchronized/blocking.
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraCaptureError} When any kind of error occured while starting the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*
* @example
2021-03-09 04:02:10 -07:00
* ```ts
2021-02-19 08:07:53 -07:00
* camera.current.startRecording({
* onRecordingFinished: (video) => console.log(video),
* onRecordingError: (error) => console.error(error),
* })
* setTimeout(() => {
* camera.current.stopRecording()
* }, 5000)
* ```
2021-02-19 08:07:53 -07:00
*/
public startRecording(options: RecordVideoOptions): void {
2021-02-19 08:23:54 -07:00
const { onRecordingError, onRecordingFinished, ...passThroughOptions } = options;
if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function')
throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!');
const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => {
2021-02-19 08:07:53 -07:00
if (error != null) return onRecordingError(error);
if (video != null) return onRecordingFinished(video);
};
// TODO: Use TurboModules to either make this a sync invokation, or make it async.
try {
2021-02-19 08:23:54 -07:00
CameraModule.startRecording(this.handle, passThroughOptions, onRecordCallback);
2021-02-19 08:07:53 -07:00
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Stop the current video recording.
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraCaptureError} When any kind of error occured while stopping the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error
2021-03-08 10:16:45 -07:00
*
2021-02-19 08:07:53 -07:00
* @example
2021-03-09 04:02:10 -07:00
* ```ts
2021-02-19 08:07:53 -07:00
* await camera.current.startRecording()
* setTimeout(async () => {
* const video = await camera.current.stopRecording()
* }, 5000)
* ```
2021-02-19 08:07:53 -07:00
*/
public async stopRecording(): Promise<void> {
try {
return await CameraModule.stopRecording(this.handle);
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Focus the camera to a specific point in the coordinate system.
* @param {Point} point The point to focus to. This should be relative to the Camera view's coordinate system,
2021-02-19 08:07:53 -07:00
* and expressed in Pixel on iOS and Points on Android.
* * `(0, 0)` means **top left**.
* * `(CameraView.width, CameraView.height)` means **bottom right**.
*
* Make sure the value doesn't exceed the CameraView's dimensions.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-03-08 10:30:23 -07:00
* @example
2021-03-09 04:02:10 -07:00
* ```ts
2021-03-08 10:30:23 -07:00
* await camera.current.focus({
* x: tapEvent.x,
* y: tapEvent.y
* })
* ```
2021-02-19 08:07:53 -07:00
*/
public async focus(point: Point): Promise<void> {
try {
return await CameraModule.focus(this.handle, point);
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Get a list of video codecs the current camera supports. Returned values are ordered by efficiency (descending).
*
* This function can only be called after the camera has been initialized,
2021-03-08 10:51:53 -07:00
* so only use this after the {@linkcode onInitialized | onInitialized()} event has fired.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @platform iOS
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while getting available video codecs. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*/
public async getAvailableVideoCodecs(): Promise<CameraVideoCodec[]> {
try {
return await CameraModule.getAvailableVideoCodecs(this.handle);
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Get a list of photo codecs the current camera supports. Returned values are ordered by efficiency (descending).
*
* This function can only be called after the camera has been initialized,
2021-03-08 10:51:53 -07:00
* so only use this after the {@linkcode onInitialized | onInitialized()} event has fired.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @platform iOS
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while getting available photo codecs. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*/
public async getAvailablePhotoCodecs(): Promise<CameraPhotoCodec[]> {
try {
return await CameraModule.getAvailablePhotoCodecs(this.handle);
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
//#endregion
//#region Static Functions (NativeModule)
/**
* Get a list of all available camera devices on the current phone.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while getting all available camera devices. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-03-08 10:30:23 -07:00
* @example
2021-03-09 04:02:10 -07:00
* ```ts
2021-03-08 10:30:23 -07:00
* const devices = await Camera.getAvailableCameraDevices()
* const filtered = devices.filter((d) => matchesMyExpectations(d))
* const sorted = devices.sort(sortDevicesByAmountOfCameras)
* return {
* back: sorted.find((d) => d.position === "back"),
* front: sorted.find((d) => d.position === "front")
* }
* ```
2021-02-19 08:07:53 -07:00
*/
public static async getAvailableCameraDevices(): Promise<CameraDevice[]> {
try {
return await CameraModule.getAvailableCameraDevices();
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Gets the current Camera Permission Status. Check this before mounting the Camera to ensure
* the user has permitted the app to use the camera.
*
2021-03-08 10:51:53 -07:00
* To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while getting the current permission status. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*/
public static async getCameraPermissionStatus(): Promise<CameraPermissionStatus> {
try {
return await CameraModule.getCameraPermissionStatus();
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Gets the current Microphone-Recording Permission Status. Check this before mounting the Camera to ensure
* the user has permitted the app to use the microphone.
*
2021-03-08 10:51:53 -07:00
* To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while getting the current permission status. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*/
public static async getMicrophonePermissionStatus(): Promise<CameraPermissionStatus> {
try {
return await CameraModule.getMicrophonePermissionStatus();
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Shows a "request permission" alert to the user, and resolves with the new camera permission status.
*
* If the user has previously blocked the app from using the camera, the alert will not be shown
* and `"denied"` will be returned.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*/
public static async requestCameraPermission(): Promise<CameraPermissionRequestResult> {
try {
return await CameraModule.requestCameraPermission();
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
/**
* Shows a "request permission" alert to the user, and resolves with the new microphone permission status.
*
* If the user has previously blocked the app from using the microphone, the alert will not be shown
* and `"denied"` will be returned.
2021-03-08 10:16:45 -07:00
*
2021-03-08 10:51:53 -07:00
* @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error
2021-02-19 08:07:53 -07:00
*/
public static async requestMicrophonePermission(): Promise<CameraPermissionRequestResult> {
try {
return await CameraModule.requestMicrophonePermission();
} catch (e) {
throw tryParseNativeCameraError(e);
}
}
//#endregion
//#region Events (Wrapped to maintain reference equality)
2021-02-19 08:23:54 -07:00
private onError(event?: NativeSyntheticEvent<OnErrorEvent>): void {
if (event == null) throw new Error('onError() was invoked but event was null!');
2021-02-19 08:07:53 -07:00
if (this.props.onError != null) {
const error = event.nativeEvent;
const cause = isErrorWithCause(error.cause) ? error.cause : undefined;
this.props.onError(
// @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work
2021-02-19 08:23:54 -07:00
new CameraRuntimeError(error.code, error.message, cause),
2021-02-19 08:07:53 -07:00
);
}
}
2021-02-19 08:23:54 -07:00
private onInitialized(): void {
2021-02-19 08:07:53 -07:00
this.props.onInitialized?.();
}
2021-02-19 08:23:54 -07:00
private onCodeScanned(event?: NativeSyntheticEvent<OnCodeScannedEvent>): void {
if (event == null) throw new Error('onCodeScanned() was invoked but event was null!');
if (this.props.onCodeScanned == null)
console.warn('Camera: onCodeScanned event was invoked but no listeners attached! Did you forget to remove the `scannableCodes` property?');
else this.props.onCodeScanned(event.nativeEvent.codes);
2021-02-19 08:07:53 -07:00
}
//#endregion
2021-03-07 06:23:14 -07:00
/**
* @internal
*/
2021-02-19 08:23:54 -07:00
static getDerivedStateFromProps(props: CameraProps, state: CameraState): CameraState | null {
2021-02-19 08:07:53 -07:00
const newCameraId = props.device.id;
2021-02-19 08:23:54 -07:00
if (state.cameraId !== newCameraId) return { ...state, cameraId: newCameraId };
2021-02-19 08:07:53 -07:00
return null;
}
2021-03-07 06:23:14 -07:00
/**
* @internal
*/
2021-02-19 08:07:53 -07:00
public render(): React.ReactNode {
// We remove the big `device` object from the props because we only need to pass `cameraId` to native.
const { device: _, ...props } = this.props;
return (
<NativeCameraView
{...props}
cameraId={this.state.cameraId}
ref={this.ref}
onInitialized={this.onInitialized}
// @ts-expect-error with our callback wrapping we have to extract NativeSyntheticEvent params
onError={this.onError}
// @ts-expect-error with our callback wrapping we have to extract NativeSyntheticEvent params
onCodeScanned={this.onCodeScanned}
/>
);
}
}
// requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager'
const NativeCameraView = requireNativeComponent<CameraProps>(
2021-02-19 08:23:54 -07:00
'CameraView',
2021-02-19 08:07:53 -07:00
// @ts-expect-error because the type declarations are kinda wrong, no?
2021-02-19 08:23:54 -07:00
Camera,
2021-02-19 08:07:53 -07:00
);