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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
414
|
+
// Convert normalized coordinates to preview coordinates
|
|
415
|
+
float previewX = x * previewView.getWidth();
|
|
416
|
+
float previewY = y * previewView.getHeight();
|
|
341
417
|
|
|
342
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
598
|
-
|
|
599
|
-
self.focusView?.layer.
|
|
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
|
-
|
|
610
|
-
|
|
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.
|
|
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 +
|
|
16
|
+
"author": "Lihang Xu + Modified",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|