From 96799b5aa666a6e66a8edb2c3aede81b55eb4bfa Mon Sep 17 00:00:00 2001 From: Felipe Rodriguez Esturo Date: Tue, 10 Jun 2025 16:37:24 -0300 Subject: [PATCH 1/6] Android Event Emitter for receiving Bytes when recording Video --- .../camera/core/CameraSession+Video.kt | 8 ++++++-- .../camera/react/CameraView+RecordVideo.kt | 8 ++++++-- .../com/mrousavy/camera/react/CameraView.kt | 13 ++++++++++++- .../mrousavy/camera/react/CameraViewModule.kt | 19 ++++++++++++++++++- package/src/Camera.tsx | 11 +++++++++-- package/src/types/VideoFile.ts | 1 + 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Video.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Video.kt index fcc54b8e95..eea1c385fd 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Video.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Video.kt @@ -17,7 +17,8 @@ fun CameraSession.startRecording( enableAudio: Boolean, options: RecordVideoOptions, callback: (video: Video) -> Unit, - onError: (error: CameraError) -> Unit + onError: (error: CameraError) -> Unit, + onBytesWrittenCallback: (bytes: Long) -> Unit ) { if (camera == null) throw CameraNotReadyError() if (recording != null) throw RecordingInProgressError() @@ -49,7 +50,10 @@ fun CameraSession.startRecording( is VideoRecordEvent.Pause -> Log.i(CameraSession.TAG, "Recording paused!") - is VideoRecordEvent.Status -> Log.i(CameraSession.TAG, "Status update! Recorded ${event.recordingStats.numBytesRecorded} bytes.") + is VideoRecordEvent.Status -> { + Log.i(CameraSession.TAG, "Status update! Recorded ${event.recordingStats.numBytesRecorded} bytes.") + onBytesWrittenCallback(event.recordingStats.numBytesRecorded) + } is VideoRecordEvent.Finalize -> { if (isRecordingCanceled) { diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt index e2e16e4716..da22b3cc9b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt @@ -17,7 +17,11 @@ import com.mrousavy.camera.core.types.Video import com.mrousavy.camera.react.utils.makeErrorMap fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) { - // check audio permission + val onBytesWrittenCallback = { bytes: Long -> + val map = Arguments.createMap() + map.putDouble("bytes", bytes.toDouble()) + sendEvent("onBytesWritten", map) + } if (audio) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { throw MicrophonePermissionError() @@ -36,7 +40,7 @@ fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Cal val errorMap = makeErrorMap(error.code, error.message) onRecordCallback(null, errorMap) } - cameraSession.startRecording(audio, options, callback, onError) + cameraSession.startRecording(audio, options, callback, onError, onBytesWrittenCallback) } fun CameraView.pauseRecording() { diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt index 64a751eaab..3516dd4779 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt @@ -7,6 +7,9 @@ import android.view.Gravity import android.view.ScaleGestureDetector import android.widget.FrameLayout import androidx.camera.view.PreviewView +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.WritableMap +import com.facebook.react.modules.core.DeviceEventManagerModule import com.google.mlkit.vision.barcode.common.Barcode import com.mrousavy.camera.core.CameraConfiguration import com.mrousavy.camera.core.CameraSession @@ -77,7 +80,7 @@ class CameraView(context: Context) : var photoHdr = false var videoBitRateOverride: Double? = null var videoBitRateMultiplier: Double? = null - + private var reactContext: ReactApplicationContext? = null // TODO: Use .BALANCED once CameraX fixes it https://issuetracker.google.com/issues/337214687 var photoQualityBalance = QualityBalance.SPEED var lowLightBoost = false @@ -125,6 +128,10 @@ class CameraView(context: Context) : updatePreview() } + fun setReactContext(reactContext: ReactApplicationContext) { + this.reactContext = reactContext + } + override fun onAttachedToWindow() { Log.i(TAG, "CameraView attached to window!") super.onAttachedToWindow() @@ -148,6 +155,10 @@ class CameraView(context: Context) : cameraSession.close() } + fun sendEvent(eventName: String, params: WritableMap?) { + reactContext?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(eventName, params) + } + fun update() { Log.i(TAG, "Updating CameraSession...") val now = System.currentTimeMillis() diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt index 7ae420949d..2808980a6f 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt @@ -33,6 +33,8 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlin.math.max + @ReactModule(name = CameraViewModule.TAG) @Suppress("unused") @@ -52,7 +54,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase } } } - + private var listenersCount = 0 private val backgroundCoroutineScope = CoroutineScope(CameraQueues.cameraExecutor.asCoroutineDispatcher()) override fun invalidate() { @@ -73,6 +75,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase val uiManager = UIManagerHelper.getUIManager(context, uiManagerType) ?: throw Error("UIManager not found!") val view = uiManager.resolveView(viewId) as? CameraView ?: throw ViewNotFoundError(viewId) + view.setReactContext(reactApplicationContext) Log.d(TAG, "Found view $viewId!") return@runOnUiThreadAndWait view } @@ -188,6 +191,20 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase } } + // Required for rn built in EventEmitter Calls. + @ReactMethod + fun addListener(eventName: String?) { + this.listenersCount++ + } + + @ReactMethod + fun removeListeners(count: Int?) { + var finalCount = 0 + if(count != null){ + finalCount = count + } + this.listenersCount = max(0, this.listenersCount - finalCount); + } private fun canRequestPermission(permission: String): Boolean { val activity = currentActivity as? PermissionAwareActivity return activity?.shouldShowRequestPermissionRationale(permission) ?: false diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index afe056a761..ef8d67c544 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { findNodeHandle, StyleSheet } from 'react-native' +import { findNodeHandle, StyleSheet, NativeEventEmitter } from 'react-native' import type { CameraDevice } from './types/CameraDevice' import type { CameraCaptureError } from './CameraError' import { CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError' @@ -81,6 +81,8 @@ export class Camera extends React.PureComponent { /** @internal */ displayName = Camera.displayName private lastFrameProcessor: ((frame: Frame) => void) | undefined + private nativeEventEmitter: NativeEventEmitter = new NativeEventEmitter(CameraModule) + private bytesEventEmitterListener: EmitterSubscription | null = null private isNativeViewMounted = false private lastUIRotation: number | undefined = undefined private rotationHelper = new RotationHelper() @@ -201,10 +203,14 @@ export class Camera extends React.PureComponent { * ``` */ public startRecording(options: RecordVideoOptions): void { - const { onRecordingError, onRecordingFinished, ...passThruOptions } = options + const { onRecordingError, onRecordingFinished, onBytesWritten, ...passThruOptions } = options if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!') + this.bytesEventEmitterListener = this.nativeEventEmitter.addListener('onBytesWritten', (data) => { + onBytesWritten?.(data.bytes) + }) + if (options.flash === 'on') { // Enable torch for video recording this.setState({ @@ -316,6 +322,7 @@ export class Camera extends React.PureComponent { */ public async stopRecording(): Promise { try { + // this.bytesEventEmitterListener?.remove() return await CameraModule.stopRecording(this.handle) } catch (e) { throw tryParseNativeCameraError(e) diff --git a/package/src/types/VideoFile.ts b/package/src/types/VideoFile.ts index 6391df76fa..d9586df8ff 100644 --- a/package/src/types/VideoFile.ts +++ b/package/src/types/VideoFile.ts @@ -22,6 +22,7 @@ export interface RecordVideoOptions { /** * Called when there was an unexpected runtime error while recording the video. */ + onBytesWritten?: (bytes: number) => void onRecordingError: (error: CameraCaptureError) => void /** * Called when the recording has been successfully saved to file. From fb6309050b3b1d557f2f5e8a47644f47285216ff Mon Sep 17 00:00:00 2001 From: Felipe Rodriguez Esturo Date: Wed, 11 Jun 2025 15:41:35 -0300 Subject: [PATCH 2/6] Implement Android Event Emitter as it should be for Android Views --- .../com/mrousavy/camera/core/CameraSession.kt | 1 + .../camera/react/CameraView+Events.kt | 11 +++++++++++ .../camera/react/CameraView+RecordVideo.kt | 4 +--- .../com/mrousavy/camera/react/CameraView.kt | 13 ++++--------- .../camera/react/CameraViewManager.kt | 1 + .../mrousavy/camera/react/CameraViewModule.kt | 16 ---------------- .../java/com/mrousavy/camera/react/Events.kt | 9 +++++++++ package/src/Camera.tsx | 19 +++++++++---------- package/src/types/CameraProps.ts | 4 ++++ package/src/types/VideoFile.ts | 5 ++++- 10 files changed, 44 insertions(+), 39 deletions(-) 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 13f6a763c1..eadbf10cc9 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 @@ -221,5 +221,6 @@ class CameraSession(internal val context: Context, internal val callback: Callba fun onOutputOrientationChanged(outputOrientation: Orientation) fun onPreviewOrientationChanged(previewOrientation: Orientation) fun onCodeScanned(codes: List, scannerFrame: CodeScannerFrame) + fun onBytesWrittenVideo(bytesWritten: Double) } } diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView+Events.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView+Events.kt index aacb5c7079..1a491d0b9b 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView+Events.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView+Events.kt @@ -125,6 +125,17 @@ fun CameraView.invokeOnAverageFpsChanged(averageFps: Double) { this.sendEvent(event) } +fun CameraView.invokeOnBytesWrittenVideo(bytesWritten: Double) { + Log.i(CameraView.TAG, "invokeOnBytesWrittenVideo($bytesWritten)") + + val surfaceId = UIManagerHelper.getSurfaceId(this) + val data = Arguments.createMap() + data.putDouble("bytesWritten", bytesWritten) + + val event = BytesWrittenVideoEvent(surfaceId, id, data) + this.sendEvent(event) +} + fun CameraView.invokeOnCodeScanned(barcodes: List, scannerFrame: CodeScannerFrame) { val codes = Arguments.createArray() barcodes.forEach { barcode -> diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt index da22b3cc9b..f65f42b18a 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView+RecordVideo.kt @@ -18,9 +18,7 @@ import com.mrousavy.camera.react.utils.makeErrorMap fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallback: Callback) { val onBytesWrittenCallback = { bytes: Long -> - val map = Arguments.createMap() - map.putDouble("bytes", bytes.toDouble()) - sendEvent("onBytesWritten", map) + this.onBytesWrittenVideo(bytes.toDouble()) } if (audio) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt index 3516dd4779..2673ae1308 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt @@ -80,7 +80,6 @@ class CameraView(context: Context) : var photoHdr = false var videoBitRateOverride: Double? = null var videoBitRateMultiplier: Double? = null - private var reactContext: ReactApplicationContext? = null // TODO: Use .BALANCED once CameraX fixes it https://issuetracker.google.com/issues/337214687 var photoQualityBalance = QualityBalance.SPEED var lowLightBoost = false @@ -128,10 +127,6 @@ class CameraView(context: Context) : updatePreview() } - fun setReactContext(reactContext: ReactApplicationContext) { - this.reactContext = reactContext - } - override fun onAttachedToWindow() { Log.i(TAG, "CameraView attached to window!") super.onAttachedToWindow() @@ -155,10 +150,6 @@ class CameraView(context: Context) : cameraSession.close() } - fun sendEvent(eventName: String, params: WritableMap?) { - reactContext?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(eventName, params) - } - fun update() { Log.i(TAG, "Updating CameraSession...") val now = System.currentTimeMillis() @@ -361,4 +352,8 @@ class CameraView(context: Context) : override fun onAverageFpsChanged(averageFps: Double) { invokeOnAverageFpsChanged(averageFps) } + + override fun onBytesWrittenVideo(bytesWritten: Double) { + invokeOnBytesWrittenVideo(bytesWritten) + } } diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt index c5845b727f..ed2a42a3ae 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewManager.kt @@ -41,6 +41,7 @@ class CameraViewManager : ViewGroupManager() { .put(CameraOutputOrientationChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOutputOrientationChanged")) .put(CameraPreviewOrientationChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPreviewOrientationChanged")) .put(AverageFpsChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onAverageFpsChanged")) + .put(BytesWrittenVideoEvent.EVENT_NAME, MapBuilder.of("registrationName", "onBytesWrittenVideo")) .build() override fun getName(): String = TAG diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt index 2808980a6f..4c43be6501 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt @@ -54,7 +54,6 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase } } } - private var listenersCount = 0 private val backgroundCoroutineScope = CoroutineScope(CameraQueues.cameraExecutor.asCoroutineDispatcher()) override fun invalidate() { @@ -75,7 +74,6 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase val uiManager = UIManagerHelper.getUIManager(context, uiManagerType) ?: throw Error("UIManager not found!") val view = uiManager.resolveView(viewId) as? CameraView ?: throw ViewNotFoundError(viewId) - view.setReactContext(reactApplicationContext) Log.d(TAG, "Found view $viewId!") return@runOnUiThreadAndWait view } @@ -191,20 +189,6 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase } } - // Required for rn built in EventEmitter Calls. - @ReactMethod - fun addListener(eventName: String?) { - this.listenersCount++ - } - - @ReactMethod - fun removeListeners(count: Int?) { - var finalCount = 0 - if(count != null){ - finalCount = count - } - this.listenersCount = max(0, this.listenersCount - finalCount); - } private fun canRequestPermission(permission: String): Boolean { val activity = currentActivity as? PermissionAwareActivity return activity?.shouldShowRequestPermissionRationale(permission) ?: false diff --git a/package/android/src/main/java/com/mrousavy/camera/react/Events.kt b/package/android/src/main/java/com/mrousavy/camera/react/Events.kt index acbb77aa92..441b6cfeb6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/Events.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/Events.kt @@ -98,7 +98,16 @@ class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: Writ Event(surfaceId, viewId) { override fun getEventName() = EVENT_NAME override fun getEventData() = data + companion object { const val EVENT_NAME = "topCameraCodeScanned" } +} + class BytesWrittenVideoEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : + Event(surfaceId, viewId) { + override fun getEventName() = EVENT_NAME + override fun getEventData() = data + companion object { + const val EVENT_NAME = "bytesWrittenVideoEvent" + } } diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index ef8d67c544..f1e2bf1a66 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { findNodeHandle, StyleSheet, NativeEventEmitter } from 'react-native' +import { findNodeHandle, StyleSheet } from 'react-native' import type { CameraDevice } from './types/CameraDevice' import type { CameraCaptureError } from './CameraError' import { CameraRuntimeError, tryParseNativeCameraError, isErrorWithCause } from './CameraError' @@ -7,7 +7,7 @@ import type { CameraProps, DrawableFrameProcessor, OnShutterEvent, ReadonlyFrame import { CameraModule } from './NativeCameraModule' import type { PhotoFile, TakePhotoOptions } from './types/PhotoFile' import type { Point } from './types/Point' -import type { RecordVideoOptions, VideoFile } from './types/VideoFile' +import type { OnBytesWrittenVideoEvent, RecordVideoOptions, VideoFile } from './types/VideoFile' import { VisionCameraProxy } from './frame-processors/VisionCameraProxy' import { CameraDevices } from './CameraDevices' import type { EmitterSubscription, NativeSyntheticEvent, NativeMethods } from 'react-native' @@ -81,8 +81,6 @@ export class Camera extends React.PureComponent { /** @internal */ displayName = Camera.displayName private lastFrameProcessor: ((frame: Frame) => void) | undefined - private nativeEventEmitter: NativeEventEmitter = new NativeEventEmitter(CameraModule) - private bytesEventEmitterListener: EmitterSubscription | null = null private isNativeViewMounted = false private lastUIRotation: number | undefined = undefined private rotationHelper = new RotationHelper() @@ -94,6 +92,7 @@ export class Camera extends React.PureComponent { super(props) this.onViewReady = this.onViewReady.bind(this) this.onAverageFpsChanged = this.onAverageFpsChanged.bind(this) + this.onBytesWrittenVideo = this.onBytesWrittenVideo.bind(this) this.onInitialized = this.onInitialized.bind(this) this.onStarted = this.onStarted.bind(this) this.onStopped = this.onStopped.bind(this) @@ -203,14 +202,10 @@ export class Camera extends React.PureComponent { * ``` */ public startRecording(options: RecordVideoOptions): void { - const { onRecordingError, onRecordingFinished, onBytesWritten, ...passThruOptions } = options + const { onRecordingError, onRecordingFinished, ...passThruOptions } = options if (typeof onRecordingError !== 'function' || typeof onRecordingFinished !== 'function') throw new CameraRuntimeError('parameter/invalid-parameter', 'The onRecordingError or onRecordingFinished functions were not set!') - this.bytesEventEmitterListener = this.nativeEventEmitter.addListener('onBytesWritten', (data) => { - onBytesWritten?.(data.bytes) - }) - if (options.flash === 'on') { // Enable torch for video recording this.setState({ @@ -322,7 +317,6 @@ export class Camera extends React.PureComponent { */ public async stopRecording(): Promise { try { - // this.bytesEventEmitterListener?.remove() return await CameraModule.stopRecording(this.handle) } catch (e) { throw tryParseNativeCameraError(e) @@ -606,6 +600,10 @@ export class Camera extends React.PureComponent { }) } + private onBytesWrittenVideo({ nativeEvent: { bytesWritten } }: NativeSyntheticEvent): void { + this.props.onBytesWrittenVideo?.(bytesWritten) + } + /** @internal */ componentDidUpdate(): void { if (!this.isNativeViewMounted) return @@ -664,6 +662,7 @@ export class Camera extends React.PureComponent { isMirrored={props.isMirrored ?? shouldBeMirrored} onViewReady={this.onViewReady} onAverageFpsChanged={enableFpsGraph ? this.onAverageFpsChanged : undefined} + onBytesWrittenVideo={this.onBytesWrittenVideo} onInitialized={this.onInitialized} onCodeScanned={this.onCodeScanned} onStarted={this.onStarted} diff --git a/package/src/types/CameraProps.ts b/package/src/types/CameraProps.ts index 18bfc9b618..2ca134d76d 100644 --- a/package/src/types/CameraProps.ts +++ b/package/src/types/CameraProps.ts @@ -416,5 +416,9 @@ export interface CameraProps extends ViewProps { * ``` */ codeScanner?: CodeScanner + /** + * Called whenever the video is written to the file. + */ + onBytesWrittenVideo?: (bytes: number) => void //#endregion } diff --git a/package/src/types/VideoFile.ts b/package/src/types/VideoFile.ts index d9586df8ff..a821a3f076 100644 --- a/package/src/types/VideoFile.ts +++ b/package/src/types/VideoFile.ts @@ -22,7 +22,6 @@ export interface RecordVideoOptions { /** * Called when there was an unexpected runtime error while recording the video. */ - onBytesWritten?: (bytes: number) => void onRecordingError: (error: CameraCaptureError) => void /** * Called when the recording has been successfully saved to file. @@ -55,3 +54,7 @@ export interface VideoFile extends TemporaryFile { */ height: number } + +export interface OnBytesWrittenVideoEvent { + bytesWritten: number +} From 458dd5e3af8d1177e372cc111c8846ed7866c64c Mon Sep 17 00:00:00 2001 From: Felipe Rodriguez Esturo Date: Wed, 11 Jun 2025 18:44:47 -0300 Subject: [PATCH 3/6] working for ios --- example/src/CameraPage.tsx | 1 + package/ios/Core/CameraSession+Video.swift | 25 ++++++++++++++++++- package/ios/Core/CameraSession.swift | 1 + .../ios/React/CameraView+RecordVideo.swift | 3 ++- package/ios/React/CameraView.swift | 7 ++++++ package/ios/React/CameraViewManager.m | 1 + 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/example/src/CameraPage.tsx b/example/src/CameraPage.tsx index af65e71462..1a66536817 100644 --- a/example/src/CameraPage.tsx +++ b/example/src/CameraPage.tsx @@ -205,6 +205,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { ref={camera} onInitialized={onInitialized} onError={onError} + onBytesWrittenVideo={(bytes) => console.log(`Bytes written: ${bytes / 1024 / 1024} MB!`)} onStarted={() => console.log('Camera started!')} onStopped={() => console.log('Camera stopped!')} onPreviewStarted={() => console.log('Preview started!')} diff --git a/package/ios/Core/CameraSession+Video.swift b/package/ios/Core/CameraSession+Video.swift index 8e57710f24..8889061e18 100644 --- a/package/ios/Core/CameraSession+Video.swift +++ b/package/ios/Core/CameraSession+Video.swift @@ -18,7 +18,8 @@ extension CameraSession { */ func startRecording(options: RecordVideoOptions, onVideoRecorded: @escaping (_ video: Video) -> Void, - onError: @escaping (_ error: CameraError) -> Void) { + onError: @escaping (_ error: CameraError) -> Void, + onBytesWritten: @escaping (_ bytes: Double) -> Void) { // Run on Camera Queue CameraQueues.cameraQueue.async { let start = DispatchTime.now() @@ -48,6 +49,8 @@ extension CameraSession { } self.recordingSession = nil + self.recordingSizeTimer?.cancel() + self.recordingSizeTimer = nil if self.didCancelRecording { VisionLogger.log(level: .info, message: "RecordingSession finished because the recording was canceled.") @@ -128,6 +131,26 @@ extension CameraSession { self.didCancelRecording = false self.recordingSession = recordingSession + let timer = DispatchSource.makeTimerSource(queue: CameraQueues.cameraQueue) + timer.schedule(deadline: .now(), repeating: 0.4) + + timer.setEventHandler { + guard let session = self.recordingSession else { + timer.cancel() + return + } + + let path = session.url.path + if let size = try? FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber { + let bytes = size.doubleValue + + DispatchQueue.main.async { + onBytesWritten(bytes) + } + } + } + self.recordingSizeTimer = timer + self.recordingSizeTimer?.resume() let end = DispatchTime.now() VisionLogger.log(level: .info, message: "RecordingSesssion started in \(Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000)ms!") } catch let error as CameraError { diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index 10b0f3399c..ee96c66957 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -31,6 +31,7 @@ final class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegat // State var metadataProvider = MetadataProvider() var recordingSession: RecordingSession? + var recordingSizeTimer: DispatchSourceTimer? var didCancelRecording = false var orientationManager = OrientationManager() diff --git a/package/ios/React/CameraView+RecordVideo.swift b/package/ios/React/CameraView+RecordVideo.swift index 913d420efd..f50a82611f 100644 --- a/package/ios/React/CameraView+RecordVideo.swift +++ b/package/ios/React/CameraView+RecordVideo.swift @@ -28,7 +28,8 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud }, onError: { error in callback.reject(error: error) - } + }, + onBytesWritten: self.onBytesWrittenVideo ) } catch { // Some error occured while initializing VideoSettings diff --git a/package/ios/React/CameraView.swift b/package/ios/React/CameraView.swift index c773975353..64282f295f 100644 --- a/package/ios/React/CameraView.swift +++ b/package/ios/React/CameraView.swift @@ -80,6 +80,7 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat @objc var onOutputOrientationChangedEvent: RCTDirectEventBlock? @objc var onViewReadyEvent: RCTDirectEventBlock? @objc var onAverageFpsChangedEvent: RCTDirectEventBlock? + @objc var onBytesWrittenVideoEvent: RCTDirectEventBlock? @objc var onCodeScannedEvent: RCTDirectEventBlock? // zoom @@ -392,4 +393,10 @@ public final class CameraView: UIView, CameraSessionDelegate, PreviewViewDelegat "averageFps": averageFps, ]) } + + func onBytesWrittenVideo(bytes: Double) { + onBytesWrittenVideoEvent?([ + "bytesWritten": bytes, + ]) + } } diff --git a/package/ios/React/CameraViewManager.m b/package/ios/React/CameraViewManager.m index 527c9bc0fd..4aacb1ba2a 100644 --- a/package/ios/React/CameraViewManager.m +++ b/package/ios/React/CameraViewManager.m @@ -68,6 +68,7 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage RCT_REMAP_VIEW_PROPERTY(onPreviewOrientationChanged, onPreviewOrientationChangedEvent, RCTDirectEventBlock); RCT_REMAP_VIEW_PROPERTY(onViewReady, onViewReadyEvent, RCTDirectEventBlock); RCT_REMAP_VIEW_PROPERTY(onAverageFpsChanged, onAverageFpsChangedEvent, RCTDirectEventBlock); +RCT_REMAP_VIEW_PROPERTY(onBytesWrittenVideo, onBytesWrittenVideoEvent, RCTDirectEventBlock); // Code Scanner RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary); RCT_REMAP_VIEW_PROPERTY(onCodeScanned, onCodeScannedEvent, RCTDirectEventBlock); From 99eec90a6af39ae736d3e98119f5bede9aa7ea71 Mon Sep 17 00:00:00 2001 From: Felipe Rodriguez Esturo Date: Wed, 11 Jun 2025 18:45:18 -0300 Subject: [PATCH 4/6] update docs --- docs/docs/guides/RECORDING_VIDEOS.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/guides/RECORDING_VIDEOS.mdx b/docs/docs/guides/RECORDING_VIDEOS.mdx index 99a5dc2f5d..3cd6ed19c2 100644 --- a/docs/docs/guides/RECORDING_VIDEOS.mdx +++ b/docs/docs/guides/RECORDING_VIDEOS.mdx @@ -42,6 +42,7 @@ To start a video recording you first have to enable video capture: {...props} video={true} audio={true} // <-- optional + onBytesWrittenVideo={(bytes) => {/*Whatever you need with bytes in realtime while it is recording*/}} // <-- optional /> ``` From 8922601906e1a3cfa1521e045d1f366abbab0bb7 Mon Sep 17 00:00:00 2001 From: Felipe Rodriguez Esturo Date: Wed, 11 Jun 2025 20:16:16 -0300 Subject: [PATCH 5/6] small changes --- .../java/com/mrousavy/camera/react/CameraView.kt | 4 +--- .../com/mrousavy/camera/react/CameraViewModule.kt | 3 +-- .../main/java/com/mrousavy/camera/react/Events.kt | 1 - package/src/types/CameraProps.ts | 12 +++++++++++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt index 2673ae1308..7b3fbebab4 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraView.kt @@ -7,9 +7,6 @@ import android.view.Gravity import android.view.ScaleGestureDetector import android.widget.FrameLayout import androidx.camera.view.PreviewView -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.WritableMap -import com.facebook.react.modules.core.DeviceEventManagerModule import com.google.mlkit.vision.barcode.common.Barcode import com.mrousavy.camera.core.CameraConfiguration import com.mrousavy.camera.core.CameraSession @@ -80,6 +77,7 @@ class CameraView(context: Context) : var photoHdr = false var videoBitRateOverride: Double? = null var videoBitRateMultiplier: Double? = null + // TODO: Use .BALANCED once CameraX fixes it https://issuetracker.google.com/issues/337214687 var photoQualityBalance = QualityBalance.SPEED var lowLightBoost = false diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt index 4c43be6501..7ae420949d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraViewModule.kt @@ -33,8 +33,6 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlin.math.max - @ReactModule(name = CameraViewModule.TAG) @Suppress("unused") @@ -54,6 +52,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase } } } + private val backgroundCoroutineScope = CoroutineScope(CameraQueues.cameraExecutor.asCoroutineDispatcher()) override fun invalidate() { diff --git a/package/android/src/main/java/com/mrousavy/camera/react/Events.kt b/package/android/src/main/java/com/mrousavy/camera/react/Events.kt index 441b6cfeb6..984af49082 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/Events.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/Events.kt @@ -98,7 +98,6 @@ class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: Writ Event(surfaceId, viewId) { override fun getEventName() = EVENT_NAME override fun getEventData() = data - companion object { const val EVENT_NAME = "topCameraCodeScanned" } diff --git a/package/src/types/CameraProps.ts b/package/src/types/CameraProps.ts index 2ca134d76d..ea8ad1e0d4 100644 --- a/package/src/types/CameraProps.ts +++ b/package/src/types/CameraProps.ts @@ -417,7 +417,17 @@ export interface CameraProps extends ViewProps { */ codeScanner?: CodeScanner /** - * Called whenever the video is written to the file. + * Used to get the bytes written to the video file on real time while it is being recorded + * + * @see See [the Code Scanner documentation](https://react-native-vision-camera.com/docs/guides/code-scanning) for more information + * @example + * ```tsx + * const onBytesWrittenVideo = (bytes: number) => { + * console.log(`Bytes written: ${bytes}`) + * } + * + * return + * ``` */ onBytesWrittenVideo?: (bytes: number) => void //#endregion From b412b29d858f9b8ce322e29a9c8c51773995a525 Mon Sep 17 00:00:00 2001 From: Felipe Rodriguez Esturo Date: Wed, 11 Jun 2025 21:34:59 -0300 Subject: [PATCH 6/6] fix lint issues --- .../main/java/com/mrousavy/camera/react/Events.kt | 14 +++++++------- package/ios/React/CameraView+RecordVideo.swift | 2 +- package/src/Camera.tsx | 2 +- package/src/NativeCameraView.ts | 3 +++ package/src/types/CameraProps.ts | 2 -- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/react/Events.kt b/package/android/src/main/java/com/mrousavy/camera/react/Events.kt index 984af49082..3bdafe594c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/Events.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/Events.kt @@ -102,11 +102,11 @@ class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: Writ const val EVENT_NAME = "topCameraCodeScanned" } } - class BytesWrittenVideoEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : - Event(surfaceId, viewId) { - override fun getEventName() = EVENT_NAME - override fun getEventData() = data - companion object { - const val EVENT_NAME = "bytesWrittenVideoEvent" - } +class BytesWrittenVideoEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : + Event(surfaceId, viewId) { + override fun getEventName() = EVENT_NAME + override fun getEventData() = data + companion object { + const val EVENT_NAME = "bytesWrittenVideoEvent" + } } diff --git a/package/ios/React/CameraView+RecordVideo.swift b/package/ios/React/CameraView+RecordVideo.swift index f50a82611f..ddd06fa42c 100644 --- a/package/ios/React/CameraView+RecordVideo.swift +++ b/package/ios/React/CameraView+RecordVideo.swift @@ -29,7 +29,7 @@ extension CameraView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAud onError: { error in callback.reject(error: error) }, - onBytesWritten: self.onBytesWrittenVideo + onBytesWritten: onBytesWrittenVideo ) } catch { // Some error occured while initializing VideoSettings diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index f1e2bf1a66..900242e5a9 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -92,7 +92,6 @@ export class Camera extends React.PureComponent { super(props) this.onViewReady = this.onViewReady.bind(this) this.onAverageFpsChanged = this.onAverageFpsChanged.bind(this) - this.onBytesWrittenVideo = this.onBytesWrittenVideo.bind(this) this.onInitialized = this.onInitialized.bind(this) this.onStarted = this.onStarted.bind(this) this.onStopped = this.onStopped.bind(this) @@ -103,6 +102,7 @@ export class Camera extends React.PureComponent { this.onPreviewOrientationChanged = this.onPreviewOrientationChanged.bind(this) this.onError = this.onError.bind(this) this.onCodeScanned = this.onCodeScanned.bind(this) + this.onBytesWrittenVideo = this.onBytesWrittenVideo.bind(this) this.ref = React.createRef() this.lastFrameProcessor = undefined this.state = { diff --git a/package/src/NativeCameraView.ts b/package/src/NativeCameraView.ts index b53009ce56..eb491b3112 100644 --- a/package/src/NativeCameraView.ts +++ b/package/src/NativeCameraView.ts @@ -4,6 +4,7 @@ import type { ErrorWithCause } from './CameraError' import type { CameraProps, OnShutterEvent } from './types/CameraProps' import type { Code, CodeScanner, CodeScannerFrame } from './types/CodeScanner' import type { Orientation } from './types/Orientation' +import type { OnBytesWrittenVideoEvent } from './types/VideoFile' export interface OnCodeScannedEvent { codes: Code[] @@ -35,6 +36,7 @@ export type NativeCameraViewProps = Omit< | 'codeScanner' | 'fps' | 'videoBitRate' + | 'onBytesWrittenVideo' > & { // private intermediate props cameraId: string @@ -58,6 +60,7 @@ export type NativeCameraViewProps = Omit< onShutter?: (event: NativeSyntheticEvent) => void onOutputOrientationChanged?: (event: NativeSyntheticEvent) => void onPreviewOrientationChanged?: (event: NativeSyntheticEvent) => void + onBytesWrittenVideo?: (event: NativeSyntheticEvent) => void } // requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager' diff --git a/package/src/types/CameraProps.ts b/package/src/types/CameraProps.ts index ea8ad1e0d4..e19d458d01 100644 --- a/package/src/types/CameraProps.ts +++ b/package/src/types/CameraProps.ts @@ -418,8 +418,6 @@ export interface CameraProps extends ViewProps { codeScanner?: CodeScanner /** * Used to get the bytes written to the video file on real time while it is being recorded - * - * @see See [the Code Scanner documentation](https://react-native-vision-camera.com/docs/guides/code-scanning) for more information * @example * ```tsx * const onBytesWrittenVideo = (bytes: number) => {