From 4a73cb96c1008ae856f594826ac5567ef68c1cf4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Mon, 11 Oct 2021 18:27:23 +0200 Subject: [PATCH] fix: Fix `view-not-found` race condition in C++ code (#511) * Add custom `onViewReady` event to get layout `componentDidMount` is async, so the native view _might_ not exist yet causing a race condition in the `setFrameProcessor` code. This PR fixes this by calling `setFrameProcessor` only after the native view has actually mounted, and to ensure that I created a custom event that fires at that point. * Update CameraView.swift --- .../com/mrousavy/camera/CameraView+Events.kt | 6 ++++++ .../java/com/mrousavy/camera/CameraView.kt | 5 +++++ .../com/mrousavy/camera/CameraViewManager.kt | 1 + ios/CameraView.swift | 14 +++++++++++++- ios/CameraViewManager.m | 3 ++- src/Camera.tsx | 19 ++++++++++--------- 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt b/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt index 3536a3c..3fdf42a 100644 --- a/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt +++ b/android/src/main/java/com/mrousavy/camera/CameraView+Events.kt @@ -42,6 +42,12 @@ fun CameraView.invokeOnFrameProcessorPerformanceSuggestionAvailable(currentFps: reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraPerformanceSuggestionAvailable", event) } +fun CameraView.invokeOnViewReady() { + val event = Arguments.createMap() + val reactContext = context as ReactContext + reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, "cameraViewReady", event) +} + private fun errorToMap(error: Throwable): WritableMap { val map = Arguments.createMap() map.putString("message", error.message) diff --git a/android/src/main/java/com/mrousavy/camera/CameraView.kt b/android/src/main/java/com/mrousavy/camera/CameraView.kt index c3cd74b..a6c9c5a 100644 --- a/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -108,6 +108,7 @@ class CameraView(context: Context, private val frameProcessorThread: ExecutorSer } // private properties + private var isMounted = false private val reactContext: ReactContext get() = context as ReactContext @@ -266,6 +267,10 @@ class CameraView(context: Context, private val frameProcessorThread: ExecutorSer override fun onAttachedToWindow() { super.onAttachedToWindow() updateLifecycleState() + if (!isMounted) { + isMounted = true + invokeOnViewReady() + } } override fun onDetachedFromWindow() { diff --git a/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index 5b01ffe..ae2c3d9 100644 --- a/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -55,6 +55,7 @@ class CameraViewManager(reactContext: ReactApplicationContext) : SimpleViewManag override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { return MapBuilder.builder() + .put("cameraViewReady", MapBuilder.of("registrationName", "onViewReady")) .put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized")) .put("cameraError", MapBuilder.of("registrationName", "onError")) .put("cameraPerformanceSuggestionAvailable", MapBuilder.of("registrationName", "onFrameProcessorPerformanceSuggestionAvailable")) diff --git a/ios/CameraView.swift b/ios/CameraView.swift index 548d7fe..ad4a931 100644 --- a/ios/CameraView.swift +++ b/ios/CameraView.swift @@ -65,6 +65,7 @@ public final class CameraView: UIView { @objc var onInitialized: RCTDirectEventBlock? @objc var onError: RCTDirectEventBlock? @objc var onFrameProcessorPerformanceSuggestionAvailable: RCTDirectEventBlock? + @objc var onViewReady: RCTDirectEventBlock? // zoom @objc var enableZoomGesture = false { didSet { @@ -77,7 +78,7 @@ public final class CameraView: UIView { } // pragma MARK: Internal Properties - + internal var isMounted = false internal var isReady = false // Capture Session internal let captureSession = AVCaptureSession() @@ -179,6 +180,17 @@ public final class CameraView: UIView { object: nil) } + override public func willMove(toSuperview newSuperview: UIView?) { + super.willMove(toSuperview: newSuperview) + if !isMounted { + isMounted = true + guard let onViewReady = onViewReady else { + return + } + onViewReady(nil) + } + } + // pragma MARK: Props updating override public final func didSetProps(_ changedProps: [String]!) { ReactLogger.log(level: .info, message: "Updating \(changedProps.count) prop(s)...") diff --git a/ios/CameraViewManager.m b/ios/CameraViewManager.m index 8161908..1feb5b1 100644 --- a/ios/CameraViewManager.m +++ b/ios/CameraViewManager.m @@ -45,10 +45,11 @@ RCT_EXPORT_VIEW_PROPERTY(preset, NSString); RCT_EXPORT_VIEW_PROPERTY(torch, NSString); RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber); RCT_EXPORT_VIEW_PROPERTY(enableZoomGesture, BOOL); -// Camera View Properties +// Camera View Events RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onFrameProcessorPerformanceSuggestionAvailable, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock); // Camera View Functions RCT_EXTERN_METHOD(startRecording:(nonnull NSNumber *)node options:(NSDictionary *)options onRecordCallback:(RCTResponseSenderBlock)onRecordCallback); diff --git a/src/Camera.tsx b/src/Camera.tsx index 5270a11..f5f5beb 100644 --- a/src/Camera.tsx +++ b/src/Camera.tsx @@ -30,6 +30,7 @@ type NativeCameraViewProps = Omit< onInitialized?: (event: NativeSyntheticEvent) => void; onError?: (event: NativeSyntheticEvent) => void; onFrameProcessorPerformanceSuggestionAvailable?: (event: NativeSyntheticEvent) => void; + onViewReady: () => void; }; type RefType = React.Component & Readonly; //#endregion @@ -82,6 +83,7 @@ export class Camera extends React.PureComponent { /** @internal */ constructor(props: CameraProps) { super(props); + this.onViewReady = this.onViewReady.bind(this); this.onInitialized = this.onInitialized.bind(this); this.onError = this.onError.bind(this); this.onFrameProcessorPerformanceSuggestionAvailable = this.onFrameProcessorPerformanceSuggestionAvailable.bind(this); @@ -367,15 +369,13 @@ export class Camera extends React.PureComponent { global.unsetFrameProcessor(this.handle); } - componentDidMount(): void { - requestAnimationFrame(() => { - 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 */ @@ -411,6 +411,7 @@ export class Camera extends React.PureComponent { frameProcessorFps={frameProcessorFps === 'auto' ? -1 : frameProcessorFps} cameraId={device.id} ref={this.ref} + onViewReady={this.onViewReady} onInitialized={this.onInitialized} onError={this.onError} onFrameProcessorPerformanceSuggestionAvailable={this.onFrameProcessorPerformanceSuggestionAvailable}