@capgo/camera-preview 8.3.8 → 8.4.1

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.
@@ -1,3 +1,4 @@
1
+ // swiftlint:disable file_length cyclomatic_complexity identifier_name
1
2
  import AVFoundation
2
3
  import UIKit
3
4
  import CoreLocation
@@ -102,6 +103,7 @@ class CameraController: NSObject {
102
103
  var frontCameraInput: AVCaptureDeviceInput?
103
104
 
104
105
  var dataOutput: AVCaptureVideoDataOutput?
106
+ var metadataOutput: AVCaptureMetadataOutput?
105
107
  var photoOutput: AVCapturePhotoOutput?
106
108
 
107
109
  var rearCamera: AVCaptureDevice?
@@ -119,6 +121,10 @@ class CameraController: NSObject {
119
121
  var photoCaptureCompletionBlock: ((UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void)?
120
122
 
121
123
  var sampleBufferCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
124
+ var barcodeScannerCallback: (([[String: Any]]) -> Void)?
125
+ private let barcodeMetadataQueue = DispatchQueue(label: "com.camera.barcodeMetadataQueue", qos: .userInitiated)
126
+ private var barcodeDetectionInterval: TimeInterval = 0.5
127
+ private var lastBarcodeDetectionAt: TimeInterval = 0
122
128
 
123
129
  // Add callback for detecting when first frame is ready
124
130
  var firstFrameReadyCallback: (() -> Void)?
@@ -1561,6 +1567,137 @@ extension CameraController {
1561
1567
  self.sampleBufferCaptureCompletionBlock = completion
1562
1568
  }
1563
1569
 
1570
+ func startBarcodeScanner(formats: [String], detectionIntervalMs: Int, callback: @escaping ([[String: Any]]) -> Void) throws {
1571
+ guard let captureSession = captureSession,
1572
+ captureSession.isRunning else {
1573
+ throw CameraControllerError.captureSessionIsMissing
1574
+ }
1575
+
1576
+ stopBarcodeScanner()
1577
+
1578
+ let output = AVCaptureMetadataOutput()
1579
+ guard captureSession.canAddOutput(output) else {
1580
+ throw CameraControllerError.invalidOperation
1581
+ }
1582
+
1583
+ self.barcodeScannerCallback = callback
1584
+ self.barcodeDetectionInterval = TimeInterval(max(100, detectionIntervalMs)) / 1000.0
1585
+ self.lastBarcodeDetectionAt = 0
1586
+
1587
+ captureSession.beginConfiguration()
1588
+ captureSession.addOutput(output)
1589
+ captureSession.commitConfiguration()
1590
+
1591
+ let requestedTypes = metadataObjectTypes(for: formats)
1592
+ let availableTypes = output.availableMetadataObjectTypes
1593
+ let enabledTypes = requestedTypes.isEmpty ? availableTypes : requestedTypes.filter { availableTypes.contains($0) }
1594
+
1595
+ guard !enabledTypes.isEmpty else {
1596
+ captureSession.beginConfiguration()
1597
+ captureSession.removeOutput(output)
1598
+ captureSession.commitConfiguration()
1599
+ self.barcodeScannerCallback = nil
1600
+ throw CameraControllerError.invalidOperation
1601
+ }
1602
+
1603
+ output.metadataObjectTypes = enabledTypes
1604
+ output.setMetadataObjectsDelegate(self, queue: barcodeMetadataQueue)
1605
+ self.metadataOutput = output
1606
+ }
1607
+
1608
+ func stopBarcodeScanner() {
1609
+ metadataOutput?.setMetadataObjectsDelegate(nil, queue: nil)
1610
+ if let output = metadataOutput,
1611
+ let captureSession = captureSession,
1612
+ captureSession.outputs.contains(output) {
1613
+ captureSession.beginConfiguration()
1614
+ captureSession.removeOutput(output)
1615
+ captureSession.commitConfiguration()
1616
+ }
1617
+ metadataOutput = nil
1618
+ barcodeScannerCallback = nil
1619
+ lastBarcodeDetectionAt = 0
1620
+ }
1621
+
1622
+ private func metadataObjectTypes(for formats: [String]) -> [AVMetadataObject.ObjectType] {
1623
+ guard !formats.isEmpty else { return [] }
1624
+
1625
+ var result: [AVMetadataObject.ObjectType] = []
1626
+ for format in formats {
1627
+ let mappedTypes = metadataObjectTypes(for: format)
1628
+ for type in mappedTypes where !result.contains(type) {
1629
+ result.append(type)
1630
+ }
1631
+ }
1632
+ return result
1633
+ }
1634
+
1635
+ private func metadataObjectTypes(for format: String) -> [AVMetadataObject.ObjectType] {
1636
+ switch format {
1637
+ case "aztec":
1638
+ return [.aztec]
1639
+ case "code_39":
1640
+ return [.code39, .code39Mod43]
1641
+ case "code_93":
1642
+ return [.code93]
1643
+ case "code_128":
1644
+ return [.code128]
1645
+ case "data_matrix":
1646
+ return [.dataMatrix]
1647
+ case "ean_8":
1648
+ return [.ean8]
1649
+ case "ean_13", "upc_a":
1650
+ return [.ean13]
1651
+ case "itf":
1652
+ return [.interleaved2of5, .itf14]
1653
+ case "pdf417":
1654
+ return [.pdf417]
1655
+ case "qr_code":
1656
+ return [.qr]
1657
+ case "upc_e":
1658
+ return [.upce]
1659
+ case "codabar":
1660
+ if #available(iOS 15.4, *) {
1661
+ return [.codabar]
1662
+ }
1663
+ return []
1664
+ default:
1665
+ return []
1666
+ }
1667
+ }
1668
+
1669
+ private func barcodeFormat(from type: AVMetadataObject.ObjectType) -> String {
1670
+ switch type {
1671
+ case .aztec:
1672
+ return "aztec"
1673
+ case .code39, .code39Mod43:
1674
+ return "code_39"
1675
+ case .code93:
1676
+ return "code_93"
1677
+ case .code128:
1678
+ return "code_128"
1679
+ case .dataMatrix:
1680
+ return "data_matrix"
1681
+ case .ean8:
1682
+ return "ean_8"
1683
+ case .ean13:
1684
+ return "ean_13"
1685
+ case .interleaved2of5, .itf14:
1686
+ return "itf"
1687
+ case .pdf417:
1688
+ return "pdf417"
1689
+ case .qr:
1690
+ return "qr_code"
1691
+ case .upce:
1692
+ return "upc_e"
1693
+ default:
1694
+ if #available(iOS 15.4, *), type == .codabar {
1695
+ return "codabar"
1696
+ }
1697
+ return "unknown"
1698
+ }
1699
+ }
1700
+
1564
1701
  func getSupportedFlashModes() throws -> [String] {
1565
1702
  var currentCamera: AVCaptureDevice?
1566
1703
  switch currentCameraPosition {
@@ -2040,6 +2177,7 @@ extension CameraController {
2040
2177
  }
2041
2178
 
2042
2179
  func cleanup() {
2180
+ stopBarcodeScanner()
2043
2181
  if let captureSession = self.captureSession {
2044
2182
  captureSession.stopRunning()
2045
2183
  captureSession.inputs.forEach { captureSession.removeInput($0) }
@@ -2065,6 +2203,7 @@ extension CameraController {
2065
2203
  self.allDiscoveredDevices = []
2066
2204
 
2067
2205
  self.dataOutput = nil
2206
+ self.metadataOutput = nil
2068
2207
  self.photoOutput = nil
2069
2208
  self.fileVideoOutput = nil
2070
2209
 
@@ -2522,7 +2661,7 @@ extension CameraController: AVCapturePhotoCaptureDelegate {
2522
2661
  }
2523
2662
  }
2524
2663
 
2525
- extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate {
2664
+ extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureMetadataOutputObjectsDelegate {
2526
2665
  func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
2527
2666
  // Check if we're waiting for the first frame
2528
2667
  if !hasReceivedFirstFrame, let firstFrameCallback = firstFrameReadyCallback {
@@ -2573,6 +2712,33 @@ extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate {
2573
2712
 
2574
2713
  sampleBufferCaptureCompletionBlock = nil
2575
2714
  }
2715
+
2716
+ func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
2717
+ guard let callback = barcodeScannerCallback else { return }
2718
+
2719
+ let now = Date().timeIntervalSince1970
2720
+ guard now - lastBarcodeDetectionAt >= barcodeDetectionInterval else { return }
2721
+
2722
+ let barcodes: [[String: Any]] = metadataObjects.compactMap { object in
2723
+ guard let readableObject = object as? AVMetadataMachineReadableCodeObject,
2724
+ let value = readableObject.stringValue,
2725
+ !value.isEmpty else {
2726
+ return nil
2727
+ }
2728
+
2729
+ return [
2730
+ "value": value,
2731
+ "format": barcodeFormat(from: readableObject.type)
2732
+ ]
2733
+ }
2734
+
2735
+ guard !barcodes.isEmpty else { return }
2736
+ lastBarcodeDetectionAt = now
2737
+
2738
+ DispatchQueue.main.async {
2739
+ callback(barcodes)
2740
+ }
2741
+ }
2576
2742
  }
2577
2743
 
2578
2744
  enum CameraControllerError: Swift.Error {
@@ -1,3 +1,4 @@
1
+ // swiftlint:disable file_length type_body_length cyclomatic_complexity function_body_length
1
2
  import Foundation
2
3
  import AVFoundation
3
4
  import Photos
@@ -34,7 +35,7 @@ extension UIWindow {
34
35
  */
35
36
  @objc(CameraPreview)
36
37
  public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
37
- private let pluginVersion: String = "8.3.8"
38
+ private let pluginVersion: String = "8.4.1"
38
39
  public let identifier = "CameraPreviewPlugin"
39
40
  public let jsName = "CameraPreview"
40
41
  public let pluginMethods: [CAPPluginMethod] = [
@@ -43,6 +44,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
43
44
  CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
44
45
  CAPPluginMethod(name: "capture", returnType: CAPPluginReturnPromise),
45
46
  CAPPluginMethod(name: "captureSample", returnType: CAPPluginReturnPromise),
47
+ CAPPluginMethod(name: "startBarcodeScanner", returnType: CAPPluginReturnPromise),
48
+ CAPPluginMethod(name: "stopBarcodeScanner", returnType: CAPPluginReturnPromise),
46
49
  CAPPluginMethod(name: "getSupportedFlashModes", returnType: CAPPluginReturnPromise),
47
50
  CAPPluginMethod(name: "getHorizontalFov", returnType: CAPPluginReturnPromise),
48
51
  CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
@@ -108,6 +111,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
108
111
  private var permissionCallID: String?
109
112
  private var waitingForLocation: Bool = false
110
113
  private var isPresentingPermissionAlert: Bool = false
114
+ private var pendingStartBarcodeScannerOptions: (formats: [String], detectionInterval: Int)?
115
+ private var hasResolvedStartCall: Bool = false
111
116
 
112
117
  // Store original webview colors to restore them when stopping
113
118
  private var originalWebViewBackgroundColor: UIColor?
@@ -709,6 +714,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
709
714
  }
710
715
 
711
716
  self.isInitializing = true
717
+ self.hasResolvedStartCall = false
712
718
 
713
719
  self.cameraPosition = call.getString("position") ?? "rear"
714
720
  let deviceId = call.getString("deviceId")
@@ -759,6 +765,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
759
765
 
760
766
  // Default to high if not provided
761
767
  let videoQuality = call.getString("videoQuality") ?? "high"
768
+ self.pendingStartBarcodeScannerOptions = self.barcodeScannerStartOptions(from: call)
762
769
 
763
770
  let initialZoomLevel = call.getFloat("initialZoomLevel")
764
771
 
@@ -768,6 +775,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
768
775
  let hasHeight = call.getInt("height") != nil
769
776
 
770
777
  if hasAspectRatio && (hasWidth || hasHeight) {
778
+ self.pendingStartBarcodeScannerOptions = nil
771
779
  call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
772
780
  return
773
781
  }
@@ -785,6 +793,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
785
793
  print(error)
786
794
  DispatchQueue.main.async {
787
795
  self.isInitializing = false
796
+ self.pendingStartBarcodeScannerOptions = nil
788
797
  call.reject(error.localizedDescription)
789
798
  }
790
799
  return
@@ -807,6 +816,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
807
816
  let handleDenied: (AVAuthorizationStatus) -> Void = { _ in
808
817
  DispatchQueue.main.async {
809
818
  self.isInitializing = false
819
+ self.pendingStartBarcodeScannerOptions = nil
810
820
  call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied")
811
821
  }
812
822
  }
@@ -880,7 +890,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
880
890
  returnedObject["height"] = self.previewView.frame.height as any JSValue
881
891
  returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
882
892
  returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
883
- call.resolve(returnedObject)
893
+ self.resolveStartCall(call, returnedObject: returnedObject)
884
894
  }
885
895
  }
886
896
 
@@ -892,11 +902,46 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
892
902
  returnedObject["height"] = self.previewView.frame.height as any JSValue
893
903
  returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
894
904
  returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
895
- call.resolve(returnedObject)
905
+ self.resolveStartCall(call, returnedObject: returnedObject)
896
906
  }
897
907
  }
898
908
  }
899
909
 
910
+ private func barcodeScannerStartOptions(from call: CAPPluginCall) -> (formats: [String], detectionInterval: Int)? {
911
+ if call.getBool("barcodeScanner") == true {
912
+ return (formats: [], detectionInterval: 500)
913
+ }
914
+
915
+ guard let options = call.getObject("barcodeScanner") else {
916
+ return nil
917
+ }
918
+
919
+ let formats = options["formats"] as? [String] ?? []
920
+ let detectionInterval = (options["detectionInterval"] as? Int) ?? (options["detectionInterval"] as? NSNumber)?.intValue ?? 500
921
+ return (formats: formats, detectionInterval: detectionInterval)
922
+ }
923
+
924
+ private func resolveStartCall(_ call: CAPPluginCall, returnedObject: JSObject) {
925
+ guard !hasResolvedStartCall else { return }
926
+ hasResolvedStartCall = true
927
+ cameraController.firstFrameReadyCallback = nil
928
+
929
+ if let options = pendingStartBarcodeScannerOptions {
930
+ do {
931
+ try self.cameraController.startBarcodeScanner(formats: options.formats, detectionIntervalMs: options.detectionInterval) { [weak self] barcodes in
932
+ self?.notifyListeners("barcodeScanned", data: ["barcodes": barcodes])
933
+ }
934
+ } catch {
935
+ self.pendingStartBarcodeScannerOptions = nil
936
+ call.reject("Failed to start barcode scanner: \(error.localizedDescription)")
937
+ return
938
+ }
939
+ self.pendingStartBarcodeScannerOptions = nil
940
+ }
941
+
942
+ call.resolve(returnedObject)
943
+ }
944
+
900
945
  @objc func flip(_ call: CAPPluginCall) {
901
946
  guard isInitialized else {
902
947
  call.reject("Camera not initialized")
@@ -1478,6 +1523,30 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1478
1523
  }
1479
1524
  }
1480
1525
 
1526
+ @objc func startBarcodeScanner(_ call: CAPPluginCall) {
1527
+ guard self.isInitialized else {
1528
+ call.reject("Camera is not running")
1529
+ return
1530
+ }
1531
+
1532
+ let formats = call.getArray("formats") as? [String] ?? []
1533
+ let detectionInterval = call.getInt("detectionInterval") ?? 500
1534
+
1535
+ do {
1536
+ try self.cameraController.startBarcodeScanner(formats: formats, detectionIntervalMs: detectionInterval) { [weak self] barcodes in
1537
+ self?.notifyListeners("barcodeScanned", data: ["barcodes": barcodes])
1538
+ }
1539
+ call.resolve()
1540
+ } catch {
1541
+ call.reject("Failed to start barcode scanner: \(error.localizedDescription)")
1542
+ }
1543
+ }
1544
+
1545
+ @objc func stopBarcodeScanner(_ call: CAPPluginCall) {
1546
+ self.cameraController.stopBarcodeScanner()
1547
+ call.resolve()
1548
+ }
1549
+
1481
1550
  @objc func getSupportedFlashModes(_ call: CAPPluginCall) {
1482
1551
  do {
1483
1552
  let supportedFlashModes = try self.cameraController.getSupportedFlashModes()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.3.8",
3
+ "version": "8.4.1",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
@@ -25,7 +25,10 @@
25
25
  "video",
26
26
  "photo",
27
27
  "image",
28
- "capture"
28
+ "capture",
29
+ "barcode",
30
+ "qr",
31
+ "scanner"
29
32
  ],
30
33
  "main": "dist/esm/index.js",
31
34
  "types": "dist/esm/index.d.ts",
@@ -51,7 +54,8 @@
51
54
  "clean": "rimraf ./dist",
52
55
  "maestro:smoke": "bunx maestro test .maestro/camera-preview-smoke.yaml",
53
56
  "watch": "tsc --watch",
54
- "prepublishOnly": "bun run build"
57
+ "prepublishOnly": "bun run build",
58
+ "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
55
59
  },
56
60
  "devDependencies": {
57
61
  "@capacitor/android": "^8.0.0",