import React, { type Ref, useImperativeHandle, useMemo, useRef } from 'react' import type { ViewProps } from 'react-native' import { callback } from 'react-native-nitro-modules' import type { SharedValue } from 'react-native-reanimated' import { useExposureUpdater } from '../hooks/internal/useExposureUpdater' import { useGestureControllers } from '../hooks/internal/useGestureControllers' import { useTorchModeUpdater } from '../hooks/internal/useTorchModeUpdater' import { useZoomUpdater } from '../hooks/internal/useZoomUpdater' import { type CameraProps, useCamera } from '../hooks/useCamera' import { usePreviewOutput } from '../hooks/usePreviewOutput' import type { CameraController } from '../specs/CameraController.nitro' import type { FocusOptions, MeteringMode, } from '../specs/common-types/FocusOptions' import type { Point } from '../specs/common-types/Point' import type { TorchMode } from '../specs/common-types/TorchMode' import type { PreviewView, PreviewViewMethods, PreviewViewProps, } from '../specs/views/PreviewView.nitro' import { NativePreviewView } from './NativePreviewView' /** * A React-ref to the {@linkcode Camera} view. * @example * ```tsx * function App() { * const camera = useRef(null) * return * } * ``` */ export interface CameraRef extends PreviewViewMethods, Pick< CameraController, 'resetFocus' | 'startZoomAnimation' | 'cancelZoomAnimation' > { /** * Focuses the Camera pipeline to the specified {@linkcode viewPoint} * relative to the Camera view's coordinate system using * the specified {@linkcode MeteringMode}s. * @param viewPoint The point in the view coordinate system to focus to. * @param options Options for the focus operation. * @throws If the Camera isn't ready yet. * @example * ```tsx * // Focus center * await camera.current.focusTo({ x: width / 2, y: height / 2 }) * ``` */ focusTo(viewPoint: Point, options?: FocusOptions): Promise /** * Get a ref to the {@linkcode PreviewView}, * or `undefined` if it has not yet been set. * This value is set after the first * mount, and usually won't change. */ preview: PreviewView | undefined /** * Get the current {@linkcode CameraController}. * This property will be set after `onStarted` * has been called, and may change over time. */ controller: CameraController | undefined } /** * Props for the {@linkcode Camera} component. * * Extends {@linkcode CameraProps} (the options for the underlying * {@linkcode useCamera} hook) with {@linkcode PreviewView}-specific options * and convenience props for gesture handling and animated values. */ export interface CameraViewProps extends CameraProps, Pick, Pick { // PreviewView props /** * Called when the {@linkcode Camera}'s Preview * received its first Frame. */ onPreviewStarted?: () => void /** * Called when the {@linkcode Camera}'s Preview * stopped streaming Frames. */ onPreviewStopped?: () => void // Native gestures /** * Enables (or disables) the native pinch-to-zoom gesture. * * @throws If this property is enabled and {@linkcode CameraViewProps.zoom | zoom} is not `undefined`. * @default false */ enableNativeZoomGesture?: boolean /** * Enables (or disables) the native tap-to-focus gesture. * * @default false */ enableNativeTapToFocusGesture?: boolean // Controller props /** * Sets the {@linkcode CameraController.zoom | zoom} value. * * You can also manually set zoom via * {@linkcode CameraController.setZoom | setZoom(...)}. * * @note This property can be animated via Reanimated by passing a {@linkcode SharedValue}. * @throws If this property is set and {@linkcode enableNativeZoomGesture} is enabled. * @default 1 */ zoom?: number | SharedValue /** * Sets the {@linkcode CameraController.exposureBias | exposureBias} value. * * You can also manually set the exposure bias via * {@linkcode CameraController.setExposureBias | setExposureBias(...)}. * * @note This property can be animated via Reanimated by passing a {@linkcode SharedValue}. * @default 0 */ exposure?: number | SharedValue /** * Sets the {@linkcode CameraController.torchMode | torchMode} value. * @default 'off' */ torchMode?: TorchMode // Ref Wrapper /** * @see {@linkcode CameraRef} */ ref?: Ref } function getAnimatableNumberInitialValue( value: number | SharedValue | undefined, ): number | undefined { if (value == null) return undefined else if (typeof value === 'number') return value else return value.get() } function CameraImpl({ implementationMode, resizeMode, onPreviewStarted, onPreviewStopped, outputs = [], zoom, exposure, torchMode, enableNativeZoomGesture = false, enableNativeTapToFocusGesture = false, ref, ...props }: CameraViewProps): React.ReactElement { // 1. Create `PreviewOutput` for the Camera const previewOutput = usePreviewOutput() // 3. Create session/controller and add the preview output const controller = useCamera({ ...props, outputs: [previewOutput, ...outputs], getInitialExposureBias: () => getAnimatableNumberInitialValue(exposure), getInitialZoom: () => getAnimatableNumberInitialValue(zoom), }) // 4. Create `ref` for `PreviewView` const previewViewRef = useRef(null) const setHybridRef = useMemo( () => callback((r: PreviewView) => { previewViewRef.current = r }), [], ) // 5. Create a ref that exposes some funcs on the Controller and the PreviewView. useImperativeHandle( ref, () => ({ startZoomAnimation(zoom, rate) { if (controller == null) throw new Error(`Camera is not yet ready!`) return controller.startZoomAnimation(zoom, rate) }, cancelZoomAnimation() { if (controller == null) throw new Error(`Camera is not yet ready!`) return controller.cancelZoomAnimation() }, async focusTo(viewPoint, options = {}) { if (controller == null) throw new Error(`Camera is not yet ready!`) if (previewViewRef.current == null) throw new Error(`Camera Preview is not yet ready!`) const meteringPoint = previewViewRef.current.createMeteringPoint( viewPoint.x, viewPoint.y, ) await controller.focusTo(meteringPoint, options) }, async resetFocus() { if (controller == null) throw new Error(`Camera is not yet ready!`) await controller.resetFocus() }, createMeteringPoint(x, y, size) { if (previewViewRef.current == null) throw new Error(`Camera Preview is not yet ready!`) return previewViewRef.current.createMeteringPoint(x, y, size) }, takeSnapshot() { if (previewViewRef.current == null) throw new Error(`Camera Preview is not yet ready!`) return previewViewRef.current.takeSnapshot() }, convertCameraPointToViewPoint(cameraPoint) { if (previewViewRef.current == null) throw new Error(`Camera Preview is not yet ready!`) return previewViewRef.current.convertCameraPointToViewPoint(cameraPoint) }, convertViewPointToCameraPoint(viewPoint) { if (previewViewRef.current == null) throw new Error(`Camera Preview is not yet ready!`) return previewViewRef.current.convertViewPointToCameraPoint(viewPoint) }, convertScannedObjectCoordinatesToViewCoordinates(scannedObject) { if (previewViewRef.current == null) throw new Error(`Camera Preview is not yet ready!`) return previewViewRef.current.convertScannedObjectCoordinatesToViewCoordinates( scannedObject, ) }, get preview(): PreviewView | undefined { return previewViewRef.current ?? undefined }, get controller(): CameraController | undefined { return controller }, }), [controller], ) // 6. Update CameraController props useZoomUpdater(controller, zoom) useExposureUpdater(controller, exposure) useTorchModeUpdater(controller, torchMode) // 7. Attach any native gesture controllers if (enableNativeZoomGesture && zoom != null) { throw new Error( `\`zoom\` must not be set if \`enableNativeZoomGesture\` is enabled!`, ) } const gestureControllers = useGestureControllers({ controller: controller, enableNativeZoomGesture: enableNativeZoomGesture, enableNativeTapToFocusGesture: enableNativeTapToFocusGesture, }) // 8. Render view return ( ) } /** * The `` component. * * This is a convenience wrapper around {@linkcode useCamera | useCamera(...)} * that adds a {@linkcode PreviewView}, wraps methods in a {@linkcode CameraRef}, * and supports updating {@linkcode CameraViewProps.zoom | zoom} and * {@linkcode CameraViewProps.exposure | exposure} via * Reanimated {@linkcode SharedValue}s. * * @example * ```tsx * function App() { * const camera = useRef(null) * const device = useCameraDevice('back') * const photoOutput = usePhotoOutput() * * return ( * * ) * } * ``` */ export const Camera = React.memo(CameraImpl)