capacitor-camera-view 2.0.2 → 2.2.0-rc.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.
Files changed (31) hide show
  1. package/README.md +215 -19
  2. package/android/build.gradle +9 -5
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +491 -116
  5. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +181 -31
  6. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
  7. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
  8. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/VideoRecordingQuality.kt +10 -0
  9. package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +114 -5
  10. package/dist/docs.json +281 -8
  11. package/dist/esm/definitions.d.ts +128 -6
  12. package/dist/esm/definitions.js.map +1 -1
  13. package/dist/esm/web.d.ts +26 -4
  14. package/dist/esm/web.js +218 -18
  15. package/dist/esm/web.js.map +1 -1
  16. package/dist/plugin.cjs.js +219 -18
  17. package/dist/plugin.cjs.js.map +1 -1
  18. package/dist/plugin.js +219 -18
  19. package/dist/plugin.js.map +1 -1
  20. package/ios/Sources/CameraViewPlugin/CameraError.swift +125 -2
  21. package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
  22. package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +28 -1
  23. package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
  24. package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +38 -7
  25. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
  26. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +302 -0
  27. package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +246 -166
  28. package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +194 -96
  29. package/ios/Sources/CameraViewPlugin/TempFileManager.swift +215 -0
  30. package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
  31. package/package.json +17 -17
@@ -1,6 +1,8 @@
1
1
  import Foundation
2
2
 
3
- enum CameraError: Error, LocalizedError {
3
+ /// Camera-related errors with detailed error codes and recovery suggestions.
4
+ /// Conforms to CustomNSError for integration with NSError-based APIs.
5
+ enum CameraError: Error, LocalizedError, CustomNSError {
4
6
  case cameraUnavailable
5
7
  case configurationFailed(Error)
6
8
  case frameCaptureError
@@ -12,7 +14,76 @@ enum CameraError: Error, LocalizedError {
12
14
  case unsupportedFlashMode
13
15
  case torchUnavailable
14
16
  case zoomFactorOutOfRange
15
-
17
+ case permissionDenied
18
+ case deviceLocked
19
+ case recordingAlreadyInProgress
20
+ case noRecordingInProgress
21
+ case audioDeviceUnavailable
22
+ case audioInputAdditionFailed
23
+
24
+ // MARK: - CustomNSError
25
+
26
+ static var errorDomain: String {
27
+ return "com.michaelwolz.capacitorcameraview.CameraError"
28
+ }
29
+
30
+ var errorCode: Int {
31
+ switch self {
32
+ case .cameraUnavailable:
33
+ return 1001
34
+ case .configurationFailed:
35
+ return 1002
36
+ case .frameCaptureError:
37
+ return 1003
38
+ case .inputAdditionFailed:
39
+ return 1004
40
+ case .outputAdditionFailed:
41
+ return 1005
42
+ case .photoOutputError:
43
+ return 1006
44
+ case .photoOutputNotConfigured:
45
+ return 1007
46
+ case .sessionNotRunning:
47
+ return 1008
48
+ case .unsupportedFlashMode:
49
+ return 1009
50
+ case .torchUnavailable:
51
+ return 1010
52
+ case .zoomFactorOutOfRange:
53
+ return 1011
54
+ case .permissionDenied:
55
+ return 1012
56
+ case .deviceLocked:
57
+ return 1013
58
+ case .recordingAlreadyInProgress:
59
+ return 1014
60
+ case .noRecordingInProgress:
61
+ return 1015
62
+ case .audioDeviceUnavailable:
63
+ return 1016
64
+ case .audioInputAdditionFailed:
65
+ return 1017
66
+ }
67
+ }
68
+
69
+ var errorUserInfo: [String: Any] {
70
+ var userInfo: [String: Any] = [
71
+ NSLocalizedDescriptionKey: errorDescription ?? "Unknown error"
72
+ ]
73
+
74
+ if let recovery = recoverySuggestion {
75
+ userInfo[NSLocalizedRecoverySuggestionErrorKey] = recovery
76
+ }
77
+
78
+ if case .configurationFailed(let underlyingError) = self {
79
+ userInfo[NSUnderlyingErrorKey] = underlyingError
80
+ }
81
+
82
+ return userInfo
83
+ }
84
+
85
+ // MARK: - LocalizedError
86
+
16
87
  var errorDescription: String? {
17
88
  switch self {
18
89
  case .cameraUnavailable:
@@ -37,6 +108,58 @@ enum CameraError: Error, LocalizedError {
37
108
  return "Torch is not available on this device."
38
109
  case .zoomFactorOutOfRange:
39
110
  return "The requested zoom factor is out of range."
111
+ case .permissionDenied:
112
+ return "Camera access has been denied."
113
+ case .deviceLocked:
114
+ return "The camera device is currently locked by another process."
115
+ case .recordingAlreadyInProgress:
116
+ return "A video recording is already in progress"
117
+ case .noRecordingInProgress:
118
+ return "No video recording is in progress"
119
+ case .audioDeviceUnavailable:
120
+ return "No microphone is available on this device."
121
+ case .audioInputAdditionFailed:
122
+ return "Failed to add the microphone input to the capture session."
123
+ }
124
+ }
125
+
126
+ /// Provides actionable recovery suggestions for each error type.
127
+ var recoverySuggestion: String? {
128
+ switch self {
129
+ case .cameraUnavailable:
130
+ return "Try using a different camera position or check if the device has a camera."
131
+ case .configurationFailed:
132
+ return "Try stopping and restarting the camera session."
133
+ case .frameCaptureError:
134
+ return "Ensure the camera session is running and try again."
135
+ case .inputAdditionFailed:
136
+ return "The camera may be in use by another application. Close other camera apps and try again."
137
+ case .outputAdditionFailed:
138
+ return "Try stopping and restarting the camera session."
139
+ case .photoOutputError:
140
+ return "Try capturing the photo again. If the issue persists, restart the camera session."
141
+ case .photoOutputNotConfigured:
142
+ return "Start the camera session before attempting to capture a photo."
143
+ case .sessionNotRunning:
144
+ return "Call start() to begin the camera session before using this feature."
145
+ case .unsupportedFlashMode:
146
+ return "Use getSupportedFlashModes() to check available flash modes for this camera."
147
+ case .torchUnavailable:
148
+ return "This device or camera position does not support torch functionality."
149
+ case .zoomFactorOutOfRange:
150
+ return "Use getZoom() to check the supported zoom range for this camera."
151
+ case .permissionDenied:
152
+ return "Go to Settings > Privacy > Camera and enable access for this app."
153
+ case .deviceLocked:
154
+ return "Wait for the other process to release the camera or restart the app."
155
+ case .recordingAlreadyInProgress:
156
+ return nil
157
+ case .noRecordingInProgress:
158
+ return nil
159
+ case .audioDeviceUnavailable:
160
+ return "Ensure the device has a microphone and that microphone access has been granted."
161
+ case .audioInputAdditionFailed:
162
+ return "The microphone may be in use by another application. Close other apps and try again."
40
163
  }
41
164
  }
42
165
  }
@@ -0,0 +1,109 @@
1
+ import AVFoundation
2
+ import Foundation
3
+
4
+ // MARK: - Typed Barcode Detection Event
5
+
6
+ /// Represents a detected barcode with its value, type, and position.
7
+ /// This struct is Sendable for safe use across concurrency boundaries.
8
+ public struct BarcodeDetectedEvent: Sendable {
9
+ /// The decoded string value of the barcode.
10
+ public let value: String
11
+
12
+ /// The type of barcode detected (e.g., "org.iso.QRCode").
13
+ public let type: String
14
+
15
+ /// The bounding rectangle of the barcode in screen coordinates.
16
+ public let boundingRect: BoundingRect
17
+
18
+ /// Bounding rectangle coordinates.
19
+ public struct BoundingRect: Sendable {
20
+ public let x: Double
21
+ public let y: Double
22
+ public let width: Double
23
+ public let height: Double
24
+
25
+ public init(x: Double, y: Double, width: Double, height: Double) {
26
+ self.x = x
27
+ self.y = y
28
+ self.width = width
29
+ self.height = height
30
+ }
31
+
32
+ /// Converts to dictionary for Capacitor event emission.
33
+ public func toDictionary() -> [String: Double] {
34
+ return [
35
+ "x": x,
36
+ "y": y,
37
+ "width": width,
38
+ "height": height
39
+ ]
40
+ }
41
+ }
42
+
43
+ public init(value: String, type: String, boundingRect: BoundingRect) {
44
+ self.value = value
45
+ self.type = type
46
+ self.boundingRect = boundingRect
47
+ }
48
+
49
+ /// Converts to dictionary for Capacitor event emission.
50
+ public func toDictionary() -> [String: Any] {
51
+ return [
52
+ "value": value,
53
+ "type": type,
54
+ "boundingRect": boundingRect.toDictionary()
55
+ ]
56
+ }
57
+ }
58
+
59
+ // MARK: - Camera Event Delegate Protocol
60
+
61
+ /// Protocol for receiving typed camera events.
62
+ /// Implement this protocol for type-safe event handling.
63
+ ///
64
+ /// Usage:
65
+ /// ```swift
66
+ /// class MyHandler: CameraEventDelegate {
67
+ /// func cameraDidDetectBarcode(_ event: BarcodeDetectedEvent) {
68
+ /// print("Detected: \(event.value)")
69
+ /// }
70
+ /// }
71
+ /// ```
72
+ public protocol CameraEventDelegate: AnyObject {
73
+ /// Called when a barcode is detected in the camera feed.
74
+ /// - Parameter event: The barcode detection event with all relevant data.
75
+ func cameraDidDetectBarcode(_ event: BarcodeDetectedEvent)
76
+ }
77
+
78
+ // MARK: - Notification Names
79
+
80
+ /// Extension for camera-related notification names.
81
+ /// These maintain backwards compatibility with the NotificationCenter-based event system.
82
+ public extension Notification.Name {
83
+ /// Posted when a barcode is detected.
84
+ /// UserInfo contains: "value", "type", "boundingRect"
85
+ static let cameraViewBarcodeDetected = Notification.Name("barcodeDetected")
86
+ }
87
+
88
+ // MARK: - Event Emitter Helper
89
+
90
+ /// Helper class for emitting camera events through both delegate and NotificationCenter.
91
+ /// This maintains backwards compatibility while enabling the new typed delegate pattern.
92
+ internal final class CameraEventEmitter {
93
+ /// Weak reference to the delegate to avoid retain cycles.
94
+ weak var delegate: CameraEventDelegate?
95
+
96
+ /// Emits a barcode detected event through both channels.
97
+ /// - Parameter event: The barcode detection event to emit.
98
+ func emitBarcodeDetected(_ event: BarcodeDetectedEvent) {
99
+ // Call typed delegate first (preferred path)
100
+ delegate?.cameraDidDetectBarcode(event)
101
+
102
+ // Also post to NotificationCenter for backwards compatibility
103
+ NotificationCenter.default.post(
104
+ name: .cameraViewBarcodeDetected,
105
+ object: nil,
106
+ userInfo: event.toDictionary()
107
+ )
108
+ }
109
+ }
@@ -1,12 +1,29 @@
1
1
  import AVFoundation
2
2
  import Capacitor
3
3
 
4
- public struct CameraSessionConfiguration {
4
+ /// Configuration for a camera capture session.
5
+ /// This struct is Sendable as all properties are value types.
6
+ public struct CameraSessionConfiguration: Sendable {
7
+ /// Specific device ID to use. Takes precedence over position.
5
8
  let deviceId: String?
9
+
10
+ /// Whether to enable barcode detection.
6
11
  let enableBarcodeDetection: Bool
12
+
13
+ /// Optional array of specific barcode types to detect.
14
+ /// If nil, all supported types are detected (for backwards compatibility).
15
+ let barcodeTypes: [AVMetadataObject.ObjectType]?
16
+
17
+ /// Camera position to use (front or back).
7
18
  let position: AVCaptureDevice.Position
19
+
20
+ /// Preferred camera device types in order of preference.
8
21
  let preferredCameraDeviceTypes: [String]?
22
+
23
+ /// Whether to upgrade to triple camera if available (Pro models).
9
24
  let useTripleCameraIfAvailable: Bool
25
+
26
+ /// Initial zoom factor.
10
27
  let zoomFactor: CGFloat?
11
28
  }
12
29
 
@@ -21,9 +38,19 @@ public func sessionConfigFromPluginCall(_ call: CAPPluginCall) -> CameraSessionC
21
38
  let useTripleCameraIfAvailable = call.getBool("useTripleCameraIfAvailable", false)
22
39
  let zoomFactor = call.getDouble("zoomFactor").map { CGFloat($0) }
23
40
 
41
+ // Parse barcode types if provided
42
+ let barcodeTypes: [AVMetadataObject.ObjectType]?
43
+ if let barcodeTypeStrings = call.getArray("barcodeTypes") as? [String] {
44
+ let converted = convertToNativeBarcodeTypes(barcodeTypeStrings)
45
+ barcodeTypes = converted.isEmpty ? nil : converted
46
+ } else {
47
+ barcodeTypes = nil
48
+ }
49
+
24
50
  return CameraSessionConfiguration(
25
51
  deviceId: deviceId,
26
52
  enableBarcodeDetection: enableBarcodeDetection,
53
+ barcodeTypes: barcodeTypes,
27
54
  position: position,
28
55
  preferredCameraDeviceTypes: preferredCameraDeviceTypes,
29
56
  useTripleCameraIfAvailable: useTripleCameraIfAvailable,
@@ -5,39 +5,34 @@ extension CameraViewManager: AVCaptureMetadataOutputObjectsDelegate {
5
5
  /// Set up metadata output for the capture session in case it's not configured yet
6
6
  /// Make sure to call `captureSession.beginConfiguration` before calling this
7
7
  ///
8
+ /// - Parameter barcodeTypes: Optional array of specific barcode types to detect.
9
+ /// If nil, all supported types are detected (backwards compatible).
8
10
  /// - Throws: An error if the output cannot be set.
9
- internal func setupMetadataOutput() throws {
10
- let metadataOutput = AVCaptureMetadataOutput()
11
+ internal func setupMetadataOutput(barcodeTypes: [AVMetadataObject.ObjectType]? = nil) throws {
12
+ let requestedBarcodeTypes = barcodeTypes ?? ALL_SUPPORTED_BARCODE_TYPES
11
13
 
12
- if (captureSession.outputs.contains { $0 is AVCaptureMetadataOutput }) {
13
- // Nothing todo, we already have an output
14
- return
15
- }
14
+ let metadataOutput: AVCaptureMetadataOutput
16
15
 
17
- if !captureSession.canAddOutput(metadataOutput) {
18
- throw CameraError.outputAdditionFailed
19
- }
16
+ if let existingOutput = captureSession.outputs.first(where: { $0 is AVCaptureMetadataOutput }) as? AVCaptureMetadataOutput {
17
+ metadataOutput = existingOutput
18
+ } else {
19
+ let newOutput = AVCaptureMetadataOutput()
20
+ if !captureSession.canAddOutput(newOutput) {
21
+ throw CameraError.outputAdditionFailed
22
+ }
20
23
 
21
- captureSession.addOutput(metadataOutput)
24
+ captureSession.addOutput(newOutput)
25
+ newOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
26
+ metadataOutput = newOutput
27
+ }
22
28
 
23
- metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
29
+ let supportedTypes = Set(metadataOutput.availableMetadataObjectTypes)
30
+ let resolvedTypes = requestedBarcodeTypes.filter { supportedTypes.contains($0) }
24
31
 
25
- // Set all available barcode types
26
- metadataOutput.metadataObjectTypes = [
27
- .qr,
28
- .code128,
29
- .code39,
30
- .code39Mod43,
31
- .code93,
32
- .ean8,
33
- .ean13,
34
- .interleaved2of5,
35
- .itf14,
36
- .pdf417,
37
- .aztec,
38
- .dataMatrix,
39
- .upce
40
- ]
32
+ if metadataOutput.metadataObjectTypes != resolvedTypes {
33
+ // Update the metadata output with the resolved types only if they differ from the current configuration
34
+ metadataOutput.metadataObjectTypes = resolvedTypes
35
+ }
41
36
  }
42
37
 
43
38
  /// Remove the metadata output if in case it is already configured, e.g. because
@@ -80,21 +75,15 @@ extension CameraViewManager: AVCaptureMetadataOutputObjectsDelegate {
80
75
  return
81
76
  }
82
77
 
83
- let boundingRect: [String: Double] = [
84
- "x": Double(transformedMetadataObject.bounds.origin.x),
85
- "y": Double(transformedMetadataObject.bounds.origin.y),
86
- "width": Double(transformedMetadataObject.bounds.width),
87
- "height": Double(transformedMetadataObject.bounds.height)
88
- ]
78
+ let boundingRect = BarcodeDetectedEvent.BoundingRect(
79
+ x: Double(transformedMetadataObject.bounds.origin.x),
80
+ y: Double(transformedMetadataObject.bounds.origin.y),
81
+ width: Double(transformedMetadataObject.bounds.width),
82
+ height: Double(transformedMetadataObject.bounds.height)
83
+ )
89
84
 
90
- NotificationCenter.default.post(
91
- name: Notification.Name("barcodeDetected"),
92
- object: nil,
93
- userInfo: [
94
- "value": barcodeValue,
95
- "type": barcodeType,
96
- "boundingRect": boundingRect
97
- ]
85
+ eventEmitter.emitBarcodeDetected(
86
+ BarcodeDetectedEvent(value: barcodeValue, type: barcodeType, boundingRect: boundingRect)
98
87
  )
99
88
  }
100
89
  }
@@ -26,6 +26,9 @@ extension CameraViewManager: AVCapturePhotoCaptureDelegate {
26
26
 
27
27
  /// Delegate method called when a photo has been captured via `AVCapturePhotoCaptureDelegate`
28
28
  ///
29
+ /// This method handles both the legacy UIImage-based callback and the optimized Data-based
30
+ /// callback to eliminate double JPEG encoding when possible.
31
+ ///
29
32
  /// - Parameters:
30
33
  /// - output: The photo output that captured the photo.
31
34
  /// - photo: The captured photo.
@@ -35,17 +38,45 @@ extension CameraViewManager: AVCapturePhotoCaptureDelegate {
35
38
  didFinishProcessingPhoto photo: AVCapturePhoto,
36
39
  error: Error?
37
40
  ) {
38
- if let error = error {
39
- photoCaptureHandler?(nil, error)
40
- return
41
- }
41
+ // Handle optimized Data-based callback first (avoids double encoding)
42
+ if let dataHandler = photoDataCaptureHandler {
43
+ photoDataCaptureHandler = nil
44
+
45
+ if let error = error {
46
+ dataHandler(nil, error)
47
+ return
48
+ }
49
+
50
+ guard let data = photo.fileDataRepresentation() else {
51
+ dataHandler(nil, CameraError.photoOutputError)
52
+ return
53
+ }
42
54
 
43
- guard let data = photo.fileDataRepresentation(), let image = UIImage(data: data) else {
44
- photoCaptureHandler?(nil, CameraError.photoOutputError)
55
+ dataHandler(data, nil)
45
56
  return
46
57
  }
47
58
 
48
- photoCaptureHandler?(image, nil)
59
+ // Handle legacy UIImage-based callback
60
+ if let imageHandler = photoCaptureHandler {
61
+ photoCaptureHandler = nil
62
+
63
+ if let error = error {
64
+ imageHandler(nil, error)
65
+ return
66
+ }
67
+
68
+ guard let data = photo.fileDataRepresentation() else {
69
+ imageHandler(nil, CameraError.photoOutputError)
70
+ return
71
+ }
72
+
73
+ guard let image = UIImage(data: data) else {
74
+ imageHandler(nil, CameraError.photoOutputError)
75
+ return
76
+ }
77
+
78
+ imageHandler(image, nil)
79
+ }
49
80
  }
50
81
 
51
82
  }
@@ -33,7 +33,8 @@ extension CameraViewManager: AVCaptureVideoDataOutputSampleBufferDelegate {
33
33
  captureSession.addOutput(avVideoDataOutput)
34
34
  }
35
35
 
36
- /// Capture a snapshot from the camera feed
36
+ /// Capture a snapshot from the camera feed using the shared Metal-backed CIContext.
37
+ /// Using a shared CIContext eliminates the ~80% CPU overhead of creating one per frame.
37
38
  public func captureOutput(
38
39
  _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer,
39
40
  from connection: AVCaptureConnection
@@ -52,8 +53,8 @@ extension CameraViewManager: AVCaptureVideoDataOutputSampleBufferDelegate {
52
53
  }
53
54
 
54
55
  let ciImage = CIImage(cvPixelBuffer: imageBuffer)
55
- let context = CIContext()
56
- guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
56
+ // Use the shared Metal-backed CIContext for efficient rendering
57
+ guard let cgImage = CameraViewManager.sharedCIContext.createCGImage(ciImage, from: ciImage.extent) else {
57
58
  completionHandler(nil, CameraError.frameCaptureError)
58
59
  return
59
60
  }