@capgo/camera-preview 7.14.5 → 7.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -229,6 +229,15 @@ public class CameraPreview
229
229
 
230
230
  @PluginMethod
231
231
  public void start(PluginCall call) {
232
+ // Prevent starting while an existing view is still active or stopping
233
+ if (cameraXView != null) {
234
+ try {
235
+ if (cameraXView.isRunning() || cameraXView.isBusy()) {
236
+ call.reject("Camera is busy or stopping. Please retry shortly.");
237
+ return;
238
+ }
239
+ } catch (Exception ignored) {}
240
+ }
232
241
  boolean disableAudio = Boolean.TRUE.equals(
233
242
  call.getBoolean("disableAudio", true)
234
243
  );
@@ -367,9 +376,18 @@ public class CameraPreview
367
376
  rotationOverlay = null;
368
377
  }
369
378
 
370
- if (cameraXView != null && cameraXView.isRunning()) {
371
- cameraXView.stopSession();
372
- cameraXView = null;
379
+ if (cameraXView != null) {
380
+ boolean willDefer = false;
381
+ try {
382
+ willDefer = cameraXView.isCapturing();
383
+ } catch (Exception ignored) {}
384
+ if (cameraXView.isRunning()) {
385
+ cameraXView.stopSession();
386
+ }
387
+ // Only drop the reference if no deferred stop is pending
388
+ if (!willDefer) {
389
+ cameraXView = null;
390
+ }
373
391
  }
374
392
  // Restore original window background if modified earlier
375
393
  if (originalWindowBackground != null) {
@@ -1486,6 +1504,12 @@ public class CameraPreview
1486
1504
  bridge.releaseCall(pluginCall);
1487
1505
  }
1488
1506
 
1507
+ @Override
1508
+ public void onCameraStopped() {
1509
+ // Ensure reference is cleared once underlying CameraXView has fully stopped
1510
+ cameraXView = null;
1511
+ }
1512
+
1489
1513
  private JSObject getViewSize(
1490
1514
  double x,
1491
1515
  double y,
@@ -107,6 +107,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
107
107
  void onSampleTakenError(String message);
108
108
  void onCameraStarted(int width, int height, int x, int y);
109
109
  void onCameraStartError(String message);
110
+ void onCameraStopped();
110
111
  }
111
112
 
112
113
  public interface VideoRecordingCallback {
@@ -144,6 +145,27 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
144
145
  private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
145
146
  private String currentExposureMode = "CONTINUOUS"; // Default behavior
146
147
  private boolean isVideoCaptureInitializing = false;
148
+ // Capture/stop coordination
149
+ private final Object captureLock = new Object();
150
+ private volatile boolean isCapturingPhoto = false;
151
+ private volatile boolean stopRequested = false;
152
+ private volatile boolean previewDetachedOnDeferredStop = false;
153
+
154
+ public boolean isCapturing() {
155
+ return isCapturingPhoto;
156
+ }
157
+
158
+ public boolean isStopDeferred() {
159
+ synchronized (captureLock) {
160
+ return isCapturingPhoto && stopRequested;
161
+ }
162
+ }
163
+
164
+ public boolean isBusy() {
165
+ synchronized (captureLock) {
166
+ return isCapturingPhoto || stopRequested;
167
+ }
168
+ }
147
169
 
148
170
  public CameraXView(Context context, WebView webView) {
149
171
  this.context = context;
@@ -257,6 +279,32 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
257
279
  }
258
280
 
259
281
  public void stopSession() {
282
+ // If a capture is in progress, defer heavy teardown until it completes.
283
+ synchronized (captureLock) {
284
+ if (isCapturingPhoto) {
285
+ stopRequested = true;
286
+ // Hide/detach the preview immediately so UI can close, but keep camera running
287
+ if (!previewDetachedOnDeferredStop) {
288
+ mainExecutor.execute(() -> {
289
+ try {
290
+ if (previewContainer != null) {
291
+ ViewGroup parent = (ViewGroup) previewContainer.getParent();
292
+ if (parent != null) {
293
+ parent.removeView(previewContainer);
294
+ }
295
+ }
296
+ previewDetachedOnDeferredStop = true;
297
+ } catch (Exception ignored) {}
298
+ });
299
+ }
300
+ return;
301
+ }
302
+ }
303
+
304
+ performImmediateStop();
305
+ }
306
+
307
+ private void performImmediateStop() {
260
308
  isRunning = false;
261
309
  // Cancel any ongoing focus operation when stopping session
262
310
  if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
@@ -265,15 +313,27 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
265
313
  currentFocusFuture = null;
266
314
 
267
315
  mainExecutor.execute(() -> {
268
- lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
269
- if (cameraProvider != null) {
270
- cameraProvider.unbindAll();
271
- }
272
- lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
273
- if (cameraExecutor != null) {
274
- cameraExecutor.shutdownNow();
316
+ try {
317
+ lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
318
+ if (cameraProvider != null) {
319
+ cameraProvider.unbindAll();
320
+ }
321
+ lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
322
+ if (cameraExecutor != null) {
323
+ cameraExecutor.shutdown();
324
+ }
325
+ removePreviewView();
326
+ } catch (Exception e) {
327
+ Log.w(TAG, "performImmediateStop: error during stop", e);
328
+ } finally {
329
+ stopRequested = false;
330
+ previewDetachedOnDeferredStop = false;
331
+ if (listener != null) {
332
+ try {
333
+ listener.onCameraStopped();
334
+ } catch (Exception ignored) {}
335
+ }
275
336
  }
276
- removePreviewView();
277
337
  });
278
338
  }
279
339
 
@@ -1065,6 +1125,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1065
1125
  return;
1066
1126
  }
1067
1127
 
1128
+ synchronized (captureLock) {
1129
+ isCapturingPhoto = true;
1130
+ }
1131
+
1068
1132
  File tempFile = new File(context.getCacheDir(), "temp_image.jpg");
1069
1133
  ImageCapture.OutputFileOptions outputFileOptions =
1070
1134
  new ImageCapture.OutputFileOptions.Builder(tempFile).build();
@@ -1081,6 +1145,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1081
1145
  "Photo capture failed: " + exception.getMessage()
1082
1146
  );
1083
1147
  }
1148
+ // End of capture lifecycle
1149
+ synchronized (captureLock) {
1150
+ isCapturingPhoto = false;
1151
+ if (stopRequested) {
1152
+ performImmediateStop();
1153
+ }
1154
+ }
1084
1155
  }
1085
1156
 
1086
1157
  @Override
@@ -1194,6 +1265,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1194
1265
  "Error processing image: " + e.getMessage()
1195
1266
  );
1196
1267
  }
1268
+ } finally {
1269
+ // End of capture lifecycle
1270
+ synchronized (captureLock) {
1271
+ isCapturingPhoto = false;
1272
+ if (stopRequested) {
1273
+ performImmediateStop();
1274
+ }
1275
+ }
1197
1276
  }
1198
1277
  }
1199
1278
  }
@@ -80,6 +80,10 @@ class CameraController: NSObject {
80
80
  // Track output preparation status
81
81
  private var outputsPrepared: Bool = false
82
82
 
83
+ // Capture/stop coordination
84
+ var isCapturingPhoto: Bool = false
85
+ var stopRequestedAfterCapture: Bool = false
86
+
83
87
  var isUsingMultiLensVirtualCamera: Bool {
84
88
  guard let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera else { return false }
85
89
  // A rear multi-lens virtual camera will have a min zoom of 1.0 but support wider angles
@@ -944,14 +948,27 @@ extension CameraController {
944
948
  }
945
949
  }
946
950
 
947
- self.photoCaptureCompletionBlock = { (image, photoData, metadata, error) in
951
+ self.isCapturingPhoto = true
952
+
953
+ self.photoCaptureCompletionBlock = { [weak self] (image, photoData, metadata, error) in
954
+ guard let self = self else { return }
948
955
  if let error = error {
949
956
  completion(nil, nil, nil, error)
957
+ // End capture lifecycle
958
+ self.isCapturingPhoto = false
959
+ if self.stopRequestedAfterCapture {
960
+ DispatchQueue.main.async { self.cleanup(); self.stopRequestedAfterCapture = false }
961
+ }
950
962
  return
951
963
  }
952
964
 
953
965
  guard let image = image else {
954
966
  completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
967
+ // End capture lifecycle
968
+ self.isCapturingPhoto = false
969
+ if self.stopRequestedAfterCapture {
970
+ DispatchQueue.main.async { self.cleanup(); self.stopRequestedAfterCapture = false }
971
+ }
955
972
  return
956
973
  }
957
974
 
@@ -980,6 +997,11 @@ extension CameraController {
980
997
  }
981
998
 
982
999
  completion(finalImage, photoData, metadata, nil)
1000
+ // End capture lifecycle
1001
+ self.isCapturingPhoto = false
1002
+ if self.stopRequestedAfterCapture {
1003
+ DispatchQueue.main.async { self.cleanup(); self.stopRequestedAfterCapture = false }
1004
+ }
983
1005
  }
984
1006
 
985
1007
  photoOutput.capturePhoto(with: settings, delegate: self)
@@ -521,6 +521,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
521
521
  call.reject("camera already started")
522
522
  return
523
523
  }
524
+ // Guard against starting while a deferred stop is pending due to in-flight capture
525
+ if self.cameraController.isCapturingPhoto || self.cameraController.stopRequestedAfterCapture {
526
+ call.reject("camera is stopping or busy, please retry shortly")
527
+ return
528
+ }
524
529
  self.isInitializing = true
525
530
 
526
531
  self.cameraPosition = call.getString("position") ?? "rear"
@@ -739,7 +744,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
739
744
 
740
745
  // UI operations must be on main thread
741
746
  DispatchQueue.main.async {
742
- // Always attempt to stop and clean up, regardless of captureSession state
747
+ // If a photo capture is in-flight, defer cleanup until it finishes,
748
+ // but hide the preview immediately so UI can close.
743
749
  self.cameraController.removeGridOverlay()
744
750
  if let previewView = self.previewView {
745
751
  previewView.removeFromSuperview()
@@ -749,14 +755,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
749
755
  self.webView?.isOpaque = true
750
756
  self.isInitialized = false
751
757
  self.isInitializing = false
752
- self.cameraController.cleanup()
753
758
 
754
- // Remove notification observers
759
+ // Remove notification observers regardless
755
760
  NotificationCenter.default.removeObserver(self)
756
-
757
761
  NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
758
762
  UIDevice.current.endGeneratingDeviceOrientationNotifications()
759
763
 
764
+ if self.cameraController.isCapturingPhoto {
765
+ // Defer heavy cleanup until capture callback completes
766
+ self.cameraController.stopRequestedAfterCapture = true
767
+ } else {
768
+ // No capture pending; cleanup now
769
+ self.cameraController.cleanup()
770
+ }
771
+
760
772
  call.resolve()
761
773
  }
762
774
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.14.5",
3
+ "version": "7.14.6",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {