import { useEffect, useMemo } from 'react' import { getCameraDevice } from '../devices/getCameraDevice' import type { CameraController, CameraControllerConfiguration, } from '../specs/CameraController.nitro' import type { CameraOrientation } from '../specs/common-types/CameraOrientation' import type { CameraPosition } from '../specs/common-types/CameraPosition' import type { Constraint } from '../specs/common-types/Constraint' import type { MirrorMode } from '../specs/common-types/MirrorMode' import type { OrientationSource } from '../specs/common-types/OrientationSource' import type { CameraDevice } from '../specs/inputs/CameraDevice.nitro' import type { CameraOutput } from '../specs/outputs/CameraOutput.nitro' import type { CameraVideoOutput } from '../specs/outputs/CameraVideoOutput.nitro' import type { CameraOutputConfiguration } from '../specs/session/CameraOutputConfiguration' import type { CameraSession, InterruptionReason, } from '../specs/session/CameraSession.nitro' import type { CameraSessionConfig } from '../specs/session/CameraSessionConfig.nitro' import type { CameraSessionConnection } from '../specs/session/CameraSessionConnection' import { useCameraController } from './internal/useCameraController' import { useCameraControllerConfiguration } from './internal/useCameraControllerConfiguration' import { useCameraSession } from './internal/useCameraSession' import { useCameraSessionIsRunning } from './internal/useCameraSessionIsRunning' import { useListenerSubscription } from './internal/useListenerSubscription' import { useCameraDevices } from './useCameraDevices' import { useOrientation } from './useOrientation' export interface CameraProps { // Session Configuration /** * Starts the {@linkcode CameraSession} when set to `true`, and stops it * when set back to `false`. * * @see {@linkcode CameraSession.start | CameraSession.start()} * @see {@linkcode CameraSession.stop | CameraSession.stop()} * @see {@linkcode CameraSession.isRunning} */ isActive: boolean // Connection Configuration /** * The {@linkcode CameraDevice} to open, or a {@linkcode CameraPosition} * (e.g. `'back'`) to auto-pick a matching device via * {@linkcode getCameraDevice | getCameraDevice(...)}. * * @see {@linkcode CameraSessionConnection.input} */ device: CameraDevice | CameraPosition /** * The {@linkcode CameraOutput}s the {@linkcode device} will stream into. * * @see {@linkcode CameraSessionConnection.outputs} * @see {@linkcode CameraOutputConfiguration.output} */ outputs?: CameraOutput[] /** * {@linkcode Constraint}s (e.g. `{ fps: 60 }`) that the Camera pipeline * will try to match when configuring the {@linkcode CameraSession}. * * @see {@linkcode CameraSessionConnection.constraints} */ constraints?: Constraint[] /** * Called once the given {@linkcode constraints} have been fully resolved * into a concrete {@linkcode CameraSessionConfig}. * * @see {@linkcode CameraSessionConnection.onSessionConfigSelected} */ onSessionConfigSelected?: (config: CameraSessionConfig) => void /** * Set a desired {@linkcode OrientationSource} * for automatically applying {@linkcode CameraOrientation} * to all {@linkcode outputs}, or `'custom'` if you * prefer to manually specify {@linkcode CameraOrientation} * yourself. * * @see {@linkcode CameraOutput.outputOrientation} */ orientationSource?: OrientationSource | 'custom' /** * Sets whether the {@linkcode CameraOutput}s are mirrored along * the vertical axis. {@linkcode MirrorMode | 'auto'} mirrors * automatically on selfie cameras. * * @see {@linkcode CameraOutputConfiguration.mirrorMode} * @default 'auto' */ mirrorMode?: MirrorMode // Camera Controller Configuration /** * If `true`, auto-focus transitions are performed slower and smoother * to appear less intrusive in video recordings. * * @see {@linkcode CameraControllerConfiguration.enableSmoothAutoFocus} * @platform iOS * @default false */ enableSmoothAutoFocus?: boolean /** * If `true`, the Camera pipeline may extend exposure times (effectively * dropping frame rate) in low-light scenes to receive more light. * * @see {@linkcode CameraControllerConfiguration.enableLowLightBoost} * @default false */ enableLowLightBoost?: boolean /** * If `true`, geometric distortion at the edges (e.g. on ultra-wide-angle * cameras) is corrected, at the cost of a small amount of field of view. * * @see {@linkcode CameraControllerConfiguration.enableDistortionCorrection} * @platform iOS * @default true */ enableDistortionCorrection?: boolean // Initial Props for Controller /** * A getter for the initial zoom value to apply when the * {@linkcode CameraController} is created. Later, the zoom can be * adjusted via {@linkcode CameraController.setZoom | CameraController.setZoom(...)}. * * @see {@linkcode CameraSessionConnection.initialZoom} */ getInitialZoom?: () => number | undefined /** * A getter for the initial exposure bias to apply when the * {@linkcode CameraController} is created. Later, the exposure bias can be * adjusted via {@linkcode CameraController.setExposureBias | CameraController.setExposureBias(...)}. * * @see {@linkcode CameraSessionConnection.initialExposureBias} */ getInitialExposureBias?: () => number | undefined // Callbacks /** * Called whenever the {@linkcode CameraSession} * has been configured with new connections via * {@linkcode CameraSession.configure | configure(...)} * and connections to the individual outputs are formed. * * This is a good place to check output * capabilities, such as {@linkcode CameraVideoOutput.getSupportedVideoCodecs | CameraVideoOutput.getSupportedVideoCodecs()} */ onConfigured?: () => void /** * Called when the {@linkcode CameraSession} * has been started. * * @see {@linkcode CameraSession.addOnStartedListener} */ onStarted?: () => void /** * Called when the {@linkcode CameraSession} * has been stopped. * * @see {@linkcode CameraSession.addOnStoppedListener} */ onStopped?: () => void /** * Called whenever the {@linkcode CameraSession} * has encountered an error. * * @see {@linkcode CameraSession.addOnErrorListener} */ onError?: (error: Error) => void /** * Called whenever the {@linkcode CameraSession} * has encountered an interruption of the given * {@linkcode InterruptionReason}. * Interruptions are temporarily. * * @see {@linkcode CameraSession.addOnInterruptionStartedListener} */ onInterruptionStarted?: (interruption: InterruptionReason) => void /** * Called when a previous interruption * has ended and the {@linkcode CameraSession} * is running uninterrupted again. * * @see {@linkcode CameraSession.addOnInterruptionEndedListener} */ onInterruptionEnded?: () => void /** * Called when the subject area substantially changes, * e.g. when the user pans away from a scene that was * previously in focus. * * This is a good point to reset any locked AE/AF/AWB * focus states back to continuously auto-focus. * * @see {@linkcode CameraController.addSubjectAreaChangedListener} * @platform iOS */ onSubjectAreaChanged?: () => void } function defaultOnErrorHandler(error: Error) { console.error(error) } /** * Use the Camera. * * This creates a {@linkcode CameraSession}, manages * the input and outputs (including orientation and * mirror modes), wraps listeners as stable React * callbacks, and returns a {@linkcode CameraController}. * * @example * ```ts * const camera = useCamera({ * isActive: true, * device: 'back', * outputs: [] * }) * ``` */ export function useCamera({ isActive, device, outputs = [], constraints, onSessionConfigSelected, mirrorMode, onConfigured, orientationSource = 'device', onStarted, onStopped, onError = defaultOnErrorHandler, onInterruptionStarted, onInterruptionEnded, onSubjectAreaChanged, enableDistortionCorrection, enableLowLightBoost, enableSmoothAutoFocus, getInitialExposureBias, getInitialZoom, }: CameraProps): CameraController | undefined { // 1. Create session const session = useCameraSession({ enableMultiCamSupport: false }) // 2. Update output orientations const orientationSourceOrUndefined = orientationSource === 'custom' ? undefined : orientationSource const orientation = useOrientation(orientationSourceOrUndefined) useEffect(() => { if (orientation == null) return for (const output of outputs) { output.outputOrientation = orientation } }, [orientation, outputs]) // 3. Get the input - either find one via position, or use the user provided one const devices = useCameraDevices() const input = useMemo(() => { if (typeof device === 'string') { // The user passed a `CameraPosition` (e.g. "back") - try to find a device ourselves const position = device const foundDevice = getCameraDevice(devices, position) if (foundDevice == null) { throw new Error(`This device does not have any "${position}" Cameras!`) } return foundDevice } else { // The user passed an actual device. return as-is. return device } }, [device, devices]) // 4. Configure the session with the input + outputs to create a `CameraController` const controller = useCameraController(session, input, outputs, { mirrorMode: mirrorMode, onConfigured: onConfigured, getInitialExposureBias: getInitialExposureBias, getInitialZoom: getInitialZoom, constraints: constraints, onSessionConfigSelected: onSessionConfigSelected, }) // 5. Configure the Controller with some settings useCameraControllerConfiguration(controller, { enableSmoothAutoFocus: enableSmoothAutoFocus, enableDistortionCorrection: enableDistortionCorrection, enableLowLightBoost: enableLowLightBoost, }) // 6. Start (or stop) the Session if we have a Controller and `isActive` is true. const hasController = controller != null useCameraSessionIsRunning(session, isActive && hasController) // 7. Set up listeners and delegate to JS useListenerSubscription(session, 'addOnStartedListener', onStarted) useListenerSubscription(session, 'addOnStoppedListener', onStopped) useListenerSubscription(session, 'addOnErrorListener', onError) useListenerSubscription( session, 'addOnInterruptionStartedListener', onInterruptionStarted, ) useListenerSubscription( session, 'addOnInterruptionEndedListener', onInterruptionEnded, ) useListenerSubscription( controller, 'addSubjectAreaChangedListener', onSubjectAreaChanged, ) // 8. Give the user the controller return controller }