From 413be519d5bdc04c38d831191c8fcea52df2963e Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Thu, 1 Feb 2024 18:13:41 -0700 Subject: [PATCH] Add onVideoChunkReady callback --- .../com/mrousavy/camera/CameraView+Events.kt | 10 + .../java/com/mrousavy/camera/CameraView.kt | 5 + .../com/mrousavy/camera/CameraViewManager.kt | 1 + .../com/mrousavy/camera/core/CameraSession.kt | 5 +- .../mrousavy/camera/core/ChunkedRecorder.kt | 14 +- .../mrousavy/camera/core/RecordingSession.kt | 4 +- package/src/Camera.tsx | 811 +++++++++--------- 7 files changed, 440 insertions(+), 410 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt index 4b18754..7061f0b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt @@ -11,6 +11,7 @@ import com.mrousavy.camera.core.CodeScannerFrame import com.mrousavy.camera.core.UnknownCameraError import com.mrousavy.camera.core.code import com.mrousavy.camera.types.CodeType +import java.io.File fun CameraView.invokeOnInitialized() { Log.i(CameraView.TAG, "invokeOnInitialized()") @@ -33,6 +34,15 @@ fun CameraView.invokeOnStopped() { reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStopped", null) } +fun CameraView.invokeOnChunkReady(filepath: File, index: Int) { + Log.e(CameraView.TAG, "invokeOnError(...):") + val event = Arguments.createMap() + event.putInt("index", index) + event.putString("filepath", filepath.toString()) + val reactContext = context as ReactContext + reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "onVideoChunkReady", event) +} + fun CameraView.invokeOnError(error: Throwable) { Log.e(CameraView.TAG, "invokeOnError(...):") error.printStackTrace() diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index a3d86ff..204258c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -25,6 +25,7 @@ import com.mrousavy.camera.types.Torch import com.mrousavy.camera.types.VideoStabilizationMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import java.io.File // // TODOs for the CameraView which are currently too hard to implement either because of CameraX' limitations, or my brain capacity. @@ -265,6 +266,10 @@ class CameraView(context: Context) : invokeOnStopped() } + override fun onVideoChunkReady(filepath: File, index: Int) { + invokeOnChunkReady(filepath, index) + } + override fun onCodeScanned(codes: List, scannerFrame: CodeScannerFrame) { invokeOnCodeScanned(codes, scannerFrame) } diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index 54f7fce..13bf33a 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -29,6 +29,7 @@ class CameraViewManager : ViewGroupManager() { .put("cameraStopped", MapBuilder.of("registrationName", "onStopped")) .put("cameraError", MapBuilder.of("registrationName", "onError")) .put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned")) + .put("onVideoChunkReady", MapBuilder.of("registrationName", "onVideoChunkReady")) .build() override fun getName(): String = TAG diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 5b8d76e..fbb1918 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -54,6 +54,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import java.io.File class CameraSession(private val context: Context, private val cameraManager: CameraManager, private val callback: Callback) : CameraManager.AvailabilityCallback(), @@ -640,7 +641,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam orientation, options, callback, - onError + onError, + this.callback, ) recording.start() this.recording = recording @@ -724,6 +726,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam fun onInitialized() fun onStarted() fun onStopped() + fun onVideoChunkReady(filepath: File, index: Int) fun onCodeScanned(codes: List, scannerFrame: CodeScannerFrame) } } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt b/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt index 5d08166..e1626d3 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/ChunkedRecorder.kt @@ -13,12 +13,13 @@ import com.mrousavy.camera.types.RecordVideoOptions import java.io.File import java.nio.ByteBuffer -class ChunkedRecordingManager(private val encoder: MediaCodec, private val outputDirectory: File, private val orientationHint: Int, private val iFrameInterval: Int) : +class ChunkedRecordingManager(private val encoder: MediaCodec, private val outputDirectory: File, private val orientationHint: Int, private val iFrameInterval: Int, private val callbacks: CameraSession.Callback) : MediaCodec.Callback() { companion object { private const val TAG = "ChunkedRecorder" fun fromParams( + callbacks: CameraSession.Callback, size: Size, enableAudio: Boolean, fps: Int? = null, @@ -57,7 +58,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu // Create a MediaCodec encoder, and configure it with our format. Get a Surface // we can use for input and wrap it with a class that handles the EGL work. codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) - return ChunkedRecordingManager(codec, outputDirectory, 0, iFrameInterval) + return ChunkedRecordingManager(codec, outputDirectory, 0, iFrameInterval, callbacks) } } @@ -79,7 +80,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu } // Muxer specific - private class MuxerContext(val muxer: MediaMuxer, startTimeUs: Long, encodedFormat: MediaFormat) { + private class MuxerContext(val muxer: MediaMuxer, val filepath: File, val chunkIndex: Int, startTimeUs: Long, encodedFormat: MediaFormat) { val videoTrack: Int = muxer.addTrack(encodedFormat) val startTimeUs: Long = startTimeUs @@ -97,7 +98,10 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu private var muxerContext: MuxerContext? = null private fun createNextMuxer(bufferInfo: BufferInfo) { - muxerContext?.finish() + muxerContext?.let { + it.finish() + this.callbacks.onVideoChunkReady(it.filepath, it.chunkIndex) + } chunkIndex++ val newFileName = "$chunkIndex.mp4" @@ -109,7 +113,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu ) muxer.setOrientationHint(orientationHint) muxerContext = MuxerContext( - muxer, bufferInfo.presentationTimeUs, this.encodedFormat!! + muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!! ) } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt index 88b08f3..d0a4c72 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt @@ -24,7 +24,8 @@ class RecordingSession( private val cameraOrientation: Orientation, private val options: RecordVideoOptions, private val callback: (video: Video) -> Unit, - private val onError: (error: CameraError) -> Unit + private val onError: (error: CameraError) -> Unit, + private val allCallbacks: CameraSession.Callback, ) { companion object { private const val TAG = "RecordingSession" @@ -45,6 +46,7 @@ class RecordingSession( private val bitRate = getBitRate() private val recorder = ChunkedRecordingManager.fromParams( + allCallbacks, size, enableAudio, fps, diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index be99be6..2d53358 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -18,32 +18,37 @@ export type CameraPermissionStatus = 'granted' | 'not-determined' | 'denied' | ' export type CameraPermissionRequestResult = 'granted' | 'denied' interface OnCodeScannedEvent { - codes: Code[] - frame: CodeScannerFrame + codes: Code[] + frame: CodeScannerFrame } interface OnErrorEvent { - code: string - message: string - cause?: ErrorWithCause + code: string + message: string + cause?: ErrorWithCause +} +interface OnVideoChunkReadyEvent { + filepath: string + index: int } type NativeCameraViewProps = Omit & { - cameraId: string - enableFrameProcessor: boolean - codeScannerOptions?: Omit - onInitialized?: (event: NativeSyntheticEvent) => void - onError?: (event: NativeSyntheticEvent) => void - onCodeScanned?: (event: NativeSyntheticEvent) => void - onStarted?: (event: NativeSyntheticEvent) => void - onStopped?: (event: NativeSyntheticEvent) => void - onViewReady: () => void + cameraId: string + enableFrameProcessor: boolean + codeScannerOptions?: Omit + onInitialized?: (event: NativeSyntheticEvent) => void + onError?: (event: NativeSyntheticEvent) => void + onCodeScanned?: (event: NativeSyntheticEvent) => void + onStarted?: (event: NativeSyntheticEvent) => void + onStopped?: (event: NativeSyntheticEvent) => void + onVideoChunkReady?: (event: NativeSyntheticEvent) => void + onViewReady: () => void } type NativeRecordVideoOptions = Omit & { - videoBitRateOverride?: number - videoBitRateMultiplier?: number + videoBitRateOverride?: number + videoBitRateMultiplier?: number } type RefType = React.Component & Readonly interface CameraState { - isRecordingWithFlash: boolean + isRecordingWithFlash: boolean } //#endregion @@ -77,427 +82,427 @@ interface CameraState { * @component */ export class Camera extends React.PureComponent { - /** @internal */ - static displayName = 'Camera' - /** @internal */ - displayName = Camera.displayName - private lastFrameProcessor: FrameProcessor | undefined - private isNativeViewMounted = false + /** @internal */ + static displayName = 'Camera' + /** @internal */ + displayName = Camera.displayName + private lastFrameProcessor: FrameProcessor | undefined + private isNativeViewMounted = false - private readonly ref: React.RefObject + private readonly ref: React.RefObject - /** @internal */ - constructor(props: CameraProps) { - super(props) - this.onViewReady = this.onViewReady.bind(this) - this.onInitialized = this.onInitialized.bind(this) - this.onStarted = this.onStarted.bind(this) - this.onStopped = this.onStopped.bind(this) - this.onError = this.onError.bind(this) - this.onCodeScanned = this.onCodeScanned.bind(this) - this.ref = React.createRef() - this.lastFrameProcessor = undefined - this.state = { - isRecordingWithFlash: false, - } - } + /** @internal */ + constructor(props: CameraProps) { + super(props) + this.onViewReady = this.onViewReady.bind(this) + this.onInitialized = this.onInitialized.bind(this) + this.onStarted = this.onStarted.bind(this) + this.onStopped = this.onStopped.bind(this) + this.onError = this.onError.bind(this) + this.onCodeScanned = this.onCodeScanned.bind(this) + this.ref = React.createRef() + this.lastFrameProcessor = undefined + this.state = { + isRecordingWithFlash: false, + } + } - private get handle(): number { - const nodeHandle = findNodeHandle(this.ref.current) - if (nodeHandle == null || nodeHandle === -1) { - throw new CameraRuntimeError( - 'system/view-not-found', - "Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?", - ) - } + private get handle(): number { + const nodeHandle = findNodeHandle(this.ref.current) + if (nodeHandle == null || nodeHandle === -1) { + throw new CameraRuntimeError( + 'system/view-not-found', + "Could not get the Camera's native view tag! Does the Camera View exist in the native view-tree?", + ) + } - return nodeHandle - } + return nodeHandle + } - //#region View-specific functions (UIViewManager) - /** - * Take a single photo and write it's content to a temporary file. - * - * @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 - * @example - * ```ts - * const photo = await camera.current.takePhoto({ - * qualityPrioritization: 'quality', - * flash: 'on', - * enableAutoRedEyeReduction: true - * }) - * ``` - */ - public async takePhoto(options?: TakePhotoOptions): Promise { - try { - return await CameraModule.takePhoto(this.handle, options ?? {}) - } catch (e) { - throw tryParseNativeCameraError(e) - } - } + //#region View-specific functions (UIViewManager) + /** + * Take a single photo and write it's content to a temporary file. + * + * @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 + * @example + * ```ts + * const photo = await camera.current.takePhoto({ + * qualityPrioritization: 'quality', + * flash: 'on', + * enableAutoRedEyeReduction: true + * }) + * ``` + */ + public async takePhoto(options?: TakePhotoOptions): Promise { + try { + return await CameraModule.takePhoto(this.handle, options ?? {}) + } catch (e) { + throw tryParseNativeCameraError(e) + } + } - private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number { - if (typeof bitRate === 'number' || bitRate == null) return 1 - switch (bitRate) { - case 'extra-low': - return 0.6 - case 'low': - return 0.8 - case 'normal': - return 1 - case 'high': - return 1.2 - case 'extra-high': - return 1.4 - } - } + private getBitRateMultiplier(bitRate: RecordVideoOptions['videoBitRate']): number { + if (typeof bitRate === 'number' || bitRate == null) return 1 + switch (bitRate) { + case 'extra-low': + return 0.6 + case 'low': + return 0.8 + case 'normal': + return 1 + case 'high': + return 1.2 + case 'extra-high': + return 1.4 + } + } - /** - * Start a new video recording. - * - * @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 - * - * @example - * ```ts - * camera.current.startRecording({ - * onRecordingFinished: (video) => console.log(video), - * onRecordingError: (error) => console.error(error), - * }) - * setTimeout(() => { - * camera.current.stopRecording() - * }, 5000) - * ``` - */ - public startRecording(options: RecordVideoOptions): void { - const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options - if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') - throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!') + /** + * Start a new video recording. + * + * @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 + * + * @example + * ```ts + * camera.current.startRecording({ + * onRecordingFinished: (video) => console.log(video), + * onRecordingError: (error) => console.error(error), + * }) + * setTimeout(() => { + * camera.current.stopRecording() + * }, 5000) + * ``` + */ + public startRecording(options: RecordVideoOptions): void { + const { onRecordingError, onRecordingFinished, videoBitRate, ...passThruOptions } = options + if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') + throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!') - if (options.flash === 'on') { - // Enable torch for video recording - this.setState({ - isRecordingWithFlash: true, - }) - } + if (options.flash === 'on') { + // Enable torch for video recording + this.setState({ + isRecordingWithFlash: true, + }) + } - const nativeOptions: NativeRecordVideoOptions = passThruOptions - if (typeof videoBitRate === 'number') { - // If the user passed an absolute number as a bit-rate, we just use this as a full override. - nativeOptions.videoBitRateOverride = videoBitRate - } else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') { - // If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it - nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate) - } + const nativeOptions: NativeRecordVideoOptions = passThruOptions + if (typeof videoBitRate === 'number') { + // If the user passed an absolute number as a bit-rate, we just use this as a full override. + nativeOptions.videoBitRateOverride = videoBitRate + } else if (typeof videoBitRate === 'string' && videoBitRate !== 'normal') { + // If the user passed 'low'/'normal'/'high', we need to apply this as a multiplier to the native bitrate instead of absolutely setting it + nativeOptions.videoBitRateMultiplier = this.getBitRateMultiplier(videoBitRate) + } - const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => { - if (this.state.isRecordingWithFlash) { - // disable torch again if it was enabled - this.setState({ - isRecordingWithFlash: false, - }) - } + const onRecordCallback = (video?: VideoFile, error?: CameraCaptureError): void => { + if (this.state.isRecordingWithFlash) { + // disable torch again if it was enabled + this.setState({ + isRecordingWithFlash: false, + }) + } - if (error != null) return onRecordingError(error) - if (video != null) return onRecordingFinished(video) - } - try { - // TODO: Use TurboModules to make this awaitable. - CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback) - } catch (e) { - throw tryParseNativeCameraError(e) - } - } + if (error != null) return onRecordingError(error) + if (video != null) return onRecordingFinished(video) + } + try { + // TODO: Use TurboModules to make this awaitable. + CameraModule.startRecording(this.handle, nativeOptions, onRecordCallback) + } catch (e) { + throw tryParseNativeCameraError(e) + } + } - /** - * Pauses the current video recording. - * - * @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error - * - * @example - * ```ts - * // Start - * await camera.current.startRecording() - * await timeout(1000) - * // Pause - * await camera.current.pauseRecording() - * await timeout(500) - * // Resume - * await camera.current.resumeRecording() - * await timeout(2000) - * // Stop - * const video = await camera.current.stopRecording() - * ``` - */ - public async pauseRecording(): Promise { - try { - return await CameraModule.pauseRecording(this.handle) - } catch (e) { - throw tryParseNativeCameraError(e) - } - } + /** + * Pauses the current video recording. + * + * @throws {@linkcode CameraCaptureError} When any kind of error occured while pausing the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error + * + * @example + * ```ts + * // Start + * await camera.current.startRecording() + * await timeout(1000) + * // Pause + * await camera.current.pauseRecording() + * await timeout(500) + * // Resume + * await camera.current.resumeRecording() + * await timeout(2000) + * // Stop + * const video = await camera.current.stopRecording() + * ``` + */ + public async pauseRecording(): Promise { + try { + return await CameraModule.pauseRecording(this.handle) + } catch (e) { + throw tryParseNativeCameraError(e) + } + } - /** - * Resumes a currently paused video recording. - * - * @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error - * - * @example - * ```ts - * // Start - * await camera.current.startRecording() - * await timeout(1000) - * // Pause - * await camera.current.pauseRecording() - * await timeout(500) - * // Resume - * await camera.current.resumeRecording() - * await timeout(2000) - * // Stop - * const video = await camera.current.stopRecording() - * ``` - */ - public async resumeRecording(): Promise { - try { - return await CameraModule.resumeRecording(this.handle) - } catch (e) { - throw tryParseNativeCameraError(e) - } - } + /** + * Resumes a currently paused video recording. + * + * @throws {@linkcode CameraCaptureError} When any kind of error occured while resuming the video recording. Use the {@linkcode CameraCaptureError.code | code} property to get the actual error + * + * @example + * ```ts + * // Start + * await camera.current.startRecording() + * await timeout(1000) + * // Pause + * await camera.current.pauseRecording() + * await timeout(500) + * // Resume + * await camera.current.resumeRecording() + * await timeout(2000) + * // Stop + * const video = await camera.current.stopRecording() + * ``` + */ + public async resumeRecording(): Promise { + try { + return await CameraModule.resumeRecording(this.handle) + } catch (e) { + throw tryParseNativeCameraError(e) + } + } - /** - * Stop the current video recording. - * - * @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 - * - * @example - * ```ts - * await camera.current.startRecording() - * setTimeout(async () => { - * const video = await camera.current.stopRecording() - * }, 5000) - * ``` - */ - public async stopRecording(): Promise { - try { - return await CameraModule.stopRecording(this.handle) - } catch (e) { - throw tryParseNativeCameraError(e) - } - } + /** + * Stop the current video recording. + * + * @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 + * + * @example + * ```ts + * await camera.current.startRecording() + * setTimeout(async () => { + * const video = await camera.current.stopRecording() + * }, 5000) + * ``` + */ + public async stopRecording(): Promise { + 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 and is expressed in points. - * * `(0, 0)` means **top left**. - * * `(CameraView.width, CameraView.height)` means **bottom right**. - * - * Make sure the value doesn't exceed the CameraView's dimensions. - * - * @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error - * @example - * ```ts - * await camera.current.focus({ - * x: tapEvent.x, - * y: tapEvent.y - * }) - * ``` - */ - public async focus(point: Point): Promise { - try { - return await CameraModule.focus(this.handle, point) - } catch (e) { - throw tryParseNativeCameraError(e) - } - } - //#endregion + /** + * 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 and is expressed in points. + * * `(0, 0)` means **top left**. + * * `(CameraView.width, CameraView.height)` means **bottom right**. + * + * Make sure the value doesn't exceed the CameraView's dimensions. + * + * @throws {@linkcode CameraRuntimeError} When any kind of error occured while focussing. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error + * @example + * ```ts + * await camera.current.focus({ + * x: tapEvent.x, + * y: tapEvent.y + * }) + * ``` + */ + public async focus(point: Point): Promise { + try { + return await CameraModule.focus(this.handle, point) + } catch (e) { + throw tryParseNativeCameraError(e) + } + } + //#endregion - //#region Static Functions (NativeModule) - /** - * Get a list of all available camera devices on the current phone. - * - * If you use Hooks, use the `useCameraDevices(..)` hook instead. - * - * * For Camera Devices attached to the phone, it is safe to assume that this will never change. - * * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices could change over time when the external Camera device gets plugged in or plugged out, so use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes. - * - * @example - * ```ts - * const devices = Camera.getAvailableCameraDevices() - * const backCameras = devices.filter((d) => d.position === "back") - * const frontCameras = devices.filter((d) => d.position === "front") - * ``` - */ - public static getAvailableCameraDevices(): CameraDevice[] { - return CameraDevices.getAvailableCameraDevices() - } - /** - * Adds a listener that gets called everytime the Camera Devices change, for example - * when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out. - * - * If you use Hooks, use the `useCameraDevices()` hook instead. - */ - public static addCameraDevicesChangedListener(listener: (newDevices: CameraDevice[]) => void): EmitterSubscription { - return CameraDevices.addCameraDevicesChangedListener(listener) - } - /** - * Gets the current Camera Permission Status. Check this before mounting the Camera to ensure - * the user has permitted the app to use the camera. - * - * To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}. - */ - public static getCameraPermissionStatus(): CameraPermissionStatus { - return CameraModule.getCameraPermissionStatus() - } - /** - * 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. - * - * To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}. - */ - public static getMicrophonePermissionStatus(): CameraPermissionStatus { - return CameraModule.getMicrophonePermissionStatus() - } - /** - * 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. - * - * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error - */ - public static async requestCameraPermission(): Promise { - 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. - * - * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error - */ - public static async requestMicrophonePermission(): Promise { - try { - return await CameraModule.requestMicrophonePermission() - } catch (e) { - throw tryParseNativeCameraError(e) - } - } - //#endregion + //#region Static Functions (NativeModule) + /** + * Get a list of all available camera devices on the current phone. + * + * If you use Hooks, use the `useCameraDevices(..)` hook instead. + * + * * For Camera Devices attached to the phone, it is safe to assume that this will never change. + * * For external Camera Devices (USB cameras, Mac continuity cameras, etc.) the available Camera Devices could change over time when the external Camera device gets plugged in or plugged out, so use {@link addCameraDevicesChangedListener | addCameraDevicesChangedListener(...)} to listen for such changes. + * + * @example + * ```ts + * const devices = Camera.getAvailableCameraDevices() + * const backCameras = devices.filter((d) => d.position === "back") + * const frontCameras = devices.filter((d) => d.position === "front") + * ``` + */ + public static getAvailableCameraDevices(): CameraDevice[] { + return CameraDevices.getAvailableCameraDevices() + } + /** + * Adds a listener that gets called everytime the Camera Devices change, for example + * when an external Camera Device (USB or continuity Camera) gets plugged in or plugged out. + * + * If you use Hooks, use the `useCameraDevices()` hook instead. + */ + public static addCameraDevicesChangedListener(listener: (newDevices: CameraDevice[]) => void): EmitterSubscription { + return CameraDevices.addCameraDevicesChangedListener(listener) + } + /** + * Gets the current Camera Permission Status. Check this before mounting the Camera to ensure + * the user has permitted the app to use the camera. + * + * To actually prompt the user for camera permission, use {@linkcode Camera.requestCameraPermission | requestCameraPermission()}. + */ + public static getCameraPermissionStatus(): CameraPermissionStatus { + return CameraModule.getCameraPermissionStatus() + } + /** + * 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. + * + * To actually prompt the user for microphone permission, use {@linkcode Camera.requestMicrophonePermission | requestMicrophonePermission()}. + */ + public static getMicrophonePermissionStatus(): CameraPermissionStatus { + return CameraModule.getMicrophonePermissionStatus() + } + /** + * 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. + * + * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error + */ + public static async requestCameraPermission(): Promise { + 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. + * + * @throws {@linkcode CameraRuntimeError} When any kind of error occured while requesting permission. Use the {@linkcode CameraRuntimeError.code | code} property to get the actual error + */ + public static async requestMicrophonePermission(): Promise { + try { + return await CameraModule.requestMicrophonePermission() + } catch (e) { + throw tryParseNativeCameraError(e) + } + } + //#endregion - //#region Events (Wrapped to maintain reference equality) - private onError(event: NativeSyntheticEvent): void { - const error = event.nativeEvent - const cause = isErrorWithCause(error.cause) ? error.cause : undefined - // @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work - const cameraError = new CameraRuntimeError(error.code, error.message, cause) + //#region Events (Wrapped to maintain reference equality) + private onError(event: NativeSyntheticEvent): void { + const error = event.nativeEvent + const cause = isErrorWithCause(error.cause) ? error.cause : undefined + // @ts-expect-error We're casting from unknown bridge types to TS unions, I expect it to hopefully work + const cameraError = new CameraRuntimeError(error.code, error.message, cause) - if (this.props.onError != null) { - this.props.onError(cameraError) - } else { - // User didn't pass an `onError` handler, so just log it to console - console.error(`Camera.onError(${cameraError.code}): ${cameraError.message}`, cameraError) - } - } + if (this.props.onError != null) { + this.props.onError(cameraError) + } else { + // User didn't pass an `onError` handler, so just log it to console + console.error(`Camera.onError(${cameraError.code}): ${cameraError.message}`, cameraError) + } + } - private onInitialized(): void { - this.props.onInitialized?.() - } + private onInitialized(): void { + this.props.onInitialized?.() + } - private onStarted(): void { - this.props.onStarted?.() - } + private onStarted(): void { + this.props.onStarted?.() + } - private onStopped(): void { - this.props.onStopped?.() - } - //#endregion + private onStopped(): void { + this.props.onStopped?.() + } + //#endregion - private onCodeScanned(event: NativeSyntheticEvent): void { - const codeScanner = this.props.codeScanner - if (codeScanner == null) return + private onCodeScanned(event: NativeSyntheticEvent): void { + const codeScanner = this.props.codeScanner + if (codeScanner == null) return - codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame) - } + codeScanner.onCodeScanned(event.nativeEvent.codes, event.nativeEvent.frame) + } - //#region Lifecycle - private setFrameProcessor(frameProcessor: FrameProcessor): void { - VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor) - } + //#region Lifecycle + private setFrameProcessor(frameProcessor: FrameProcessor): void { + VisionCameraProxy.setFrameProcessor(this.handle, frameProcessor) + } - private unsetFrameProcessor(): void { - VisionCameraProxy.removeFrameProcessor(this.handle) - } + private unsetFrameProcessor(): void { + VisionCameraProxy.removeFrameProcessor(this.handle) + } - private onViewReady(): void { - this.isNativeViewMounted = true - if (this.props.frameProcessor != null) { - // user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now. - this.setFrameProcessor(this.props.frameProcessor) - this.lastFrameProcessor = this.props.frameProcessor - } - } + private onViewReady(): void { + this.isNativeViewMounted = true + if (this.props.frameProcessor != null) { + // user passed a `frameProcessor` but we didn't set it yet because the native view was not mounted yet. set it now. + this.setFrameProcessor(this.props.frameProcessor) + this.lastFrameProcessor = this.props.frameProcessor + } + } - /** @internal */ - componentDidUpdate(): void { - if (!this.isNativeViewMounted) return - const frameProcessor = this.props.frameProcessor - if (frameProcessor !== this.lastFrameProcessor) { - // frameProcessor argument identity changed. Update native to reflect the change. - if (frameProcessor != null) this.setFrameProcessor(frameProcessor) - else this.unsetFrameProcessor() + /** @internal */ + componentDidUpdate(): void { + if (!this.isNativeViewMounted) return + const frameProcessor = this.props.frameProcessor + if (frameProcessor !== this.lastFrameProcessor) { + // frameProcessor argument identity changed. Update native to reflect the change. + if (frameProcessor != null) this.setFrameProcessor(frameProcessor) + else this.unsetFrameProcessor() - this.lastFrameProcessor = frameProcessor - } - } - //#endregion + this.lastFrameProcessor = frameProcessor + } + } + //#endregion - /** @internal */ - public render(): React.ReactNode { - // We remove the big `device` object from the props because we only need to pass `cameraId` to native. - const { device, frameProcessor, codeScanner, ...props } = this.props + /** @internal */ + public render(): React.ReactNode { + // We remove the big `device` object from the props because we only need to pass `cameraId` to native. + const { device, frameProcessor, codeScanner, ...props } = this.props - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (device == null) { - throw new Error( - 'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices', - ) - } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (device == null) { + throw new Error( + 'Camera: `device` is null! Select a valid Camera device. See: https://mrousavy.com/react-native-vision-camera/docs/guides/devices', + ) + } - const shouldEnableBufferCompression = props.video === true && frameProcessor == null - const pixelFormat = props.pixelFormat ?? (frameProcessor != null ? 'yuv' : 'native') - const torch = this.state.isRecordingWithFlash ? 'on' : props.torch + const shouldEnableBufferCompression = props.video === true && frameProcessor == null + const pixelFormat = props.pixelFormat ?? (frameProcessor != null ? 'yuv' : 'native') + const torch = this.state.isRecordingWithFlash ? 'on' : props.torch - return ( - - ) - } + return ( + + ) + } } //#endregion // requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager' const NativeCameraView = requireNativeComponent( - 'CameraView', - // @ts-expect-error because the type declarations are kinda wrong, no? - Camera, + 'CameraView', + // @ts-expect-error because the type declarations are kinda wrong, no? + Camera, )