capacitor-plugin-camera-forked 3.0.99 → 3.0.101

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.
@@ -339,6 +339,9 @@ public class CameraPreviewPlugin extends Plugin {
339
339
 
340
340
  camera.getCameraControl().startFocusAndMetering(initialFocus);
341
341
 
342
+ // Enable continuous auto-focus by starting a background focus monitoring
343
+ startContinuousAutoFocus();
344
+
342
345
  Log.d("Camera", "Initialized responsive auto-focus with continuous monitoring");
343
346
  } catch (Exception e) {
344
347
  Log.e("Camera", "Failed to initialize responsive auto-focus: " + e.getMessage());
@@ -645,6 +648,15 @@ public class CameraPreviewPlugin extends Plugin {
645
648
  response.put("autoFocusSuccessful", result.isFocusSuccessful());
646
649
  response.put("x", x);
647
650
  response.put("y", y);
651
+
652
+ // If focus failed, try a backup focus attempt to reduce need for multiple taps
653
+ if (!result.isFocusSuccessful()) {
654
+ Log.d("Camera", "Initial focus failed, attempting backup focus");
655
+ performBackupFocus(previewX, previewY);
656
+ } else {
657
+ // If manual focus was successful, maintain it with a follow-up action
658
+ maintainFocusAtPoint(previewX, previewY);
659
+ }
648
660
 
649
661
  call.resolve(response);
650
662
  } catch (Exception e) {
@@ -834,7 +846,9 @@ public class CameraPreviewPlugin extends Plugin {
834
846
  .build();
835
847
 
836
848
  camera.getCameraControl().startFocusAndMetering(restartAction);
837
-
849
+
850
+ // Restart the continuous auto-focus monitoring
851
+ startContinuousAutoFocus();
838
852
  }
839
853
 
840
854
  JSObject result = new JSObject();
@@ -1404,4 +1418,154 @@ public class CameraPreviewPlugin extends Plugin {
1404
1418
  return count > 0 ? variance / count : 0.0;
1405
1419
  }
1406
1420
 
1421
+ /**
1422
+ * Start continuous auto-focus monitoring for better focus stability
1423
+ */
1424
+ private void startContinuousAutoFocus() {
1425
+ if (camera != null && previewView != null) {
1426
+ // Use a separate executor for continuous focus to avoid blocking
1427
+ ExecutorService focusExecutor = Executors.newSingleThreadExecutor();
1428
+
1429
+ focusExecutor.execute(new Runnable() {
1430
+ @Override
1431
+ public void run() {
1432
+ try {
1433
+ while (camera != null && camera.getCameraInfo().getCameraState().getValue().getType() == CameraState.Type.OPEN) {
1434
+ Thread.sleep(800); // Check every 800ms for faster transitions
1435
+
1436
+ getActivity().runOnUiThread(new Runnable() {
1437
+ @Override
1438
+ public void run() {
1439
+ try {
1440
+ // Trigger auto-focus at center to maintain continuous focus
1441
+ MeteringPointFactory factory = previewView.getMeteringPointFactory();
1442
+ float centerX = previewView.getWidth() / 2.0f;
1443
+ float centerY = previewView.getHeight() / 2.0f;
1444
+ MeteringPoint centerPoint = factory.createPoint(centerX, centerY);
1445
+
1446
+ FocusMeteringAction continuousAction = new FocusMeteringAction.Builder(centerPoint)
1447
+ .setAutoCancelDuration(1, TimeUnit.SECONDS) // Fast 1 second for responsive transitions
1448
+ .build();
1449
+
1450
+ camera.getCameraControl().startFocusAndMetering(continuousAction);
1451
+ } catch (Exception e) {
1452
+ Log.d("Camera", "Continuous focus update failed: " + e.getMessage());
1453
+ }
1454
+ }
1455
+ });
1456
+ }
1457
+ } catch (InterruptedException e) {
1458
+ Log.d("Camera", "Continuous auto-focus stopped");
1459
+ }
1460
+ }
1461
+ });
1462
+ }
1463
+ }
1464
+
1465
+ /**
1466
+ * Perform backup focus attempt if initial focus fails
1467
+ * This reduces the need for users to tap multiple times
1468
+ */
1469
+ private void performBackupFocus(float previewX, float previewY) {
1470
+ if (camera == null || previewView == null) return;
1471
+
1472
+ // Wait a moment for the camera to settle
1473
+ ExecutorService backupFocusExecutor = Executors.newSingleThreadExecutor();
1474
+ backupFocusExecutor.execute(new Runnable() {
1475
+ @Override
1476
+ public void run() {
1477
+ try {
1478
+ Thread.sleep(200); // Wait 200ms for faster backup focus
1479
+
1480
+ getActivity().runOnUiThread(new Runnable() {
1481
+ @Override
1482
+ public void run() {
1483
+ try {
1484
+ if (camera != null && previewView != null) {
1485
+ MeteringPointFactory factory = previewView.getMeteringPointFactory();
1486
+ MeteringPoint backupPoint = factory.createPoint(previewX, previewY);
1487
+
1488
+ // Try with fast duration for responsive backup focus
1489
+ FocusMeteringAction backupAction = new FocusMeteringAction.Builder(backupPoint,
1490
+ FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE)
1491
+ .setAutoCancelDuration(1, TimeUnit.SECONDS) // Fast 1 second for backup
1492
+ .build();
1493
+
1494
+ ListenableFuture<FocusMeteringResult> backupFuture =
1495
+ camera.getCameraControl().startFocusAndMetering(backupAction);
1496
+
1497
+ backupFuture.addListener(new Runnable() {
1498
+ @Override
1499
+ public void run() {
1500
+ try {
1501
+ FocusMeteringResult backupResult = backupFuture.get();
1502
+ if (backupResult.isFocusSuccessful()) {
1503
+ Log.d("Camera", "Backup focus successful");
1504
+ maintainFocusAtPoint(previewX, previewY);
1505
+ } else {
1506
+ Log.d("Camera", "Backup focus also failed");
1507
+ }
1508
+ } catch (Exception e) {
1509
+ Log.d("Camera", "Backup focus exception: " + e.getMessage());
1510
+ }
1511
+ }
1512
+ }, ContextCompat.getMainExecutor(getContext()));
1513
+ }
1514
+ } catch (Exception e) {
1515
+ Log.d("Camera", "Backup focus setup failed: " + e.getMessage());
1516
+ }
1517
+ }
1518
+ });
1519
+ } catch (InterruptedException e) {
1520
+ Log.d("Camera", "Backup focus interrupted");
1521
+ }
1522
+ }
1523
+ });
1524
+ }
1525
+
1526
+ /**
1527
+ * Maintain focus at a specific point with repeated focus actions for stability
1528
+ */
1529
+ private void maintainFocusAtPoint(float previewX, float previewY) {
1530
+ if (camera == null || previewView == null) return;
1531
+
1532
+ // Use a separate executor for focus maintenance
1533
+ ExecutorService focusMaintainExecutor = Executors.newSingleThreadExecutor();
1534
+
1535
+ focusMaintainExecutor.execute(new Runnable() {
1536
+ @Override
1537
+ public void run() {
1538
+ try {
1539
+ // Maintain focus for 2 seconds with quick refocus for responsive transitions
1540
+ for (int i = 0; i < 2; i++) {
1541
+ Thread.sleep(1000); // Wait 1 second between focus actions
1542
+
1543
+ getActivity().runOnUiThread(new Runnable() {
1544
+ @Override
1545
+ public void run() {
1546
+ try {
1547
+ if (camera != null && camera.getCameraInfo().getCameraState().getValue().getType() == CameraState.Type.OPEN) {
1548
+ MeteringPointFactory factory = previewView.getMeteringPointFactory();
1549
+ MeteringPoint maintainPoint = factory.createPoint(previewX, previewY);
1550
+
1551
+ FocusMeteringAction maintainAction = new FocusMeteringAction.Builder(maintainPoint)
1552
+ .setAutoCancelDuration(1, TimeUnit.SECONDS) // Fast 1 second maintenance
1553
+ .build();
1554
+
1555
+ camera.getCameraControl().startFocusAndMetering(maintainAction);
1556
+ Log.d("Camera", "Maintaining focus at tapped point");
1557
+ }
1558
+ } catch (Exception e) {
1559
+ Log.d("Camera", "Focus maintenance failed: " + e.getMessage());
1560
+ }
1561
+ }
1562
+ });
1563
+ }
1564
+ } catch (InterruptedException e) {
1565
+ Log.d("Camera", "Focus maintenance interrupted");
1566
+ }
1567
+ }
1568
+ });
1569
+ }
1570
+
1407
1571
  }
@@ -1235,74 +1235,80 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
1235
1235
  private func calculateBlurScore(image: UIImage) -> Double {
1236
1236
  guard let cgImage = image.cgImage else { return 0.0 }
1237
1237
 
1238
- let context = CIContext()
1239
- let ciImage = CIImage(cgImage: cgImage)
1238
+ let width = cgImage.width
1239
+ let height = cgImage.height
1240
1240
 
1241
- // Convert to grayscale for better blur detection
1242
- let grayscaleFilter = CIFilter(name: "CIColorControls")!
1243
- grayscaleFilter.setValue(ciImage, forKey: kCIInputImageKey)
1244
- grayscaleFilter.setValue(0.0, forKey: kCIInputSaturationKey)
1241
+ // Create bitmap context to access pixel data
1242
+ let bytesPerPixel = 4
1243
+ let bytesPerRow = bytesPerPixel * width
1244
+ let bitsPerComponent = 8
1245
1245
 
1246
- guard let grayscaleImage = grayscaleFilter.outputImage else { return 0.0 }
1246
+ guard let context = CGContext(
1247
+ data: nil,
1248
+ width: width,
1249
+ height: height,
1250
+ bitsPerComponent: bitsPerComponent,
1251
+ bytesPerRow: bytesPerRow,
1252
+ space: CGColorSpaceCreateDeviceRGB(),
1253
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
1254
+ ) else { return 0.0 }
1247
1255
 
1248
- // Apply Laplacian filter for edge detection
1249
- let laplacianKernel: [Float] = [
1250
- 0, -1, 0,
1251
- -1, 4, -1,
1252
- 0, -1, 0
1253
- ]
1254
-
1255
- let convolutionFilter = CIFilter(name: "CIConvolution3X3")!
1256
- convolutionFilter.setValue(grayscaleImage, forKey: kCIInputImageKey)
1257
- convolutionFilter.setValue(CIVector(values: laplacianKernel, count: 9), forKey: "inputWeights")
1258
-
1259
- guard let filteredImage = convolutionFilter.outputImage else { return 0.0 }
1260
-
1261
- // Calculate variance of the Laplacian
1262
- let extent = filteredImage.extent
1263
- let inputExtent = CIVector(x: extent.origin.x, y: extent.origin.y, z: extent.size.width, w: extent.size.height)
1264
-
1265
- let averageFilter = CIFilter(name: "CIAreaAverage")!
1266
- averageFilter.setValue(filteredImage, forKey: kCIInputImageKey)
1267
- averageFilter.setValue(inputExtent, forKey: kCIInputExtentKey)
1268
-
1269
- guard let averageImage = averageFilter.outputImage else { return 0.0 }
1270
-
1271
- var bitmap = [UInt8](repeating: 0, count: 4)
1272
- context.render(averageImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
1256
+ // Draw image into context
1257
+ context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
1273
1258
 
1274
- let average = Double(bitmap[0]) / 255.0
1275
-
1276
- // Calculate variance by processing small samples of the image
1277
- let sampleSize = min(100, Int(extent.width), Int(extent.height))
1278
- let stepX = extent.width / Double(sampleSize)
1279
- let stepY = extent.height / Double(sampleSize)
1259
+ guard let pixelData = context.data else { return 0.0 }
1260
+ let data = pixelData.bindMemory(to: UInt8.self, capacity: width * height * bytesPerPixel)
1280
1261
 
1262
+ // Convert to grayscale and apply Laplacian kernel (same as Android/Web)
1281
1263
  var variance = 0.0
1282
- var sampleCount = 0
1264
+ var count = 0
1283
1265
 
1284
- for i in 0..<sampleSize {
1285
- for j in 0..<sampleSize {
1286
- let x = extent.origin.x + Double(i) * stepX
1287
- let y = extent.origin.y + Double(j) * stepY
1288
- let sampleRect = CGRect(x: x, y: y, width: 1, height: 1)
1266
+ // Sample every 4th pixel for performance (same as Android/Web)
1267
+ let step = 4
1268
+ for y in stride(from: step, to: height - step, by: step) {
1269
+ for x in stride(from: step, to: width - step, by: step) {
1270
+ let idx = (y * width + x) * bytesPerPixel
1289
1271
 
1290
- let sampleFilter = CIFilter(name: "CIAreaAverage")!
1291
- sampleFilter.setValue(filteredImage, forKey: kCIInputImageKey)
1292
- sampleFilter.setValue(CIVector(cgRect: sampleRect), forKey: kCIInputExtentKey)
1272
+ // Convert to grayscale using same formula as Android/Web
1273
+ let r = Double(data[idx])
1274
+ let g = Double(data[idx + 1])
1275
+ let b = Double(data[idx + 2])
1276
+ let gray = 0.299 * r + 0.587 * g + 0.114 * b
1293
1277
 
1294
- if let sampleImage = sampleFilter.outputImage {
1295
- var sampleBitmap = [UInt8](repeating: 0, count: 4)
1296
- context.render(sampleImage, toBitmap: &sampleBitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
1297
-
1298
- let value = Double(sampleBitmap[0]) / 255.0
1299
- variance += pow(value - average, 2)
1300
- sampleCount += 1
1301
- }
1278
+ // Calculate neighbors for 3x3 Laplacian kernel
1279
+ let neighbors: [Double] = [
1280
+ // Top row
1281
+ getGrayscaleValue(data: data, x: x-1, y: y-1, width: width, bytesPerPixel: bytesPerPixel),
1282
+ getGrayscaleValue(data: data, x: x, y: y-1, width: width, bytesPerPixel: bytesPerPixel),
1283
+ getGrayscaleValue(data: data, x: x+1, y: y-1, width: width, bytesPerPixel: bytesPerPixel),
1284
+ // Middle row (left and right)
1285
+ getGrayscaleValue(data: data, x: x-1, y: y, width: width, bytesPerPixel: bytesPerPixel),
1286
+ getGrayscaleValue(data: data, x: x+1, y: y, width: width, bytesPerPixel: bytesPerPixel),
1287
+ // Bottom row
1288
+ getGrayscaleValue(data: data, x: x-1, y: y+1, width: width, bytesPerPixel: bytesPerPixel),
1289
+ getGrayscaleValue(data: data, x: x, y: y+1, width: width, bytesPerPixel: bytesPerPixel),
1290
+ getGrayscaleValue(data: data, x: x+1, y: y+1, width: width, bytesPerPixel: bytesPerPixel)
1291
+ ]
1292
+
1293
+ // Apply 3x3 Laplacian kernel (matches Android/Web implementation)
1294
+ let laplacian = -neighbors[0] - neighbors[1] - neighbors[2] +
1295
+ -neighbors[3] + 8 * gray - neighbors[4] +
1296
+ -neighbors[5] - neighbors[6] - neighbors[7]
1297
+
1298
+ variance += laplacian * laplacian
1299
+ count += 1
1302
1300
  }
1303
1301
  }
1304
1302
 
1305
- return sampleCount > 0 ? variance / Double(sampleCount) : 0.0
1303
+ return count > 0 ? variance / Double(count) : 0.0
1304
+ }
1305
+
1306
+ private func getGrayscaleValue(data: UnsafePointer<UInt8>, x: Int, y: Int, width: Int, bytesPerPixel: Int) -> Double {
1307
+ let idx = (y * width + x) * bytesPerPixel
1308
+ let r = Double(data[idx])
1309
+ let g = Double(data[idx + 1])
1310
+ let b = Double(data[idx + 2])
1311
+ return 0.299 * r + 0.587 * g + 0.114 * b
1306
1312
  }
1307
1313
 
1308
1314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-camera-forked",
3
- "version": "3.0.99",
3
+ "version": "3.0.101",
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",