capacitor-plugin-camera-forked 3.0.8 → 3.0.10

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.
@@ -26,6 +26,7 @@ import androidx.camera.core.Camera;
26
26
  import androidx.camera.core.CameraSelector;
27
27
  import androidx.camera.core.CameraState;
28
28
  import androidx.camera.core.FocusMeteringAction;
29
+ import androidx.camera.core.FocusMeteringResult;
29
30
  import androidx.camera.core.ImageAnalysis;
30
31
  import androidx.camera.core.ImageCapture;
31
32
  import androidx.camera.core.ImageCaptureException;
@@ -46,6 +47,7 @@ import androidx.camera.video.MediaStoreOutputOptions;
46
47
  import androidx.camera.video.VideoRecordEvent;
47
48
  import androidx.camera.view.PreviewView;
48
49
  import androidx.core.app.ActivityCompat;
50
+ import androidx.core.content.ContextCompat;
49
51
  import androidx.core.util.Consumer;
50
52
  import androidx.lifecycle.LifecycleOwner;
51
53
 
@@ -69,17 +71,17 @@ import java.io.FileNotFoundException;
69
71
  import java.io.IOException;
70
72
  import java.io.InputStream;
71
73
  import java.util.Date;
74
+ import java.util.List;
72
75
  import java.util.concurrent.ExecutionException;
73
76
  import java.util.concurrent.ExecutorService;
74
77
  import java.util.concurrent.Executors;
75
78
  import java.util.concurrent.TimeUnit;
76
79
 
77
80
  @CapacitorPlugin(
78
- name = "CameraPreview",
79
- permissions = {
80
- @Permission(strings = {Manifest.permission.CAMERA}, alias = CameraPreviewPlugin.CAMERA),
81
- @Permission(strings = {Manifest.permission.RECORD_AUDIO}, alias = CameraPreviewPlugin.MICROPHONE),
82
- }
81
+ name = "CameraPreview",
82
+ permissions = {
83
+ @Permission(strings = {Manifest.permission.CAMERA}, alias = CameraPreviewPlugin.CAMERA),
84
+ }
83
85
  )
84
86
  public class CameraPreviewPlugin extends Plugin {
85
87
  // Permission alias constants
@@ -111,56 +113,60 @@ public class CameraPreviewPlugin extends Plugin {
111
113
  @PluginMethod
112
114
  public void initialize(PluginCall call) {
113
115
  getActivity().runOnUiThread(new Runnable() {
114
- @RequiresApi(api = Build.VERSION_CODES.P)
115
- public void run() {
116
- previewView = new PreviewView(getContext());
117
- previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
118
- FrameLayout.LayoutParams cameraPreviewParams = new FrameLayout.LayoutParams(
119
- FrameLayout.LayoutParams.MATCH_PARENT,
120
- FrameLayout.LayoutParams.MATCH_PARENT
121
- );
122
- ((ViewGroup) bridge.getWebView().getParent()).addView(previewView, cameraPreviewParams);
123
- bridge.getWebView().bringToFront();
124
-
125
- exec = Executors.newSingleThreadExecutor();
126
- cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
127
- cameraProviderFuture.addListener(() -> {
128
- try {
129
- cameraProvider = cameraProviderFuture.get();
130
- cameraSelector = new CameraSelector.Builder()
131
- .requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
132
- setupUseCases(false);
133
- call.resolve();
134
- } catch (ExecutionException | InterruptedException e) {
135
- e.printStackTrace();
136
- call.reject(e.getMessage());
137
- }
138
- }, getContext().getMainExecutor());
139
-
140
-
116
+ @RequiresApi(api = Build.VERSION_CODES.P)
117
+ public void run() {
118
+ previewView = new PreviewView(getContext());
119
+ previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
120
+ FrameLayout.LayoutParams cameraPreviewParams = new FrameLayout.LayoutParams(
121
+ FrameLayout.LayoutParams.MATCH_PARENT,
122
+ FrameLayout.LayoutParams.MATCH_PARENT
123
+ );
124
+ ((ViewGroup) bridge.getWebView().getParent()).addView(previewView, cameraPreviewParams);
125
+ bridge.getWebView().bringToFront();
126
+
127
+ exec = Executors.newSingleThreadExecutor();
128
+ cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
129
+ cameraProviderFuture.addListener(() -> {
130
+ try {
131
+ cameraProvider = cameraProviderFuture.get();
132
+ cameraSelector = new CameraSelector.Builder()
133
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
134
+ // Auto-optimize for photo capture on initialization
135
+ setupUseCases(false); // Always use photo-optimized mode
136
+ Log.d("Camera", "Initialized with photo capture optimization");
137
+ call.resolve();
138
+ } catch (ExecutionException | InterruptedException e) {
139
+ e.printStackTrace();
140
+ call.reject(e.getMessage());
141
141
  }
142
+ }, ContextCompat.getMainExecutor(getContext()));
143
+ }
142
144
  });
143
145
  }
144
146
 
145
147
  private void setupUseCases(boolean enableVideo) {
146
- //set up the resolution for the preview and image analysis.
147
- int orientation = getContext().getResources().getConfiguration().orientation;
148
- Size resolution;
149
- if (orientation == Configuration.ORIENTATION_PORTRAIT) {
150
- resolution = new Size(desiredHeight, desiredWidth);
151
- } else {
152
- resolution = new Size(desiredWidth, desiredHeight);
153
- }
148
+ // Auto-detect maximum resolution for better zoom quality
149
+ Size resolution = getOptimalResolution();
154
150
 
151
+ // Enhanced Preview setup for better quality
155
152
  Preview.Builder previewBuilder = new Preview.Builder();
153
+ if (resolution != null) {
156
154
  previewBuilder.setTargetResolution(resolution);
155
+ Log.d("Camera", "Using optimal resolution: " + resolution.getWidth() + "x" + resolution.getHeight());
156
+ } else {
157
+ // Fallback: let CameraX choose the best resolution automatically
158
+ Log.d("Camera", "Using CameraX auto-resolution selection");
159
+ }
157
160
  preview = previewBuilder.build();
158
161
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
159
162
 
163
+ // Enhanced ImageAnalysis setup
160
164
  ImageAnalysis.Builder imageAnalysisBuilder = new ImageAnalysis.Builder();
161
-
162
- imageAnalysisBuilder.setTargetResolution(resolution)
163
- .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST);
165
+ if (resolution != null) {
166
+ imageAnalysisBuilder.setTargetResolution(resolution);
167
+ }
168
+ imageAnalysisBuilder.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
169
+ .setImageQueueDepth(1); // Optimize for latest frame
164
170
 
165
171
  imageAnalysis = imageAnalysisBuilder.build();
166
172
 
@@ -208,10 +214,20 @@ public class CameraPreviewPlugin extends Plugin {
208
214
  }
209
215
  });
210
216
 
211
- imageCapture =
212
- new ImageCapture.Builder()
213
- .setTargetAspectRatio(AspectRatio.RATIO_16_9)
214
- .build();
217
+ // Enhanced ImageCapture setup for optimal photo quality with max resolution
218
+ ImageCapture.Builder imageCaptureBuilder = new ImageCapture.Builder();
219
+ if (resolution != null) {
220
+ imageCaptureBuilder.setTargetResolution(resolution);
221
+ }
222
+ imageCaptureBuilder.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) // Prioritize quality over speed
223
+ .setJpegQuality(95); // High JPEG quality (0-100, default is around 85)
224
+
225
+ // Add image stabilization if available (API 28+)
226
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
227
+ imageCaptureBuilder.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY);
228
+ }
229
+
230
+ imageCapture = imageCaptureBuilder.build();
215
231
 
216
232
  if (enableVideo) {
217
233
  Quality quality = Quality.HD;
@@ -234,6 +250,53 @@ public class CameraPreviewPlugin extends Plugin {
234
250
  }
235
251
  }
236
252
 
253
+ /**
254
+ * Get the optimal (maximum) resolution supported by the device for better zoom quality
255
+ * Uses high-quality resolution options with CameraX auto-selection fallback
256
+ */
257
+ private Size getOptimalResolution() {
258
+ try {
259
+ // Use high-quality resolution options for better zoom quality
260
+ // These are commonly supported resolutions across Android devices
261
+ int orientation = getContext().getResources().getConfiguration().orientation;
262
+
263
+ // High-quality resolution options (in order of preference)
264
+ Size[] preferredResolutions;
265
+
266
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
267
+ preferredResolutions = new Size[]{
268
+ new Size(3024, 4032), // 12MP portrait
269
+ new Size(2160, 3840), // 4K portrait
270
+ new Size(2448, 3264), // 8MP portrait
271
+ new Size(1920, 2560), // 5MP portrait
272
+ new Size(1440, 1920), // 3MP portrait
273
+ new Size(1080, 1920), // Full HD portrait
274
+ };
275
+ } else {
276
+ preferredResolutions = new Size[]{
277
+ new Size(4032, 3024), // 12MP landscape
278
+ new Size(3840, 2160), // 4K landscape
279
+ new Size(3264, 2448), // 8MP landscape
280
+ new Size(2560, 1920), // 5MP landscape
281
+ new Size(1920, 1440), // 3MP landscape
282
+ new Size(1920, 1080), // Full HD landscape
283
+ };
284
+ }
285
+
286
+ // Return the first (highest quality) option - CameraX will adapt if not supported
287
+ Size optimalResolution = preferredResolutions[0];
288
+ Log.d("Camera", "Selected optimal resolution: " + optimalResolution.getWidth() + "x" + optimalResolution.getHeight());
289
+ return optimalResolution;
290
+
291
+ } catch (Exception e) {
292
+ Log.e("Camera", "Error selecting optimal resolution: " + e.getMessage());
293
+ }
294
+
295
+ // Fallback: return null to let CameraX auto-select
296
+ Log.d("Camera", "Using CameraX auto-resolution selection as fallback");
297
+ return null;
298
+ }
299
+
237
300
  @PluginMethod
238
301
  public void startCamera(PluginCall call) {
239
302
  getActivity().runOnUiThread(new Runnable() {
@@ -322,36 +385,162 @@ public class CameraPreviewPlugin extends Plugin {
322
385
 
323
386
  @PluginMethod
324
387
  public void setFocus(PluginCall call) {
325
- if (call.hasOption("x") && call.hasOption("y")) {
388
+ if (!call.hasOption("x") || !call.hasOption("y")) {
389
+ call.reject("Invalid focus coordinates - x and y are required");
390
+ return;
391
+ }
392
+
326
393
  Float x = call.getFloat("x");
327
394
  Float y = call.getFloat("y");
328
395
 
396
+ // Validate coordinate ranges (should be 0-1 for normalized coordinates)
397
+ if (x < 0.0f || x > 1.0f || y < 0.0f || y > 1.0f) {
398
+ call.reject("Focus coordinates must be normalized values between 0.0 and 1.0");
399
+ return;
400
+ }
401
+
329
402
  if (previewView == null || camera == null) {
330
403
  call.reject("Camera preview is not initialized");
331
404
  return;
332
405
  }
333
406
 
334
- try {
335
- // Convert normalized coordinates to absolute pixel coordinates
336
- float absoluteX = x * previewView.getWidth();
337
- float absoluteY = y * previewView.getHeight();
407
+ getActivity().runOnUiThread(new Runnable() {
408
+ @Override
409
+ public void run() {
410
+ try {
411
+ // Use PreviewView's built-in MeteringPointFactory for proper coordinate transformation
412
+ MeteringPointFactory factory = previewView.getMeteringPointFactory();
338
413
 
339
- MeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(previewView.getWidth(), previewView.getHeight());
340
- MeteringPoint point = factory.createPoint(absoluteX, absoluteY);
414
+ // Convert normalized coordinates to preview coordinates
415
+ float previewX = x * previewView.getWidth();
416
+ float previewY = y * previewView.getHeight();
341
417
 
342
- FocusMeteringAction.Builder builder = new FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF);
343
- builder.setAutoCancelDuration(5, TimeUnit.SECONDS);
344
- FocusMeteringAction action = builder.build();
345
- camera.getCameraControl().startFocusAndMetering(action);
418
+ MeteringPoint focusPoint = factory.createPoint(previewX, previewY);
346
419
 
347
- call.resolve();
348
- } catch (Exception e) {
349
- call.reject("Error setting focus: " + e.getMessage());
420
+ // Get configurable options
421
+ boolean includeExposure = Boolean.TRUE.equals(call.getBoolean("includeExposure", true));
422
+ Integer autoCancelDurationParam = call.getInt("autoCancelDurationSeconds");
423
+ int autoCancelDuration = autoCancelDurationParam != null ? autoCancelDurationParam : 3;
424
+
425
+ // Build the focus and metering action
426
+ FocusMeteringAction.Builder builder = new FocusMeteringAction.Builder(focusPoint);
427
+
428
+ // Set auto-focus flags
429
+ if (includeExposure) {
430
+ // Use both AF and AE flags for the same point
431
+ builder = new FocusMeteringAction.Builder(focusPoint, FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE);
432
+ } else {
433
+ // Use only AF flag
434
+ builder = new FocusMeteringAction.Builder(focusPoint, FocusMeteringAction.FLAG_AF);
435
+ }
436
+
437
+ // Set auto-cancel duration
438
+ builder.setAutoCancelDuration(autoCancelDuration, TimeUnit.SECONDS);
439
+
440
+ FocusMeteringAction action = builder.build();
441
+
442
+ // Start focus and metering with result callback
443
+ ListenableFuture<FocusMeteringResult> future = camera.getCameraControl().startFocusAndMetering(action);
444
+
445
+ // Add callback to handle the result
446
+ future.addListener(new Runnable() {
447
+ @Override
448
+ public void run() {
449
+ try {
450
+ FocusMeteringResult result = future.get();
451
+ JSObject response = new JSObject();
452
+ response.put("success", true);
453
+ response.put("autoFocusSuccessful", result.isFocusSuccessful());
454
+
455
+ if (includeExposure) {
456
+ // Note: CameraX FocusMeteringResult doesn't provide separate exposure success status
457
+ // We'll indicate that exposure was attempted along with focus
458
+ response.put("autoExposureSuccessful", result.isFocusSuccessful());
459
+ }
460
+
461
+ response.put("x", x);
462
+ response.put("y", y);
463
+
464
+ call.resolve(response);
465
+ } catch (Exception e) {
466
+ Log.e("Camera", "Focus operation failed", e);
467
+ call.reject("Focus operation failed: " + e.getMessage());
468
+ }
469
+ }
470
+ }, ContextCompat.getMainExecutor(getContext()));
471
+
472
+ } catch (Exception e) {
473
+ Log.e("Camera", "Error setting focus", e);
474
+ call.reject("Error setting focus: " + e.getMessage());
475
+ }
476
+ }
477
+ });
478
+ }
479
+
480
+ @PluginMethod
481
+ public void setAutoFocusMode(PluginCall call) {
482
+ if (camera == null) {
483
+ call.reject("Camera not initialized");
484
+ return;
350
485
  }
351
486
 
352
- } else {
353
- call.reject("Invalid focus coordinates");
354
- }
487
+ getActivity().runOnUiThread(new Runnable() {
488
+ @Override
489
+ public void run() {
490
+ try {
491
+ String mode = call.getString("mode", "auto");
492
+
493
+ switch (mode.toLowerCase()) {
494
+ case "auto":
495
+ // Default auto-focus behavior (continuous)
496
+ // CameraX handles this automatically
497
+ break;
498
+ case "manual":
499
+ // For manual focus, we would need to disable auto-focus
500
+ // This is more complex and would require camera2 interop
501
+ Log.w("Camera", "Manual focus mode not fully supported in CameraX");
502
+ break;
503
+ case "continuous":
504
+ // This is the default behavior in CameraX
505
+ break;
506
+ default:
507
+ call.reject("Unsupported focus mode: " + mode);
508
+ return;
509
+ }
510
+
511
+ JSObject result = new JSObject();
512
+ result.put("success", true);
513
+ result.put("mode", mode);
514
+ call.resolve(result);
515
+ } catch (Exception e) {
516
+ call.reject("Error setting auto focus mode: " + e.getMessage());
517
+ }
518
+ }
519
+ });
520
+ }
521
+
522
+ @PluginMethod
523
+ public void resetFocus(PluginCall call) {
524
+ if (camera == null) {
525
+ call.reject("Camera not initialized");
526
+ return;
527
+ }
528
+
529
+ getActivity().runOnUiThread(new Runnable() {
530
+ @Override
531
+ public void run() {
532
+ try {
533
+ // Cancel any ongoing focus operations
534
+ camera.getCameraControl().cancelFocusAndMetering();
535
+
536
+ JSObject result = new JSObject();
537
+ result.put("success", true);
538
+ call.resolve(result);
539
+ } catch (Exception e) {
540
+ call.reject("Error resetting focus: " + e.getMessage());
541
+ }
542
+ }
543
+ });
355
544
  }
356
545
 
357
546
  @PluginMethod
@@ -28,6 +28,9 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
28
28
  var lastValidOrientation = "portrait"
29
29
  var focusView: UIView?
30
30
  var isFocusAnimating = false
31
+ var focusCompletionTimer: Timer?
32
+ var lastFocusTime: Date = Date()
33
+ private let focusThrottleInterval: TimeInterval = 0.5
31
34
  @objc func initialize(_ call: CAPPluginCall) {
32
35
  // Initialize a camera view for previewing video.
33
36
  DispatchQueue.main.sync {
@@ -152,6 +155,10 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
152
155
  // If the input can be added, add it to the session.
153
156
  if self.captureSession.canAddInput(videoInput) {
154
157
  self.captureSession.addInput(videoInput)
158
+
159
+ // Configure camera device for optimal focus performance
160
+ try self.configureCameraForOptimalFocus(device: videoDevice)
161
+
155
162
  self.previewView.videoPreviewLayer.session = self.captureSession
156
163
  self.previewView.videoPreviewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
157
164
 
@@ -169,7 +176,12 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
169
176
 
170
177
  self.photoOutput = AVCapturePhotoOutput()
171
178
  self.photoOutput.isHighResolutionCaptureEnabled = true
172
- //self.photoOutput.
179
+
180
+ // Configure photo output for better focus
181
+ if #available(iOS 13.0, *) {
182
+ self.photoOutput.maxPhotoQualityPrioritization = .quality
183
+ }
184
+
173
185
  if self.captureSession.canAddOutput(self.photoOutput) {
174
186
  self.captureSession.addOutput(photoOutput)
175
187
  }
@@ -195,13 +207,100 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
195
207
  print(error)
196
208
  }
197
209
  }
210
+
211
+ private func configureCameraForOptimalFocus(device: AVCaptureDevice) throws {
212
+ try device.lockForConfiguration()
213
+
214
+ // Set optimal focus mode for continuous operation
215
+ if device.isFocusModeSupported(.continuousAutoFocus) {
216
+ device.focusMode = .continuousAutoFocus
217
+ }
218
+
219
+ // Set optimal exposure mode
220
+ if device.isExposureModeSupported(.continuousAutoExposure) {
221
+ device.exposureMode = .continuousAutoExposure
222
+ }
223
+
224
+ // Enable subject area change monitoring for responsive focus
225
+ device.isSubjectAreaChangeMonitoringEnabled = true
226
+
227
+ // Configure white balance for better color accuracy
228
+ if device.isWhiteBalanceModeSupported(.continuousAutoWhiteBalance) {
229
+ device.whiteBalanceMode = .continuousAutoWhiteBalance
230
+ }
231
+
232
+ // Set focus range restriction if supported (helps with faster focus)
233
+ if device.isAutoFocusRangeRestrictionSupported {
234
+ device.autoFocusRangeRestriction = .none // Allow full range for versatility
235
+ }
236
+
237
+ // Enable smooth auto focus if available (iOS 7+)
238
+ if device.isSmoothAutoFocusSupported {
239
+ device.isSmoothAutoFocusEnabled = true
240
+ }
241
+
242
+ device.unlockForConfiguration()
243
+ }
244
+
198
245
  func takePhotoWithAVFoundation(){
199
246
  //self.captureSession.sessionPreset = AVCaptureSession.Preset.hd4K3840x2160
200
247
  let photoSettings: AVCapturePhotoSettings
201
- photoSettings = AVCapturePhotoSettings()
248
+
249
+ // Use HEIF format if available for better quality
250
+ if #available(iOS 11.0, *), self.photoOutput.availablePhotoCodecTypes.contains(.hevc) {
251
+ photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
252
+ } else {
253
+ photoSettings = AVCapturePhotoSettings()
254
+ }
255
+
202
256
  photoSettings.isHighResolutionPhotoEnabled = true
203
257
 
204
- self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
258
+ // Enable auto-focus before capture for better focus accuracy
259
+ if #available(iOS 11.0, *) {
260
+ photoSettings.photoQualityPrioritization = .quality
261
+ }
262
+
263
+ // Enable auto-focus and auto-exposure for optimal capture
264
+ if #available(iOS 14.1, *) {
265
+ photoSettings.isAutoContentAwareDistortionCorrectionEnabled = true
266
+ }
267
+
268
+ // Trigger focus before capture if the device supports it
269
+ let device = self.videoInput.device
270
+ if device.isFocusModeSupported(.autoFocus) {
271
+ do {
272
+ try device.lockForConfiguration()
273
+
274
+ // Temporarily switch to autoFocus for the photo capture
275
+ let previousFocusMode = device.focusMode
276
+ device.focusMode = .autoFocus
277
+
278
+ device.unlockForConfiguration()
279
+
280
+ // Wait a brief moment for focus to settle, then capture
281
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
282
+ self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
283
+
284
+ // Restore previous focus mode after a short delay
285
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
286
+ do {
287
+ try device.lockForConfiguration()
288
+ device.focusMode = previousFocusMode
289
+ device.unlockForConfiguration()
290
+ } catch {
291
+ print("Could not restore focus mode: \(error)")
292
+ }
293
+ }
294
+ }
295
+ } catch {
296
+ // If focus configuration fails, capture anyway
297
+ print("Could not configure focus for capture: \(error)")
298
+ self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
299
+ }
300
+ } else {
301
+ // Capture immediately if auto focus isn't supported
302
+ self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
303
+ }
205
304
  }
206
305
 
207
306
  public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
@@ -545,6 +644,12 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
545
644
  if let x = call.getFloat("x"), let y = call.getFloat("y"),
546
645
  x >= 0.0 && x <= 1.0, y >= 0.0 && y <= 1.0 {
547
646
  let point = CGPoint(x: CGFloat(x), y: CGFloat(y))
647
+
648
+ // Check if focus is currently animating and reset if stuck
649
+ if isFocusAnimating {
650
+ resetFocusIfStuck()
651
+ }
652
+
548
653
  focusWithPoint(point: point)
549
654
 
550
655
  // Calculate the point in the preview layer's coordinate space
@@ -557,6 +662,35 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
557
662
  }
558
663
  }
559
664
 
665
+ private func resetFocusIfStuck() {
666
+ // Remove any existing focus indicator
667
+ focusView?.removeFromSuperview()
668
+ focusCompletionTimer?.invalidate()
669
+ isFocusAnimating = false
670
+
671
+ // Reset focus to continuous mode
672
+ let device = self.videoInput.device
673
+ do {
674
+ try device.lockForConfiguration()
675
+ if device.isFocusModeSupported(.continuousAutoFocus) {
676
+ device.focusMode = .continuousAutoFocus
677
+ }
678
+ device.unlockForConfiguration()
679
+ } catch {
680
+ print("Could not reset focus: \(error)")
681
+ }
682
+ }
683
+
684
+ @objc func resetFocus(_ call: CAPPluginCall) {
685
+ resetFocusIfStuck()
686
+
687
+ // Reset to center focus
688
+ let centerPoint = CGPoint(x: 0.5, y: 0.5)
689
+ focusWithPoint(point: centerPoint)
690
+
691
+ call.resolve()
692
+ }
693
+
560
694
  @objc func handleTapToFocus(_ gesture: UITapGestureRecognizer) {
561
695
  let location = gesture.location(in: self.previewView)
562
696
  let convertedPoint = self.previewView.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location)
@@ -567,55 +701,114 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
567
701
 
568
702
  func focusWithPoint(point: CGPoint) {
569
703
  let device = self.videoInput.device
704
+
705
+ let now = Date()
706
+ if now.timeIntervalSince(lastFocusTime) < focusThrottleInterval {
707
+ return
708
+ }
709
+ lastFocusTime = now
710
+
570
711
  do {
571
712
  try device.lockForConfiguration()
572
713
 
714
+ focusCompletionTimer?.invalidate()
715
+
573
716
  if device.isFocusPointOfInterestSupported {
574
717
  device.focusPointOfInterest = point
575
- device.focusMode = .autoFocus
718
+
719
+ if device.isFocusModeSupported(.continuousAutoFocus) {
720
+ device.focusMode = .continuousAutoFocus
721
+ } else if device.isFocusModeSupported(.autoFocus) {
722
+ device.focusMode = .autoFocus
723
+ } else {
724
+ device.focusMode = .locked
725
+ }
726
+
727
+ if device.focusMode == .autoFocus {
728
+ NotificationCenter.default.addObserver(
729
+ self,
730
+ selector: #selector(subjectAreaDidChange),
731
+ name: .AVCaptureDeviceSubjectAreaDidChange,
732
+ object: device
733
+ )
734
+ }
576
735
  }
577
736
 
578
737
  if device.isExposurePointOfInterestSupported {
579
738
  device.exposurePointOfInterest = point
580
- device.exposureMode = .continuousAutoExposure
739
+
740
+ if device.isExposureModeSupported(.continuousAutoExposure) {
741
+ device.exposureMode = .continuousAutoExposure
742
+ } else if device.isExposureModeSupported(.autoExpose) {
743
+ device.exposureMode = .autoExpose
744
+ }
581
745
  }
582
746
 
747
+ device.isSubjectAreaChangeMonitoringEnabled = true
748
+
583
749
  device.unlockForConfiguration()
584
750
 
751
+ focusCompletionTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
752
+ DispatchQueue.main.async {
753
+ self?.hideFocusIndicatorWithCompletion()
754
+ }
755
+ }
756
+
585
757
  } catch {
586
758
  print("Could not focus: \(error.localizedDescription)")
587
759
  }
588
760
  }
589
761
 
762
+ @objc private func subjectAreaDidChange(notification: NSNotification) {
763
+ DispatchQueue.main.async { [weak self] in
764
+ self?.hideFocusIndicatorWithCompletion()
765
+ }
766
+
767
+ NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: notification.object)
768
+ }
769
+
770
+ private func hideFocusIndicatorWithCompletion() {
771
+ guard let focusView = self.focusView else { return }
772
+
773
+ UIView.animate(withDuration: 0.2, animations: {
774
+ focusView.alpha = 0.0
775
+ focusView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
776
+ }) { _ in
777
+ focusView.removeFromSuperview()
778
+ focusView.transform = CGAffineTransform.identity
779
+ self.isFocusAnimating = false
780
+ }
781
+ }
782
+
590
783
  func showFocusView(at point: CGPoint) {
591
784
  if isFocusAnimating {
592
785
  self.focusView?.removeFromSuperview()
786
+ focusCompletionTimer?.invalidate()
593
787
  }
594
788
 
595
- // Create focus view if needed
789
+ // Create focus view if needed - but make it invisible
596
790
  if self.focusView == nil {
597
- self.focusView = UIView(frame: CGRect(x: 0, y: 0, width: 75, height: 75))
598
- self.focusView?.layer.borderColor = UIColor.yellow.cgColor
599
- self.focusView?.layer.borderWidth = 1.0
791
+ self.focusView = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
792
+ // Make the focus view completely transparent
793
+ self.focusView?.layer.borderColor = UIColor.clear.cgColor
794
+ self.focusView?.layer.borderWidth = 0.0
795
+ self.focusView?.layer.cornerRadius = 40
600
796
  self.focusView?.backgroundColor = .clear
797
+ self.focusView?.alpha = 0.0
798
+
799
+ // Remove the inner circle to make it completely invisible
800
+ // No inner circle added
601
801
  }
602
802
 
603
803
  self.focusView?.center = point
604
- self.focusView?.alpha = 0.0
804
+ self.focusView?.alpha = 0.0 // Keep invisible
805
+ self.focusView?.transform = CGAffineTransform.identity
605
806
  self.previewView.addSubview(self.focusView!)
606
807
 
607
808
  self.isFocusAnimating = true
608
809
 
609
- UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseInOut, animations: {
610
- self.focusView?.alpha = 1.0
611
- }, completion: { _ in
612
- UIView.animate(withDuration: 0.3, delay: 0.5, options: .curveEaseInOut, animations: {
613
- self.focusView?.alpha = 0.0
614
- }, completion: { _ in
615
- self.focusView?.removeFromSuperview()
616
- self.isFocusAnimating = false
617
- })
618
- })
810
+ // Skip the animation since the view is invisible
811
+ // Focus functionality still works, just no visual feedback
619
812
  }
620
813
 
621
814
  @objc func requestCameraPermission(_ call: CAPPluginCall) {
@@ -718,4 +911,9 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
718
911
  takePhotoWithAVFoundation()
719
912
  }
720
913
 
914
+ deinit {
915
+ NotificationCenter.default.removeObserver(self)
916
+ focusCompletionTimer?.invalidate()
917
+ }
918
+
721
919
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-camera-forked",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "A capacitor camera plugin - A custom Capacitor camera plugin with additional features.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -13,7 +13,7 @@
13
13
  "ios/Plugin/",
14
14
  "CapacitorPluginCameraForked.podspec"
15
15
  ],
16
- "author": "Lihang Xu + Addons",
16
+ "author": "Lihang Xu + Modified",
17
17
  "license": "MIT",
18
18
  "repository": {
19
19
  "type": "git",