@capgo/camera-preview 7.14.5 → 7.14.7
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.
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +27 -3
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +87 -8
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +23 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +16 -4
- package/package.json +1 -1
|
@@ -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
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
cameraProvider
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
cameraExecutor
|
|
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.
|
|
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
|
-
//
|
|
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
|
}
|