From 4ee52d62117d22251afeb21bb61f8527886bc83d Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Sat, 9 Dec 2023 21:09:55 +0300 Subject: [PATCH] feat: Add `onStarted` and `onStopped` events (#2273) * feat: Add `onStarted` and `onStopped` events * Implement `onStart` for Android * Update CameraSession.kt * Update CameraSessionDelegate.swift --- .../com/mrousavy/camera/CameraView+Events.kt | 14 +++++++++++++ .../java/com/mrousavy/camera/CameraView.kt | 8 ++++++++ .../com/mrousavy/camera/CameraViewManager.kt | 2 ++ .../camera/core/CameraConfiguration.kt | 9 +++++++-- .../com/mrousavy/camera/core/CameraSession.kt | 13 ++++++++++++ package/example/ios/Podfile.lock | 14 ++++++------- package/ios/CameraView.swift | 20 ++++++++++++++++++- package/ios/CameraViewManager.m | 2 ++ package/ios/Core/CameraSession.swift | 2 ++ package/ios/Core/CameraSessionDelegate.swift | 8 ++++++++ package/src/Camera.tsx | 14 +++++++++++++ package/src/CameraProps.ts | 10 +++++++++- 12 files changed, 105 insertions(+), 11 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 1b20748..4b18754 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 @@ -19,6 +19,20 @@ fun CameraView.invokeOnInitialized() { reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraInitialized", null) } +fun CameraView.invokeOnStarted() { + Log.i(CameraView.TAG, "invokeOnStarted()") + + val reactContext = context as ReactContext + reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStarted", null) +} + +fun CameraView.invokeOnStopped() { + Log.i(CameraView.TAG, "invokeOnStopped()") + + val reactContext = context as ReactContext + reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraStopped", null) +} + 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 0aa8932..8337831 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -239,6 +239,14 @@ class CameraView(context: Context) : invokeOnInitialized() } + override fun onStarted() { + invokeOnStarted() + } + + override fun onStopped() { + invokeOnStopped() + } + 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 e6e013b..9c990d4 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -25,6 +25,8 @@ class CameraViewManager : ViewGroupManager() { MapBuilder.builder() .put("cameraViewReady", MapBuilder.of("registrationName", "onViewReady")) .put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized")) + .put("cameraStarted", MapBuilder.of("registrationName", "onStarted")) + .put("cameraStopped", MapBuilder.of("registrationName", "onStopped")) .put("cameraError", MapBuilder.of("registrationName", "onError")) .put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned")) .build() diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt index 8b27508..3504f25 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -76,7 +76,9 @@ data class CameraConfiguration( // Outputs & Session (Photo, Video, CodeScanner, HDR, Format) val outputsChanged: Boolean, // Side-Props for CaptureRequest (fps, low-light-boost, torch, zoom, videoStabilization) - val sidePropsChanged: Boolean + val sidePropsChanged: Boolean, + // (isActive) changed + val isActiveChanged: Boolean ) { val hasAnyDifference: Boolean get() = sidePropsChanged || outputsChanged || deviceChanged @@ -98,10 +100,13 @@ data class CameraConfiguration( left.zoom != right.zoom || left.videoStabilizationMode != right.videoStabilizationMode || left.isActive != right.isActive || left.exposure != right.exposure + val isActiveChanged = left?.isActive != right.isActive + return Difference( deviceChanged, outputsChanged, - sidePropsChanged + sidePropsChanged, + isActiveChanged ) } } 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 a5a51d6..5e1d504 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 @@ -146,6 +146,16 @@ class CameraSession(private val context: Context, private val cameraManager: Cam if (diff.deviceChanged) { callback.onInitialized() } + + // Notify about Camera start/stop + if (diff.isActiveChanged) { + // TODO: Move that into the CaptureRequest callback to get actual first-frame arrive time? + if (config.isActive) { + callback.onStarted() + } else { + callback.onStopped() + } + } } catch (error: Throwable) { Log.e(TAG, "Failed to configure CameraSession! Error: ${error.message}, Config-Diff: $diff", error) callback.onError(error) @@ -367,6 +377,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam // TODO: Do we want to do stopRepeating() or entirely destroy the session? // If the Camera is not active, we don't do anything. captureSession?.stopRepeating() + isRunning = false return } @@ -621,6 +632,8 @@ class CameraSession(private val context: Context, private val cameraManager: Cam interface CameraSessionCallback { fun onError(error: Throwable) fun onInitialized() + fun onStarted() + fun onStopped() fun onCodeScanned(codes: List, scannerFrame: CodeScannerFrame) } } diff --git a/package/example/ios/Podfile.lock b/package/example/ios/Podfile.lock index 219a6db..0f91454 100644 --- a/package/example/ios/Podfile.lock +++ b/package/example/ios/Podfile.lock @@ -27,9 +27,9 @@ PODS: - libwebp/sharpyuv (1.3.2) - libwebp/webp (1.3.2): - libwebp/sharpyuv - - MMKV (1.3.1): - - MMKVCore (~> 1.3.1) - - MMKVCore (1.3.1) + - MMKV (1.3.2): + - MMKVCore (~> 1.3.2) + - MMKVCore (1.3.2) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -507,7 +507,7 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - VisionCamera (3.6.11): + - VisionCamera (3.6.14): - React - React-callinvoker - React-Core @@ -698,8 +698,8 @@ SPEC CHECKSUMS: hermes-engine: 10fbd3f62405c41ea07e71973ea61e1878d07322 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - MMKV: 5a07930c70c70b86cd87761a42c8f3836fb681d7 - MMKVCore: e50135dbd33235b6ab390635991bab437ab873c0 + MMKV: f21593c0af4b3f2a0ceb8f820f28bb639ea22bb7 + MMKVCore: 31b4cb83f8266467eef20a35b6d78e409a11060d RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: a2faf4bad4e438ca37b2040cb8f7799baa065c18 RCTTypeSafety: cb09f3e4747b6d18331a15eb05271de7441ca0b3 @@ -747,7 +747,7 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - VisionCamera: b35fc51a521ce0a9b9da41d8b13127e3d414d195 + VisionCamera: 3cf177fa91fa9fe04622071415032c5af618a5ac Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb diff --git a/package/ios/CameraView.swift b/package/ios/CameraView.swift index 789acc0..1b99f27 100644 --- a/package/ios/CameraView.swift +++ b/package/ios/CameraView.swift @@ -59,6 +59,8 @@ public final class CameraView: UIView, CameraSessionDelegate { // events @objc var onInitialized: RCTDirectEventBlock? @objc var onError: RCTDirectEventBlock? + @objc var onStarted: RCTDirectEventBlock? + @objc var onStopped: RCTDirectEventBlock? @objc var onViewReady: RCTDirectEventBlock? @objc var onCodeScanned: RCTDirectEventBlock? // zoom @@ -283,7 +285,23 @@ public final class CameraView: UIView, CameraSessionDelegate { guard let onInitialized = onInitialized else { return } - onInitialized([String: Any]()) + onInitialized([:]) + } + + func onCameraStarted() { + ReactLogger.log(level: .info, message: "Camera started!") + guard let onStarted = onStarted else { + return + } + onStarted([:]) + } + + func onCameraStopped() { + ReactLogger.log(level: .info, message: "Camera stopped!") + guard let onStopped = onStopped else { + return + } + onStopped([:]) } func onFrame(sampleBuffer: CMSampleBuffer) { diff --git a/package/ios/CameraViewManager.m b/package/ios/CameraViewManager.m index 4fc8983..d11c0a8 100644 --- a/package/ios/CameraViewManager.m +++ b/package/ios/CameraViewManager.m @@ -52,6 +52,8 @@ RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString); // Camera View Events RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onStarted, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock); // Code Scanner RCT_EXPORT_VIEW_PROPERTY(codeScannerOptions, NSDictionary); diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift index c9395fa..63386bd 100644 --- a/package/ios/Core/CameraSession.swift +++ b/package/ios/Core/CameraSession.swift @@ -249,8 +249,10 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC // Start/Stop session if configuration.isActive { captureSession.startRunning() + delegate?.onCameraStarted() } else { captureSession.stopRunning() + delegate?.onCameraStopped() } } diff --git a/package/ios/Core/CameraSessionDelegate.swift b/package/ios/Core/CameraSessionDelegate.swift index f9d5e7e..2c1a0b2 100644 --- a/package/ios/Core/CameraSessionDelegate.swift +++ b/package/ios/Core/CameraSessionDelegate.swift @@ -21,6 +21,14 @@ protocol CameraSessionDelegate: AnyObject { Called when the [CameraSession] successfully initializes */ func onSessionInitialized() + /** + Called when the [CameraSession] starts streaming frames. (isActive=true) + */ + func onCameraStarted() + /** + Called when the [CameraSession] stopped streaming frames. (isActive=false) + */ + func onCameraStopped() /** Called for every frame (if video or frameProcessor is enabled) */ diff --git a/package/src/Camera.tsx b/package/src/Camera.tsx index d219c0e..6f8864c 100644 --- a/package/src/Camera.tsx +++ b/package/src/Camera.tsx @@ -33,6 +33,8 @@ type NativeCameraViewProps = Omit) => void onError?: (event: NativeSyntheticEvent) => void onCodeScanned?: (event: NativeSyntheticEvent) => void + onStarted?: (event: NativeSyntheticEvent) => void + onStopped?: (event: NativeSyntheticEvent) => void onViewReady: () => void } type NativeRecordVideoOptions = Omit & { @@ -89,6 +91,8 @@ export class Camera extends React.PureComponent { 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() @@ -416,6 +420,14 @@ export class Camera extends React.PureComponent { private onInitialized(): void { this.props.onInitialized?.() } + + private onStarted(): void { + this.props.onStarted?.() + } + + private onStopped(): void { + this.props.onStopped?.() + } //#endregion private onCodeScanned(event: NativeSyntheticEvent): void { @@ -481,6 +493,8 @@ export class Camera extends React.PureComponent { onViewReady={this.onViewReady} onInitialized={this.onInitialized} onCodeScanned={this.onCodeScanned} + onStarted={this.onStarted} + onStopped={this.onStopped} onError={this.onError} codeScannerOptions={codeScanner} enableFrameProcessor={frameProcessor != null} diff --git a/package/src/CameraProps.ts b/package/src/CameraProps.ts index 73bb8eb..ed2b77f 100644 --- a/package/src/CameraProps.ts +++ b/package/src/CameraProps.ts @@ -246,9 +246,17 @@ export interface CameraProps extends ViewProps { */ onError?: (error: CameraRuntimeError) => void /** - * Called when the camera was successfully initialized. + * Called when the camera session was successfully initialized. This will get called everytime a new device is set. */ onInitialized?: () => void + /** + * Called when the camera started the session (`isActive={true}`) + */ + onStarted?: () => void + /** + * Called when the camera stopped the session (`isActive={false}`) + */ + onStopped?: () => void /** * A worklet which will be called for every frame the Camera "sees". *