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.
- package/README.md +215 -19
- package/android/build.gradle +9 -5
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +491 -116
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +181 -31
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/VideoRecordingQuality.kt +10 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +114 -5
- package/dist/docs.json +281 -8
- package/dist/esm/definitions.d.ts +128 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +26 -4
- package/dist/esm/web.js +218 -18
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +219 -18
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +219 -18
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +125 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +28 -1
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +38 -7
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +302 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +246 -166
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +194 -96
- package/ios/Sources/CameraViewPlugin/TempFileManager.swift +215 -0
- package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
- package/package.json +17 -17
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
11
|
+
internal func setupMetadataOutput(barcodeTypes: [AVMetadataObject.ObjectType]? = nil) throws {
|
|
12
|
+
let requestedBarcodeTypes = barcodeTypes ?? ALL_SUPPORTED_BARCODE_TYPES
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
// Nothing todo, we already have an output
|
|
14
|
-
return
|
|
15
|
-
}
|
|
14
|
+
let metadataOutput: AVCaptureMetadataOutput
|
|
16
15
|
|
|
17
|
-
if
|
|
18
|
-
|
|
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
|
-
|
|
24
|
+
captureSession.addOutput(newOutput)
|
|
25
|
+
newOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
26
|
+
metadataOutput = newOutput
|
|
27
|
+
}
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
let supportedTypes = Set(metadataOutput.availableMetadataObjectTypes)
|
|
30
|
+
let resolvedTypes = requestedBarcodeTypes.filter { supportedTypes.contains($0) }
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
photoCaptureHandler?(nil, CameraError.photoOutputError)
|
|
55
|
+
dataHandler(data, nil)
|
|
45
56
|
return
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
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
|
-
|
|
56
|
-
guard let cgImage =
|
|
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
|
}
|