capacitor-plugin-camera-forked 3.0.96 → 3.0.99

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.
@@ -33,9 +33,6 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
33
33
  private let focusThrottleInterval: TimeInterval = 0.5
34
34
  var currentCameraDevice: AVCaptureDevice?
35
35
 
36
- // Simple focus management with fixed delay
37
- var continuousFocusReturnTimer: Timer?
38
-
39
36
  // Store the desired JPEG quality, set during initialization
40
37
  var desiredJpegQuality: CGFloat = 0.95 // Default to high quality (0.0-1.0)
41
38
 
@@ -379,21 +376,31 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
379
376
 
380
377
  device.unlockForConfiguration()
381
378
 
379
+ if device.isAdjustingFocus {
382
380
  // Wait longer for focus to settle, especially for close objects
383
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
384
- self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
385
-
386
- // Restore previous settings after capture
387
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
388
- do {
389
- try device.lockForConfiguration()
390
- device.focusMode = previousFocusMode
391
- device.exposureMode = previousExposureMode
392
- device.unlockForConfiguration()
393
- } catch {
394
- print("Could not restore camera settings: \(error)")
381
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
382
+ self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
383
+
384
+ // Restore previous settings after capture
385
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
386
+ do {
387
+ try device.lockForConfiguration()
388
+ device.focusMode = previousFocusMode
389
+ device.exposureMode = previousExposureMode
390
+ device.unlockForConfiguration()
391
+
392
+ // Focus to center after settings restoration to ensure optimal focus state
393
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
394
+ self.focusToCenterSafely()
395
+ }
396
+ } catch {
397
+ print("Could not restore camera settings: \(error)")
398
+ }
395
399
  }
396
400
  }
401
+ } else {
402
+ self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
403
+
397
404
  }
398
405
  } catch {
399
406
  // If focus configuration fails, capture anyway
@@ -425,6 +432,10 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
425
432
  let image = UIImage(data: imageData)
426
433
  let base64 = getBase64FromImage(image: image!, quality: desiredJpegQuality)
427
434
  ret["base64"] = base64
435
+
436
+ // Detect blur if base64 is included
437
+ let blurScore = calculateBlurScore(image: image!)
438
+ ret["blurScore"] = blurScore
428
439
  }
429
440
  do {
430
441
 
@@ -435,6 +446,11 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
435
446
  }
436
447
  takePhotoCall.resolve(ret)
437
448
  takePhotoCall = nil
449
+
450
+ // Focus to center after photo capture to ensure optimal focus state for next operation
451
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
452
+ self.focusToCenterSafely()
453
+ }
438
454
  }
439
455
  }
440
456
  }
@@ -480,10 +496,24 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
480
496
  let qualityValue = takeSnapshotCall.getFloat("quality") ?? 100.0
481
497
  let quality = CGFloat(qualityValue / 100.0) // Convert percentage to 0.0-1.0 range
482
498
  let base64 = getBase64FromImage(image: normalized, quality: quality);
499
+
483
500
  var ret = PluginCallResultData()
484
501
  ret["base64"] = base64
502
+
503
+ // Only detect blur if checkBlur option is true
504
+ let shouldCheckBlur = takeSnapshotCall.getBool("checkBlur", false)
505
+ if shouldCheckBlur {
506
+ let blurScore = calculateBlurScore(image: normalized)
507
+ ret["blurScore"] = blurScore
508
+ }
509
+
485
510
  takeSnapshotCall.resolve(ret)
486
511
  takeSnapshotCall = nil
512
+
513
+ // Focus to center after snapshot to ensure optimal focus state for next operation
514
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
515
+ self.focusToCenterSafely()
516
+ }
487
517
  }
488
518
  if saveFrameCall != nil {
489
519
  CameraPreviewPlugin.frameTaken = normalized
@@ -491,6 +521,11 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
491
521
  ret["success"] = true
492
522
  saveFrameCall.resolve(ret)
493
523
  saveFrameCall = nil
524
+
525
+ // Focus to center after frame save to ensure optimal focus state for next operation
526
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
527
+ self.focusToCenterSafely()
528
+ }
494
529
  }
495
530
  }
496
531
  }
@@ -866,6 +901,12 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
866
901
  factor = min(factor, device.maxAvailableVideoZoomFactor)
867
902
  device.videoZoomFactor = factor
868
903
  device.unlockForConfiguration()
904
+
905
+ // Trigger focus to center after zoom change
906
+ // if device.isFocusModeSupported(.autoFocus) {
907
+ // let centerPoint = CGPoint(x: 0.5, y: 0.5)
908
+ // focusWithPoint(point: centerPoint)
909
+ // }
869
910
  } catch {
870
911
  print("Zoom could not be used")
871
912
  }
@@ -877,17 +918,51 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
877
918
  x >= 0.0 && x <= 1.0, y >= 0.0 && y <= 1.0 {
878
919
  let point = CGPoint(x: CGFloat(x), y: CGFloat(y))
879
920
 
880
- // Simplified focus without complex throttling or maintenance
921
+ // Check if focus is currently animating and reset if stuck
922
+ if isFocusAnimating {
923
+ resetFocusIfStuck()
924
+ }
925
+
881
926
  focusWithPoint(point: point)
882
927
 
928
+ // Calculate the point in the preview layer's coordinate space
929
+ //let previewPoint = CGPoint(x: point.x * previewView.bounds.width,
930
+ // y: point.y * previewView.bounds.height)
931
+ // showFocusView(at: previewPoint)
883
932
  call.resolve()
884
933
  } else {
885
934
  call.reject("Invalid coordinates. Provide normalized x,y values (0.0-1.0)")
886
935
  }
887
936
  }
888
937
 
938
+ private func resetFocusIfStuck() {
939
+ DispatchQueue.main.async { [weak self] in
940
+ guard let self = self else { return }
941
+
942
+ // Remove any existing focus indicator
943
+ self.focusView?.removeFromSuperview()
944
+ self.focusCompletionTimer?.invalidate()
945
+ self.isFocusAnimating = false
946
+
947
+ // Reset focus to continuous mode
948
+ guard let videoInput = self.videoInput else { return }
949
+ let device = videoInput.device
950
+ do {
951
+ try device.lockForConfiguration()
952
+ if device.isFocusModeSupported(.continuousAutoFocus) {
953
+ device.focusMode = .continuousAutoFocus
954
+ }
955
+ device.unlockForConfiguration()
956
+ } catch {
957
+ print("Could not reset focus: \(error)")
958
+ }
959
+ }
960
+ }
961
+
889
962
  @objc func resetFocus(_ call: CAPPluginCall) {
890
- // Simple reset to center focus
963
+ resetFocusIfStuck()
964
+
965
+ // Reset to center focus
891
966
  let centerPoint = CGPoint(x: 0.5, y: 0.5)
892
967
  focusWithPoint(point: centerPoint)
893
968
 
@@ -899,57 +974,103 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
899
974
  let convertedPoint = self.previewView.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location)
900
975
 
901
976
  focusWithPoint(point: convertedPoint)
977
+ // showFocusView(at: location)
902
978
  }
903
979
 
904
980
  func focusWithPoint(point: CGPoint) {
905
981
  guard let videoInput = self.videoInput else { return }
906
982
  let device = videoInput.device
907
983
 
984
+ let now = Date()
985
+ if now.timeIntervalSince(lastFocusTime) < focusThrottleInterval {
986
+ return
987
+ }
988
+ lastFocusTime = now
989
+
908
990
  do {
909
991
  try device.lockForConfiguration()
910
992
 
911
- // Cancel any existing focus/exposure operations
912
- if device.isFocusModeSupported(.autoFocus) && device.isFocusPointOfInterestSupported {
993
+ focusCompletionTimer?.invalidate()
994
+
995
+ if device.isFocusPointOfInterestSupported {
913
996
  device.focusPointOfInterest = point
914
- device.focusMode = .autoFocus
997
+
998
+ // Use autoFocus for more aggressive focusing on specific points
999
+ if device.isFocusModeSupported(.autoFocus) {
1000
+ device.focusMode = .autoFocus
1001
+ } else if device.isFocusModeSupported(.continuousAutoFocus) {
1002
+ device.focusMode = .continuousAutoFocus
1003
+ }
915
1004
  }
916
1005
 
917
- if device.isExposureModeSupported(.autoExpose) && device.isExposurePointOfInterestSupported {
1006
+ if device.isExposurePointOfInterestSupported {
918
1007
  device.exposurePointOfInterest = point
919
- device.exposureMode = .autoExpose
1008
+
1009
+ // Use autoExpose for specific point exposure
1010
+ if device.isExposureModeSupported(.autoExpose) {
1011
+ device.exposureMode = .autoExpose
1012
+ } else if device.isExposureModeSupported(.continuousAutoExposure) {
1013
+ device.exposureMode = .continuousAutoExposure
1014
+ }
920
1015
  }
921
1016
 
922
- // Ensure full focus range is available
1017
+ // Ensure full focus range is available for close objects
923
1018
  if device.isAutoFocusRangeRestrictionSupported {
924
1019
  device.autoFocusRangeRestriction = .none
925
1020
  }
926
1021
 
927
- device.unlockForConfiguration()
1022
+ device.isSubjectAreaChangeMonitoringEnabled = true
928
1023
 
929
- // Cancel any existing timer
930
- continuousFocusReturnTimer?.invalidate()
1024
+ device.unlockForConfiguration()
931
1025
 
932
- // Return to continuous focus after fixed delay
933
- // This provides predictable behavior - user has 2 seconds to take photo after tap-to-focus
934
- continuousFocusReturnTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in
1026
+ // Switch back to continuous focus after a delay to maintain automatic focus
1027
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
935
1028
  self?.returnToContinuousFocus()
936
1029
  }
1030
+
937
1031
 
938
1032
  } catch {
939
1033
  print("Could not focus: \(error.localizedDescription)")
940
1034
  }
941
1035
  }
942
1036
 
1037
+ // @objc private func subjectAreaDidChange(notification: NSNotification) {
1038
+ // DispatchQueue.main.async { [weak self] in
1039
+ // self?.hideFocusIndicatorWithCompletion()
1040
+ // }
1041
+
1042
+ // NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: notification.object)
1043
+ // }
1044
+
1045
+ private func focusToCenterSafely() {
1046
+ // Check if enough time has passed since last focus operation to avoid conflicts
1047
+ let now = Date()
1048
+ if now.timeIntervalSince(lastFocusTime) < focusThrottleInterval {
1049
+ return
1050
+ }
1051
+
1052
+ // Only focus if not currently adjusting focus to avoid interrupting ongoing operations
1053
+ guard let videoInput = self.videoInput else { return }
1054
+ let device = videoInput.device
1055
+
1056
+ if !device.isAdjustingFocus {
1057
+ let centerPoint = CGPoint(x: 0.5, y: 0.5)
1058
+ focusWithPoint(point: centerPoint)
1059
+ }
1060
+ }
1061
+
943
1062
  private func returnToContinuousFocus() {
944
1063
  guard let videoInput = self.videoInput else { return }
945
1064
  let device = videoInput.device
946
1065
  do {
947
1066
  try device.lockForConfiguration()
948
1067
 
1068
+ // Return to continuous auto focus for automatic operation
949
1069
  if device.isFocusModeSupported(.continuousAutoFocus) {
950
1070
  device.focusMode = .continuousAutoFocus
951
1071
  }
952
1072
 
1073
+ // Return to continuous auto exposure
953
1074
  if device.isExposureModeSupported(.continuousAutoExposure) {
954
1075
  device.exposureMode = .continuousAutoExposure
955
1076
  }
@@ -960,6 +1081,22 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
960
1081
  }
961
1082
  }
962
1083
 
1084
+
1085
+
1086
+ // private func hideFocusIndicatorWithCompletion() {
1087
+ // DispatchQueue.main.async { [weak self] in
1088
+ // guard let self = self, let focusView = self.focusView else { return }
1089
+
1090
+ // UIView.animate(withDuration: 0.2, animations: {
1091
+ // focusView.alpha = 0.0
1092
+ // focusView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
1093
+ // }) { _ in
1094
+ // focusView.removeFromSuperview()
1095
+ // focusView.transform = CGAffineTransform.identity
1096
+ // self.isFocusAnimating = false
1097
+ // }
1098
+ // }
1099
+ // }
963
1100
  @objc func requestCameraPermission(_ call: CAPPluginCall) {
964
1101
  AVCaptureDevice.requestAccess(for: .video) { granted in
965
1102
  DispatchQueue.main.async {
@@ -1005,6 +1142,15 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
1005
1142
 
1006
1143
  @objc func takeSnapshot(_ call: CAPPluginCall) {
1007
1144
  call.keepAlive = true
1145
+
1146
+ let device = videoInput.device
1147
+ if device.isFocusModeSupported(.autoFocus) {
1148
+ // Trigger focus to center before capturing
1149
+ let centerPoint = CGPoint(x: 0.5, y: 0.5)
1150
+ focusWithPoint(point: centerPoint)
1151
+ }
1152
+
1153
+ // Set the call
1008
1154
  takeSnapshotCall = call
1009
1155
  }
1010
1156
 
@@ -1075,59 +1221,88 @@ public class CameraPreviewPlugin: CAPPlugin, AVCaptureVideoDataOutputSampleBuffe
1075
1221
  }
1076
1222
 
1077
1223
  @objc func takePhoto(_ call: CAPPluginCall) {
1224
+ call.keepAlive = true
1078
1225
  takePhotoCall = call
1226
+ takePhotoWithAVFoundation()
1227
+ }
1228
+
1229
+ deinit {
1230
+ NotificationCenter.default.removeObserver(self)
1231
+ focusCompletionTimer?.invalidate()
1232
+ }
1233
+
1234
+ // MARK: - Blur Detection
1235
+ private func calculateBlurScore(image: UIImage) -> Double {
1236
+ guard let cgImage = image.cgImage else { return 0.0 }
1079
1237
 
1080
- // Configure photo settings
1081
- let photoSettings = AVCapturePhotoSettings()
1082
- photoSettings.isHighResolutionPhotoEnabled = true
1238
+ let context = CIContext()
1239
+ let ciImage = CIImage(cgImage: cgImage)
1083
1240
 
1084
- // Use JPEG format with quality setting
1085
- if #available(iOS 11.0, *) {
1086
- photoSettings.photoQualityPrioritization = .quality
1087
- }
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)
1088
1245
 
1089
- // Configure for optimal photo capture without complex focus management
1090
- guard let device = videoInput?.device else {
1091
- self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
1092
- return
1093
- }
1246
+ guard let grayscaleImage = grayscaleFilter.outputImage else { return 0.0 }
1094
1247
 
1095
- if device.isFocusModeSupported(.autoFocus) {
1096
- do {
1097
- try device.lockForConfiguration()
1098
-
1099
- // Set to auto focus for capture
1100
- device.focusMode = .autoFocus
1101
-
1102
- // Ensure full focus range
1103
- if device.isAutoFocusRangeRestrictionSupported {
1104
- device.autoFocusRangeRestriction = .none
1105
- }
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)
1273
+
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)
1280
+
1281
+ var variance = 0.0
1282
+ var sampleCount = 0
1283
+
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)
1106
1289
 
1107
- // Set exposure for photo
1108
- if device.isExposureModeSupported(.autoExpose) {
1109
- device.exposureMode = .autoExpose
1110
- }
1290
+ let sampleFilter = CIFilter(name: "CIAreaAverage")!
1291
+ sampleFilter.setValue(filteredImage, forKey: kCIInputImageKey)
1292
+ sampleFilter.setValue(CIVector(cgRect: sampleRect), forKey: kCIInputExtentKey)
1111
1293
 
1112
- device.unlockForConfiguration()
1113
-
1114
- // Wait briefly for focus, then capture
1115
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
1116
- self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
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
1117
1301
  }
1118
- } catch {
1119
- print("Could not configure focus for capture: \(error)")
1120
- self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
1121
1302
  }
1122
- } else {
1123
- // Capture immediately if auto focus isn't supported
1124
- self.photoOutput.capturePhoto(with: photoSettings, delegate: self)
1125
1303
  }
1126
- }
1127
-
1128
- deinit {
1129
- focusCompletionTimer?.invalidate()
1130
- continuousFocusReturnTimer?.invalidate()
1304
+
1305
+ return sampleCount > 0 ? variance / Double(sampleCount) : 0.0
1131
1306
  }
1132
1307
 
1133
1308
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-camera-forked",
3
- "version": "3.0.96",
3
+ "version": "3.0.99",
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",