@capgo/camera-preview 7.4.0-alpha.3 → 7.4.0-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -12
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +585 -11
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +994 -423
- package/dist/docs.json +312 -11
- package/dist/esm/definitions.d.ts +115 -11
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +24 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +16 -1
- package/dist/esm/web.js +82 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +105 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +105 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +143 -34
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +486 -132
- package/package.json +10 -2
|
@@ -25,7 +25,7 @@ class CameraController: NSObject {
|
|
|
25
25
|
var focusIndicatorView: UIView?
|
|
26
26
|
|
|
27
27
|
var flashMode = AVCaptureDevice.FlashMode.off
|
|
28
|
-
var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
|
|
28
|
+
var photoCaptureCompletionBlock: ((UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void)?
|
|
29
29
|
|
|
30
30
|
var sampleBufferCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
|
|
31
31
|
|
|
@@ -51,6 +51,25 @@ class CameraController: NSObject {
|
|
|
51
51
|
// A rear multi-lens virtual camera will have a min zoom of 1.0 but support wider angles
|
|
52
52
|
return device.position == .back && device.isVirtualDevice && device.constituentDevices.count > 1
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
// Returns the display zoom multiplier introduced in iOS 18 to map between
|
|
56
|
+
// native zoom factor and the UI-displayed zoom factor. Falls back to 1.0 on
|
|
57
|
+
// older systems or if the property is unavailable.
|
|
58
|
+
func getDisplayZoomMultiplier() -> Float {
|
|
59
|
+
var multiplier: Float = 1.0
|
|
60
|
+
// Use KVC to avoid compile-time dependency on the iOS 18 SDK symbol
|
|
61
|
+
let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
|
|
62
|
+
if #available(iOS 18.0, *), let device = device {
|
|
63
|
+
if let value = device.value(forKey: "displayVideoZoomFactorMultiplier") as? NSNumber {
|
|
64
|
+
let m = value.floatValue
|
|
65
|
+
if m > 0 { multiplier = m }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return multiplier
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Track whether an aspect ratio was explicitly requested
|
|
72
|
+
var requestedAspectRatio: String?
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
extension CameraController {
|
|
@@ -169,7 +188,7 @@ extension CameraController {
|
|
|
169
188
|
self.outputsPrepared = true
|
|
170
189
|
}
|
|
171
190
|
|
|
172
|
-
func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, initialZoomLevel: Float
|
|
191
|
+
func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, initialZoomLevel: Float?, completionHandler: @escaping (Error?) -> Void) {
|
|
173
192
|
print("[CameraPreview] 🎬 Starting prepare - position: \(cameraPosition), deviceId: \(deviceId ?? "nil"), disableAudio: \(disableAudio), cameraMode: \(cameraMode), aspectRatio: \(aspectRatio ?? "nil"), zoom: \(initialZoomLevel)")
|
|
174
193
|
|
|
175
194
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
@@ -196,7 +215,8 @@ extension CameraController {
|
|
|
196
215
|
// Configure the session
|
|
197
216
|
captureSession.beginConfiguration()
|
|
198
217
|
|
|
199
|
-
// Set aspect ratio preset
|
|
218
|
+
// Set aspect ratio preset and remember requested ratio
|
|
219
|
+
self.requestedAspectRatio = aspectRatio
|
|
200
220
|
self.configureSessionPreset(for: aspectRatio)
|
|
201
221
|
|
|
202
222
|
// Configure device inputs
|
|
@@ -252,16 +272,31 @@ extension CameraController {
|
|
|
252
272
|
private func configureSessionPreset(for aspectRatio: String?) {
|
|
253
273
|
guard let captureSession = self.captureSession else { return }
|
|
254
274
|
|
|
255
|
-
var targetPreset: AVCaptureSession.Preset = .
|
|
256
|
-
|
|
275
|
+
var targetPreset: AVCaptureSession.Preset = .photo
|
|
257
276
|
if let aspectRatio = aspectRatio {
|
|
258
277
|
switch aspectRatio {
|
|
259
278
|
case "16:9":
|
|
260
|
-
|
|
279
|
+
if captureSession.canSetSessionPreset(.hd4K3840x2160) {
|
|
280
|
+
targetPreset = .hd4K3840x2160
|
|
281
|
+
} else if captureSession.canSetSessionPreset(.hd1920x1080) {
|
|
282
|
+
targetPreset = .hd1920x1080
|
|
283
|
+
}
|
|
261
284
|
case "4:3":
|
|
262
|
-
|
|
285
|
+
if captureSession.canSetSessionPreset(.photo) {
|
|
286
|
+
targetPreset = .photo
|
|
287
|
+
} else if captureSession.canSetSessionPreset(.high) {
|
|
288
|
+
targetPreset = .high
|
|
289
|
+
} else {
|
|
290
|
+
targetPreset = captureSession.sessionPreset
|
|
291
|
+
}
|
|
263
292
|
default:
|
|
264
|
-
|
|
293
|
+
if captureSession.canSetSessionPreset(.photo) {
|
|
294
|
+
targetPreset = .photo
|
|
295
|
+
} else if captureSession.canSetSessionPreset(.high) {
|
|
296
|
+
targetPreset = .high
|
|
297
|
+
} else {
|
|
298
|
+
targetPreset = captureSession.sessionPreset
|
|
299
|
+
}
|
|
265
300
|
}
|
|
266
301
|
}
|
|
267
302
|
|
|
@@ -270,7 +305,7 @@ extension CameraController {
|
|
|
270
305
|
}
|
|
271
306
|
}
|
|
272
307
|
|
|
273
|
-
private func setInitialZoom(level: Float) {
|
|
308
|
+
private func setInitialZoom(level: Float?) {
|
|
274
309
|
let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
|
|
275
310
|
guard let device = device else {
|
|
276
311
|
print("[CameraPreview] No device available for initial zoom")
|
|
@@ -280,7 +315,12 @@ extension CameraController {
|
|
|
280
315
|
let minZoom = device.minAvailableVideoZoomFactor
|
|
281
316
|
let maxZoom = min(device.maxAvailableVideoZoomFactor, saneMaxZoomFactor)
|
|
282
317
|
|
|
283
|
-
|
|
318
|
+
// Compute UI-level default = 1 * multiplier when not provided
|
|
319
|
+
let multiplier = self.getDisplayZoomMultiplier()
|
|
320
|
+
// if level is nil, it's the initial zoom
|
|
321
|
+
let uiLevel: Float = level ?? (2.0 * multiplier)
|
|
322
|
+
// Map UI/display zoom to native zoom using iOS 18+ multiplier
|
|
323
|
+
let adjustedLevel = multiplier != 1.0 ? (uiLevel / multiplier) : uiLevel
|
|
284
324
|
|
|
285
325
|
guard CGFloat(adjustedLevel) >= minZoom && CGFloat(adjustedLevel) <= maxZoom else {
|
|
286
326
|
print("[CameraPreview] Initial zoom level \(adjustedLevel) out of range (\(minZoom)-\(maxZoom))")
|
|
@@ -376,7 +416,8 @@ extension CameraController {
|
|
|
376
416
|
}
|
|
377
417
|
|
|
378
418
|
// Fast configuration without CATransaction overhead
|
|
379
|
-
|
|
419
|
+
// Use resizeAspect to avoid crop when no aspect ratio is requested; otherwise fill
|
|
420
|
+
previewLayer.videoGravity = (requestedAspectRatio == nil) ? .resizeAspect : .resizeAspectFill
|
|
380
421
|
previewLayer.frame = view.bounds
|
|
381
422
|
|
|
382
423
|
// Insert layer immediately (only if new)
|
|
@@ -464,6 +505,33 @@ extension CameraController {
|
|
|
464
505
|
photoOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
|
|
465
506
|
}
|
|
466
507
|
|
|
508
|
+
private func setDefaultZoomAfterFlip() {
|
|
509
|
+
let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
|
|
510
|
+
guard let device = device else {
|
|
511
|
+
print("[CameraPreview] No device available for default zoom after flip")
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Set zoom to 1.0x in UI terms, accounting for display multiplier
|
|
516
|
+
let multiplier = self.getDisplayZoomMultiplier()
|
|
517
|
+
let targetUIZoom: Float = 1.0 // We want 1.0x in the UI
|
|
518
|
+
let nativeZoom = multiplier != 1.0 ? (targetUIZoom / multiplier) : targetUIZoom
|
|
519
|
+
|
|
520
|
+
let minZoom = device.minAvailableVideoZoomFactor
|
|
521
|
+
let maxZoom = min(device.maxAvailableVideoZoomFactor, saneMaxZoomFactor)
|
|
522
|
+
let clampedZoom = max(minZoom, min(CGFloat(nativeZoom), maxZoom))
|
|
523
|
+
|
|
524
|
+
do {
|
|
525
|
+
try device.lockForConfiguration()
|
|
526
|
+
device.videoZoomFactor = clampedZoom
|
|
527
|
+
device.unlockForConfiguration()
|
|
528
|
+
self.zoomFactor = clampedZoom
|
|
529
|
+
print("[CameraPreview] Set default zoom after flip: UI=\(targetUIZoom)x, native=\(clampedZoom), multiplier=\(multiplier)")
|
|
530
|
+
} catch {
|
|
531
|
+
print("[CameraPreview] Failed to set default zoom after flip: \(error)")
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
467
535
|
func switchCameras() throws {
|
|
468
536
|
guard let currentCameraPosition = currentCameraPosition,
|
|
469
537
|
let captureSession = self.captureSession else {
|
|
@@ -553,17 +621,29 @@ extension CameraController {
|
|
|
553
621
|
|
|
554
622
|
// Update video orientation
|
|
555
623
|
self.updateVideoOrientation()
|
|
624
|
+
|
|
625
|
+
// Set default 1.0 zoom level after camera switch to prevent iOS 18+ zoom jumps
|
|
626
|
+
DispatchQueue.main.async { [weak self] in
|
|
627
|
+
self?.setDefaultZoomAfterFlip()
|
|
628
|
+
}
|
|
556
629
|
}
|
|
557
630
|
|
|
558
|
-
func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
|
|
631
|
+
func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
|
|
559
632
|
print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil")")
|
|
560
633
|
|
|
561
634
|
guard let photoOutput = self.photoOutput else {
|
|
562
|
-
completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
|
|
635
|
+
completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
|
|
563
636
|
return
|
|
564
637
|
}
|
|
565
638
|
|
|
566
639
|
let settings = AVCapturePhotoSettings()
|
|
640
|
+
// Request highest quality photo capture
|
|
641
|
+
if #available(iOS 13.0, *) {
|
|
642
|
+
settings.isHighResolutionPhotoEnabled = true
|
|
643
|
+
}
|
|
644
|
+
if #available(iOS 15.0, *) {
|
|
645
|
+
settings.photoQualityPrioritization = .balanced
|
|
646
|
+
}
|
|
567
647
|
|
|
568
648
|
// Apply the current flash mode to the photo settings
|
|
569
649
|
// Check if the current device supports flash
|
|
@@ -585,14 +665,14 @@ extension CameraController {
|
|
|
585
665
|
}
|
|
586
666
|
}
|
|
587
667
|
|
|
588
|
-
self.photoCaptureCompletionBlock = { (image, error) in
|
|
668
|
+
self.photoCaptureCompletionBlock = { (image, photoData, metadata, error) in
|
|
589
669
|
if let error = error {
|
|
590
|
-
completion(nil, error)
|
|
670
|
+
completion(nil, nil, nil, error)
|
|
591
671
|
return
|
|
592
672
|
}
|
|
593
673
|
|
|
594
674
|
guard let image = image else {
|
|
595
|
-
completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
|
|
675
|
+
completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
|
|
596
676
|
return
|
|
597
677
|
}
|
|
598
678
|
|
|
@@ -646,7 +726,7 @@ extension CameraController {
|
|
|
646
726
|
}
|
|
647
727
|
}
|
|
648
728
|
|
|
649
|
-
completion(finalImage, nil)
|
|
729
|
+
completion(finalImage, photoData, metadata, nil)
|
|
650
730
|
}
|
|
651
731
|
|
|
652
732
|
photoOutput.capturePhoto(with: settings, delegate: self)
|
|
@@ -804,6 +884,7 @@ extension CameraController {
|
|
|
804
884
|
return supportedFlashModesAsStrings
|
|
805
885
|
|
|
806
886
|
}
|
|
887
|
+
|
|
807
888
|
func getHorizontalFov() throws -> Float {
|
|
808
889
|
var currentCamera: AVCaptureDevice?
|
|
809
890
|
switch currentCameraPosition {
|
|
@@ -830,6 +911,7 @@ extension CameraController {
|
|
|
830
911
|
|
|
831
912
|
return adjustedFov
|
|
832
913
|
}
|
|
914
|
+
|
|
833
915
|
func setFlashMode(flashMode: AVCaptureDevice.FlashMode) throws {
|
|
834
916
|
var currentCamera: AVCaptureDevice?
|
|
835
917
|
switch currentCameraPosition {
|
|
@@ -903,6 +985,7 @@ extension CameraController {
|
|
|
903
985
|
|
|
904
986
|
func getZoom() throws -> (min: Float, max: Float, current: Float) {
|
|
905
987
|
var currentCamera: AVCaptureDevice?
|
|
988
|
+
|
|
906
989
|
switch currentCameraPosition {
|
|
907
990
|
case .front:
|
|
908
991
|
currentCamera = self.frontCamera
|
|
@@ -957,9 +1040,9 @@ extension CameraController {
|
|
|
957
1040
|
self.zoomFactor = zoomLevel
|
|
958
1041
|
|
|
959
1042
|
// Trigger autofocus after zoom if requested
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1043
|
+
if autoFocus {
|
|
1044
|
+
self.triggerAutoFocus()
|
|
1045
|
+
}
|
|
963
1046
|
} catch {
|
|
964
1047
|
throw CameraControllerError.invalidOperation
|
|
965
1048
|
}
|
|
@@ -1372,23 +1455,48 @@ extension CameraController: UIGestureRecognizerDelegate {
|
|
|
1372
1455
|
// Remove any existing focus indicator
|
|
1373
1456
|
focusIndicatorView?.removeFromSuperview()
|
|
1374
1457
|
|
|
1375
|
-
// Create a new focus indicator
|
|
1458
|
+
// Create a new focus indicator (iOS Camera style): square with mid-edge ticks
|
|
1376
1459
|
let indicator = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
|
|
1377
1460
|
indicator.center = point
|
|
1378
1461
|
indicator.layer.borderColor = UIColor.yellow.cgColor
|
|
1379
1462
|
indicator.layer.borderWidth = 2.0
|
|
1380
|
-
indicator.layer.cornerRadius =
|
|
1463
|
+
indicator.layer.cornerRadius = 0
|
|
1381
1464
|
indicator.backgroundColor = UIColor.clear
|
|
1382
1465
|
indicator.alpha = 0
|
|
1383
1466
|
indicator.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
|
|
1384
1467
|
|
|
1385
|
-
// Add
|
|
1386
|
-
let
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1468
|
+
// Add 4 tiny mid-edge ticks inside the square
|
|
1469
|
+
let stroke: CGFloat = 2.0
|
|
1470
|
+
let tickLen: CGFloat = 12.0
|
|
1471
|
+
let inset: CGFloat = stroke // ticks should touch the sides
|
|
1472
|
+
// Top tick (perpendicular): vertical inward from top edge
|
|
1473
|
+
let topTick = UIView(frame: CGRect(x: (indicator.bounds.width - stroke)/2,
|
|
1474
|
+
y: inset,
|
|
1475
|
+
width: stroke,
|
|
1476
|
+
height: tickLen))
|
|
1477
|
+
topTick.backgroundColor = .yellow
|
|
1478
|
+
indicator.addSubview(topTick)
|
|
1479
|
+
// Bottom tick (perpendicular): vertical inward from bottom edge
|
|
1480
|
+
let bottomTick = UIView(frame: CGRect(x: (indicator.bounds.width - stroke)/2,
|
|
1481
|
+
y: indicator.bounds.height - inset - tickLen,
|
|
1482
|
+
width: stroke,
|
|
1483
|
+
height: tickLen))
|
|
1484
|
+
bottomTick.backgroundColor = .yellow
|
|
1485
|
+
indicator.addSubview(bottomTick)
|
|
1486
|
+
// Left tick (perpendicular): horizontal inward from left edge
|
|
1487
|
+
let leftTick = UIView(frame: CGRect(x: inset,
|
|
1488
|
+
y: (indicator.bounds.height - stroke)/2,
|
|
1489
|
+
width: tickLen,
|
|
1490
|
+
height: stroke))
|
|
1491
|
+
leftTick.backgroundColor = .yellow
|
|
1492
|
+
indicator.addSubview(leftTick)
|
|
1493
|
+
// Right tick (perpendicular): horizontal inward from right edge
|
|
1494
|
+
let rightTick = UIView(frame: CGRect(x: indicator.bounds.width - inset - tickLen,
|
|
1495
|
+
y: (indicator.bounds.height - stroke)/2,
|
|
1496
|
+
width: tickLen,
|
|
1497
|
+
height: stroke))
|
|
1498
|
+
rightTick.backgroundColor = .yellow
|
|
1499
|
+
indicator.addSubview(rightTick)
|
|
1392
1500
|
|
|
1393
1501
|
view.addSubview(indicator)
|
|
1394
1502
|
focusIndicatorView = indicator
|
|
@@ -1398,7 +1506,7 @@ extension CameraController: UIGestureRecognizerDelegate {
|
|
|
1398
1506
|
indicator.alpha = 1.0
|
|
1399
1507
|
indicator.transform = CGAffineTransform.identity
|
|
1400
1508
|
}) { _ in
|
|
1401
|
-
// Keep the indicator visible
|
|
1509
|
+
// Keep the indicator visible briefly
|
|
1402
1510
|
UIView.animate(withDuration: 0.2, delay: 0.5, options: [], animations: {
|
|
1403
1511
|
indicator.alpha = 0.3
|
|
1404
1512
|
}) { _ in
|
|
@@ -1460,22 +1568,23 @@ extension CameraController: UIGestureRecognizerDelegate {
|
|
|
1460
1568
|
extension CameraController: AVCapturePhotoCaptureDelegate {
|
|
1461
1569
|
public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
|
1462
1570
|
if let error = error {
|
|
1463
|
-
self.photoCaptureCompletionBlock?(nil, error)
|
|
1571
|
+
self.photoCaptureCompletionBlock?(nil, nil, nil, error)
|
|
1464
1572
|
return
|
|
1465
1573
|
}
|
|
1466
1574
|
|
|
1467
1575
|
// Get the photo data using the modern API
|
|
1468
1576
|
guard let imageData = photo.fileDataRepresentation() else {
|
|
1469
|
-
self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
|
|
1577
|
+
self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
|
|
1470
1578
|
return
|
|
1471
1579
|
}
|
|
1472
1580
|
|
|
1473
1581
|
guard let image = UIImage(data: imageData) else {
|
|
1474
|
-
self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
|
|
1582
|
+
self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
|
|
1475
1583
|
return
|
|
1476
1584
|
}
|
|
1477
1585
|
|
|
1478
|
-
|
|
1586
|
+
// Pass through original file data and metadata so callers can preserve EXIF
|
|
1587
|
+
self.photoCaptureCompletionBlock?(image.fixedOrientation(), imageData, photo.metadata, nil)
|
|
1479
1588
|
}
|
|
1480
1589
|
}
|
|
1481
1590
|
|