@momo-kits/camerakit 0.161.2-beta.5 → 0.161.2-beta.8
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.
|
@@ -84,10 +84,10 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
|
|
84
84
|
- (void)prepareView
|
|
85
85
|
{
|
|
86
86
|
_view = [[CKCameraView alloc] init];
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// just need to pass something, it won't really be used on fabric, but it's used to create events (it won't impact sending them)
|
|
89
89
|
_view.reactTag = @-1;
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
__weak __typeof__(self) weakSelf = self;
|
|
92
92
|
|
|
93
93
|
[_view setOnReadCode:^(NSDictionary* event) {
|
|
@@ -137,7 +137,7 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
|
|
137
137
|
std::dynamic_pointer_cast<const facebook::react::CKCameraEventEmitter>(strongSelf->_eventEmitter)->onMRZ({.docMRZ = docMRZ});
|
|
138
138
|
}
|
|
139
139
|
}];
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
self.contentView = _view;
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -232,7 +232,7 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
|
|
232
232
|
}
|
|
233
233
|
id zoomMode = CKConvertFollyDynamicToId(newProps.zoomMode);
|
|
234
234
|
if (zoomMode != nil) {
|
|
235
|
-
_view.zoomMode = [
|
|
235
|
+
_view.zoomMode = [zoomMode isEqualToString:@"on"] ? CKZoomModeOn : CKZoomModeOff;
|
|
236
236
|
[changedProps addObject:@"zoomMode"];
|
|
237
237
|
}
|
|
238
238
|
id zoom = CKConvertFollyDynamicToId(newProps.zoom);
|
|
@@ -251,8 +251,8 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
|
|
251
251
|
_view.barcodeFrameSize = @{@"width": @(barcodeWidth), @"height": @(barcodeHeight)};
|
|
252
252
|
[changedProps addObject:@"barcodeFrameSize"];
|
|
253
253
|
}
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
|
|
255
|
+
|
|
256
256
|
[super updateProps:props oldProps:oldProps];
|
|
257
257
|
[_view didSetProps:changedProps];
|
|
258
258
|
}
|
|
@@ -260,6 +260,12 @@ static id CKConvertFollyDynamicToId(const folly::dynamic &dyn)
|
|
|
260
260
|
- (void)prepareForRecycle
|
|
261
261
|
{
|
|
262
262
|
[super prepareForRecycle];
|
|
263
|
+
|
|
264
|
+
if (_view != nil) {
|
|
265
|
+
[_view removeFromSuperview];
|
|
266
|
+
_view = nil;
|
|
267
|
+
}
|
|
268
|
+
|
|
263
269
|
[self prepareView];
|
|
264
270
|
}
|
|
265
271
|
|
|
@@ -18,12 +18,8 @@ import Foundation
|
|
|
18
18
|
return CameraView()
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
@objc public static func capture(camera: CameraView,
|
|
22
|
-
|
|
23
|
-
resolve: @escaping RCTPromiseResolveBlock,
|
|
24
|
-
reject: @escaping RCTPromiseRejectBlock) {
|
|
25
|
-
camera.capture(options as! [String: Any], onSuccess: { resolve($0) },
|
|
26
|
-
onError: { reject("capture_error", $0, nil) })
|
|
21
|
+
@objc public static func capture(camera: CameraView, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
22
|
+
camera.capture((options as? [String: Any]) ?? [:], onSuccess: { resolve($0) }, onError: { reject("capture_error", $0, nil) })
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
@objc public static func checkDeviceCameraAuthorizationStatus(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
@@ -361,8 +361,8 @@ public class CameraView: UIView {
|
|
|
361
361
|
let features = detector?.features(in: ciImage)
|
|
362
362
|
|
|
363
363
|
if let firstFeature = features?.first as? CIQRCodeFeature {
|
|
364
|
-
if
|
|
365
|
-
self.onBarcodeRead(barcode:
|
|
364
|
+
if let messageString = firstFeature.messageString {
|
|
365
|
+
self.onBarcodeRead(barcode: messageString, codeFormat: CodeFormat.qr)
|
|
366
366
|
}
|
|
367
367
|
return
|
|
368
368
|
}
|
|
@@ -22,19 +22,19 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
22
22
|
|
|
23
23
|
private let cameraPreview = RealPreviewView(frame: .zero)
|
|
24
24
|
private let session = AVCaptureSession()
|
|
25
|
-
|
|
26
|
-
private let sessionQueue =
|
|
27
|
-
|
|
25
|
+
private static let sharedSessionQueue = DispatchQueue(label: "com.tesla.react-native-camera-kit.session")
|
|
26
|
+
private let sessionQueue = RealCamera.sharedSessionQueue
|
|
27
|
+
|
|
28
28
|
// utilities
|
|
29
29
|
private var setupResult: SetupResult = .notStarted
|
|
30
30
|
private var configurationDepth: Int = 0 // Tracks nested beginConfiguration/commitConfiguration calls
|
|
31
31
|
private var pendingStop: Bool = false // Queues stop request during configuration
|
|
32
32
|
private var backgroundRecordingId: UIBackgroundTaskIdentifier = .invalid
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
private var videoDeviceInput: AVCaptureDeviceInput?
|
|
35
35
|
private let photoOutput = AVCapturePhotoOutput()
|
|
36
36
|
private let metadataOutput = AVCaptureMetadataOutput()
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
private var resizeMode: ResizeMode = .cover
|
|
39
39
|
private var flashMode: FlashMode = .auto
|
|
40
40
|
private var torchMode: TorchMode = .off
|
|
@@ -49,44 +49,40 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
49
49
|
private var lastOnZoom: Double?
|
|
50
50
|
private var zoom: Double?
|
|
51
51
|
private var maxZoom: Double?
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
private var deviceOrientation = UIDeviceOrientation.unknown
|
|
54
54
|
private var motionManager: CMMotionManager?
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// KVO observation
|
|
57
57
|
private var adjustingFocusObservation: NSKeyValueObservation?
|
|
58
58
|
|
|
59
|
+
private var notificationObservers: [NSObjectProtocol] = []
|
|
60
|
+
|
|
59
61
|
// Keep delegate objects in memory to avoid collecting them before photo capturing finishes
|
|
60
62
|
private var inProgressPhotoCaptureDelegates = [Int64: PhotoCaptureDelegate]()
|
|
61
|
-
|
|
63
|
+
|
|
62
64
|
private var onTextRead: ((_ text: String) -> Void)?
|
|
63
65
|
private let videoDataOutput = AVCaptureVideoDataOutput()
|
|
64
66
|
private var textRequest: VNRecognizeTextRequest?
|
|
65
67
|
private var textDetectionEnabled = false
|
|
66
68
|
private var lastTextProcess = Date.distantPast
|
|
67
69
|
private let textThrottle: TimeInterval = 0.35 // seconds
|
|
68
|
-
|
|
70
|
+
|
|
69
71
|
private var zoomStartedAt: Double = 1.0
|
|
70
|
-
|
|
72
|
+
|
|
71
73
|
// MARK: - Lifecycle
|
|
72
74
|
|
|
73
75
|
func cameraRemovedFromSuperview() {
|
|
74
|
-
sessionQueue.async {
|
|
75
|
-
|
|
76
|
-
// Only stop if session is running
|
|
77
|
-
guard self.session.isRunning else {
|
|
78
|
-
self.removeObservers()
|
|
79
|
-
return
|
|
80
|
-
}
|
|
76
|
+
sessionQueue.async { [weak self] in
|
|
77
|
+
guard let self, self.setupResult == .success else { return }
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
self.pendingStop = true
|
|
86
|
-
return
|
|
87
|
-
}
|
|
79
|
+
// Stop now if idle/safe, or defer the stop until the open configuration
|
|
80
|
+
// transaction commits (stopSessionIfNeeded sets pendingStop in that case).
|
|
81
|
+
self.stopSessionIfNeeded()
|
|
88
82
|
|
|
89
|
-
|
|
83
|
+
// If the stop was deferred, handlePendingStopIfNeeded() will remove the
|
|
84
|
+
// observers once the transaction commits; otherwise remove them now.
|
|
85
|
+
if !self.pendingStop {
|
|
90
86
|
self.removeObservers()
|
|
91
87
|
}
|
|
92
88
|
}
|
|
@@ -99,99 +95,120 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
99
95
|
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
|
100
96
|
#endif
|
|
101
97
|
}
|
|
102
|
-
|
|
98
|
+
|
|
103
99
|
deinit {
|
|
104
100
|
removeObservers()
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// MARK: - Public
|
|
108
101
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
102
|
+
// Ensure the capture hardware is released even if cameraRemovedFromSuperview()
|
|
103
|
+
// was never delivered (e.g. a forced React surface cleanup during rapid
|
|
104
|
+
// mount/unmount). Capture only the session object so the block does not
|
|
105
|
+
// resurrect `self`.
|
|
106
|
+
let session = self.session
|
|
112
107
|
sessionQueue.async {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
self.setupAdditionalOutputs(supportedBarcodeType: supportedBarcodeType)
|
|
108
|
+
if session.isRunning {
|
|
109
|
+
session.stopRunning()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
119
113
|
|
|
120
|
-
|
|
114
|
+
// MARK: - Public
|
|
121
115
|
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
|
|
117
|
+
sessionQueue.async { [weak self] in
|
|
118
|
+
guard let self else { return }
|
|
119
|
+
|
|
120
|
+
// Idempotency guard: a RealCamera configures its session exactly once.
|
|
121
|
+
// A fresh instance is created on every mount / Fabric recycle, so this
|
|
122
|
+
// never blocks a legitimate restart — resuming after stopCamera() is
|
|
123
|
+
// handled by startCamera() -> startSessionIfNeeded().
|
|
124
|
+
guard self.setupResult == .notStarted else { return }
|
|
125
|
+
|
|
126
|
+
// Configure the WHOLE session (inputs, preset, outputs) inside a single
|
|
127
|
+
// begin/commit transaction on the session queue. The preset is set here
|
|
128
|
+
// and never on the main queue, so the startRunning() below can no longer
|
|
129
|
+
// observe the session mid-configuration from another thread.
|
|
130
|
+
//
|
|
131
|
+
// This is the root-cause fix for:
|
|
132
|
+
// "[AVCaptureSession startRunning] startRunning may not be called between
|
|
133
|
+
// calls to beginConfiguration and commitConfiguration".
|
|
134
|
+
self.setupResult = self.configureSession(cameraType: cameraType,
|
|
135
|
+
supportedBarcodeType: supportedBarcodeType)
|
|
136
|
+
|
|
137
|
+
guard self.setupResult == .success else { return }
|
|
138
|
+
|
|
139
|
+
// Order matches the previous behaviour: start, then observe, then torch.
|
|
140
|
+
self.startSessionIfNeeded()
|
|
141
|
+
self.addObservers()
|
|
142
|
+
self.update(torchMode: self.torchMode)
|
|
124
143
|
|
|
125
|
-
|
|
144
|
+
// Only the preview LAYER is touched on the main queue. Connecting an
|
|
145
|
+
// AVCaptureVideoPreviewLayer to a session from the main thread is
|
|
146
|
+
// sanctioned by AVFoundation (see Apple's AVCam) and does NOT open a
|
|
147
|
+
// session configuration transaction, so it cannot race startRunning().
|
|
148
|
+
DispatchQueue.main.async { [weak self] in
|
|
149
|
+
guard let self else { return }
|
|
126
150
|
self.cameraPreview.session = self.session
|
|
127
151
|
self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill
|
|
128
|
-
self.session.sessionPreset = .photo
|
|
129
152
|
self.setVideoOrientationToInterfaceOrientation()
|
|
130
153
|
}
|
|
131
154
|
}
|
|
132
|
-
|
|
133
|
-
DispatchQueue.global(qos: .utility).async {
|
|
134
|
-
self.initializeMotionManager()
|
|
135
|
-
}
|
|
136
155
|
|
|
156
|
+
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
157
|
+
self?.initializeMotionManager()
|
|
158
|
+
}
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
// MARK: - Private optimization methods
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
|
|
163
|
+
/// Configures the video input, session preset and every output in ONE atomic
|
|
164
|
+
/// transaction. Must be called on `sessionQueue`. Order is significant:
|
|
165
|
+
/// beginConfiguration -> addInput -> set preset -> addOutputs -> commitConfiguration.
|
|
166
|
+
private func configureSession(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) -> SetupResult {
|
|
167
|
+
assertOnSessionQueue()
|
|
142
168
|
guard let videoDevice = self.getBestDevice(for: cameraType),
|
|
143
169
|
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
|
|
144
170
|
return .sessionConfigurationFailed
|
|
145
171
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
private func setupAdditionalOutputs(supportedBarcodeType: [CodeFormat]) {
|
|
165
|
-
configurationDepth += 1
|
|
166
|
-
session.beginConfiguration()
|
|
167
|
-
defer {
|
|
168
|
-
session.commitConfiguration()
|
|
169
|
-
configurationDepth -= 1
|
|
170
|
-
handlePendingStopIfNeeded()
|
|
172
|
+
|
|
173
|
+
beginConfiguration()
|
|
174
|
+
defer { commitConfiguration() }
|
|
175
|
+
|
|
176
|
+
// 1. Video input
|
|
177
|
+
guard session.canAddInput(videoDeviceInput) else {
|
|
178
|
+
return .sessionConfigurationFailed
|
|
179
|
+
}
|
|
180
|
+
session.addInput(videoDeviceInput)
|
|
181
|
+
self.videoDeviceInput = videoDeviceInput
|
|
182
|
+
self.resetZoom(forDevice: videoDevice)
|
|
183
|
+
|
|
184
|
+
// 2. Preset — set inside the transaction with the device input already
|
|
185
|
+
// present, so `canSetSessionPreset` reflects the real device. `.photo` is
|
|
186
|
+
// supported by every camera device; if it somehow is not, we keep whatever
|
|
187
|
+
// preset the session defaults to rather than crashing.
|
|
188
|
+
if session.canSetSessionPreset(.photo) {
|
|
189
|
+
session.sessionPreset = .photo
|
|
171
190
|
}
|
|
172
|
-
|
|
173
|
-
//
|
|
191
|
+
|
|
192
|
+
// 3. Photo output
|
|
174
193
|
if #available(iOS 13.0, *) {
|
|
175
194
|
if let maxPhotoQualityPrioritization = maxPhotoQualityPrioritization {
|
|
176
195
|
photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
|
|
177
196
|
}
|
|
178
197
|
}
|
|
179
|
-
|
|
180
198
|
if session.canAddOutput(photoOutput) {
|
|
181
199
|
session.addOutput(photoOutput)
|
|
182
|
-
|
|
183
|
-
if let photoOutputConnection = self.photoOutput.connection(with: .video)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
200
|
+
// Connection only exists after the output is added to the session.
|
|
201
|
+
if let photoOutputConnection = self.photoOutput.connection(with: .video),
|
|
202
|
+
photoOutputConnection.isVideoStabilizationSupported {
|
|
203
|
+
photoOutputConnection.preferredVideoStabilizationMode = .auto
|
|
187
204
|
}
|
|
188
205
|
}
|
|
189
|
-
|
|
190
|
-
//
|
|
206
|
+
|
|
207
|
+
// 4. Metadata output for barcode scanning
|
|
191
208
|
if self.session.canAddOutput(metadataOutput) {
|
|
192
209
|
self.session.addOutput(metadataOutput)
|
|
193
210
|
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
194
|
-
|
|
211
|
+
|
|
195
212
|
let availableTypes = self.metadataOutput.availableMetadataObjectTypes
|
|
196
213
|
let filteredTypes = supportedBarcodeType
|
|
197
214
|
.map { $0.toAVMetadataObjectType() }
|
|
@@ -199,39 +216,59 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
199
216
|
|
|
200
217
|
metadataOutput.metadataObjectTypes = filteredTypes
|
|
201
218
|
}
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
if
|
|
219
|
+
|
|
220
|
+
// 5. Video data output for text / MRZ detection
|
|
221
|
+
if textRequest != nil && self.session.canAddOutput(self.videoDataOutput) {
|
|
205
222
|
self.session.addOutput(self.videoDataOutput)
|
|
206
223
|
}
|
|
224
|
+
|
|
225
|
+
return .success
|
|
207
226
|
}
|
|
208
|
-
|
|
227
|
+
|
|
209
228
|
// MARK: - Pause / Resume non-essential outputs
|
|
229
|
+
|
|
230
|
+
/// Disables OCR + barcode work while a photo is captured.
|
|
231
|
+
/// MUST be called on `sessionQueue` (these are capture-output mutations).
|
|
210
232
|
private func pauseNonEssentialOutputs() {
|
|
233
|
+
assertOnSessionQueue()
|
|
211
234
|
videoDataOutput.setSampleBufferDelegate(nil, queue: nil)
|
|
212
235
|
metadataOutput.rectOfInterest = CGRect(x: 0, y: 0, width: 0, height: 0)
|
|
213
236
|
}
|
|
214
|
-
|
|
237
|
+
|
|
238
|
+
/// Re-enables OCR + barcode work after a capture. May be called from any
|
|
239
|
+
/// thread (photo-capture completion handlers run on an arbitrary queue), so it
|
|
240
|
+
/// hops to the main queue only to read preview-layer geometry and applies all
|
|
241
|
+
/// output mutations back on `sessionQueue`.
|
|
215
242
|
private func resumeNonEssentialOutputs() {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
243
|
+
sessionQueue.async { [weak self] in
|
|
244
|
+
guard let self else { return }
|
|
245
|
+
if self.textDetectionEnabled {
|
|
246
|
+
self.videoDataOutput.setSampleBufferDelegate(self, queue: globalOCRQueue)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
guard let scanner = self.scannerFrameSize, scanner != .zero else {
|
|
250
|
+
self.metadataOutput.rectOfInterest = CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// metadataOutputRectConverted must be read on the main thread.
|
|
255
|
+
DispatchQueue.main.async { [weak self] in
|
|
256
|
+
guard let self else { return }
|
|
257
|
+
let rect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scanner)
|
|
258
|
+
self.sessionQueue.async { [weak self] in
|
|
259
|
+
self?.metadataOutput.rectOfInterest = rect
|
|
260
|
+
}
|
|
261
|
+
}
|
|
225
262
|
}
|
|
226
263
|
}
|
|
227
|
-
|
|
264
|
+
|
|
228
265
|
func zoomPinchStart() {
|
|
229
266
|
sessionQueue.async {
|
|
230
267
|
guard let videoDevice = self.videoDeviceInput?.device else { return }
|
|
231
268
|
self.zoomStartedAt = videoDevice.videoZoomFactor
|
|
232
269
|
}
|
|
233
270
|
}
|
|
234
|
-
|
|
271
|
+
|
|
235
272
|
func zoomPinchChange(pinchScale: CGFloat) {
|
|
236
273
|
guard !pinchScale.isNaN else { return }
|
|
237
274
|
|
|
@@ -240,7 +277,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
240
277
|
|
|
241
278
|
let desiredZoomFactor = (self.zoomStartedAt / self.defaultZoomFactor(for: videoDevice)) * pinchScale
|
|
242
279
|
let zoomForDevice = self.getValidZoom(forDevice: videoDevice, zoom: desiredZoomFactor)
|
|
243
|
-
|
|
280
|
+
|
|
244
281
|
if zoomForDevice != self.normalizedZoom(for: videoDevice) {
|
|
245
282
|
// Only trigger zoom changes if it's an uncontrolled component (zoom isn't manually set)
|
|
246
283
|
// otherwise it's likely to cause issues inf. loops
|
|
@@ -251,14 +288,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
251
288
|
}
|
|
252
289
|
}
|
|
253
290
|
}
|
|
254
|
-
|
|
291
|
+
|
|
255
292
|
func update(maxZoom: Double?) {
|
|
256
293
|
self.maxZoom = maxZoom
|
|
257
294
|
|
|
258
295
|
// Re-update zoom value in case the max was increased
|
|
259
296
|
self.update(zoom: self.zoom)
|
|
260
297
|
}
|
|
261
|
-
|
|
298
|
+
|
|
262
299
|
func update(zoom: Double?) {
|
|
263
300
|
sessionQueue.async {
|
|
264
301
|
self.zoom = zoom
|
|
@@ -269,7 +306,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
269
306
|
self.setZoomFor(videoDevice, to: zoomForDevice)
|
|
270
307
|
}
|
|
271
308
|
}
|
|
272
|
-
|
|
309
|
+
|
|
273
310
|
/**
|
|
274
311
|
`desiredZoom` can be nil when we want to notify what the zoom factor really is
|
|
275
312
|
*/
|
|
@@ -290,11 +327,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
290
327
|
lastOnZoom = desiredOrCameraZoom
|
|
291
328
|
self.onZoomCallback?(["zoom": desiredOrCameraZoom])
|
|
292
329
|
}
|
|
293
|
-
|
|
330
|
+
|
|
294
331
|
func update(onZoom: RCTDirectEventBlock?) {
|
|
295
332
|
self.onZoomCallback = onZoom
|
|
296
333
|
}
|
|
297
|
-
|
|
334
|
+
|
|
298
335
|
func focus(at touchPoint: CGPoint, focusBehavior: FocusBehavior) {
|
|
299
336
|
DispatchQueue.main.async {
|
|
300
337
|
let devicePoint = self.cameraPreview.previewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)
|
|
@@ -309,7 +346,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
309
346
|
self.resetFocus = nil
|
|
310
347
|
self.focusFinished = nil
|
|
311
348
|
}
|
|
312
|
-
|
|
349
|
+
|
|
313
350
|
do {
|
|
314
351
|
try videoDevice.lockForConfiguration()
|
|
315
352
|
|
|
@@ -332,11 +369,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
332
369
|
}
|
|
333
370
|
}
|
|
334
371
|
}
|
|
335
|
-
|
|
372
|
+
|
|
336
373
|
func update(onOrientationChange: RCTDirectEventBlock?) {
|
|
337
374
|
self.onOrientationChange = onOrientationChange
|
|
338
375
|
}
|
|
339
|
-
|
|
376
|
+
|
|
340
377
|
func update(torchMode: TorchMode) {
|
|
341
378
|
sessionQueue.async {
|
|
342
379
|
self.torchMode = torchMode
|
|
@@ -353,29 +390,26 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
353
390
|
}
|
|
354
391
|
}
|
|
355
392
|
}
|
|
356
|
-
|
|
393
|
+
|
|
357
394
|
func update(flashMode: FlashMode) {
|
|
358
395
|
self.flashMode = flashMode
|
|
359
396
|
}
|
|
360
|
-
|
|
397
|
+
|
|
361
398
|
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
|
|
362
399
|
guard #available(iOS 13.0, *) else { return }
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
self.
|
|
366
|
-
self.
|
|
367
|
-
defer {
|
|
368
|
-
self.session.commitConfiguration()
|
|
369
|
-
self.configurationDepth -= 1
|
|
370
|
-
self.handlePendingStopIfNeeded()
|
|
371
|
-
}
|
|
400
|
+
sessionQueue.async { [weak self] in
|
|
401
|
+
guard let self else { return }
|
|
402
|
+
guard maxPhotoQualityPrioritization != self.maxPhotoQualityPrioritization else { return }
|
|
403
|
+
self.beginConfiguration()
|
|
404
|
+
defer { self.commitConfiguration() }
|
|
372
405
|
self.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization
|
|
373
406
|
self.photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization?.avQualityPrioritization ?? .balanced
|
|
374
407
|
}
|
|
375
408
|
}
|
|
376
|
-
|
|
409
|
+
|
|
377
410
|
func update(cameraType: CameraType) {
|
|
378
|
-
sessionQueue.async {
|
|
411
|
+
sessionQueue.async { [weak self] in
|
|
412
|
+
guard let self else { return }
|
|
379
413
|
if self.videoDeviceInput?.device.position == cameraType.avPosition {
|
|
380
414
|
return
|
|
381
415
|
}
|
|
@@ -387,19 +421,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
387
421
|
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
|
|
388
422
|
return
|
|
389
423
|
}
|
|
390
|
-
|
|
424
|
+
|
|
391
425
|
self.removeObservers()
|
|
392
|
-
self.
|
|
393
|
-
self.
|
|
394
|
-
defer {
|
|
395
|
-
self.session.commitConfiguration()
|
|
396
|
-
self.configurationDepth -= 1
|
|
397
|
-
self.handlePendingStopIfNeeded()
|
|
398
|
-
}
|
|
426
|
+
self.beginConfiguration()
|
|
427
|
+
defer { self.commitConfiguration() }
|
|
399
428
|
|
|
400
429
|
// Remove the existing device input first, since using the front and back camera simultaneously is not supported.
|
|
401
430
|
self.session.removeInput(currentViewDeviceInput)
|
|
402
|
-
|
|
431
|
+
|
|
403
432
|
if self.session.canAddInput(videoDeviceInput) {
|
|
404
433
|
self.session.addInput(videoDeviceInput)
|
|
405
434
|
self.resetZoom(forDevice: videoDevice)
|
|
@@ -408,14 +437,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
408
437
|
// If it fails, put back current camera
|
|
409
438
|
self.session.addInput(currentViewDeviceInput)
|
|
410
439
|
}
|
|
411
|
-
|
|
440
|
+
|
|
412
441
|
self.addObservers()
|
|
413
442
|
|
|
414
443
|
// We need to reapply the configuration after reloading the camera
|
|
415
444
|
self.update(torchMode: self.torchMode)
|
|
416
445
|
}
|
|
417
446
|
}
|
|
418
|
-
|
|
447
|
+
|
|
419
448
|
func update(resizeMode: ResizeMode) {
|
|
420
449
|
DispatchQueue.main.async {
|
|
421
450
|
switch resizeMode {
|
|
@@ -426,7 +455,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
426
455
|
}
|
|
427
456
|
}
|
|
428
457
|
}
|
|
429
|
-
|
|
458
|
+
|
|
430
459
|
func capturePicture(onWillCapture: @escaping () -> Void,
|
|
431
460
|
onSuccess: @escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions) -> Void,
|
|
432
461
|
onError: @escaping (_ message: String) -> Void) {
|
|
@@ -435,39 +464,41 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
435
464
|
entering the session queue. Do this to ensure that UI elements are accessed on
|
|
436
465
|
the main thread and session configuration is done on the session queue.
|
|
437
466
|
*/
|
|
438
|
-
|
|
439
|
-
// Pause OCR + barcode before capturing
|
|
440
|
-
self.pauseNonEssentialOutputs()
|
|
441
|
-
|
|
467
|
+
|
|
442
468
|
DispatchQueue.main.async { [weak self] in
|
|
443
469
|
guard let self = self else {
|
|
444
470
|
onError("Camera was deallocated")
|
|
445
471
|
return
|
|
446
472
|
}
|
|
447
|
-
|
|
473
|
+
|
|
448
474
|
let videoPreviewLayerOrientation =
|
|
449
475
|
self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation
|
|
450
|
-
|
|
476
|
+
|
|
451
477
|
self.sessionQueue.async { [weak self] in
|
|
452
478
|
guard let self = self else {
|
|
453
479
|
onError("Camera was deallocated")
|
|
454
480
|
return
|
|
455
481
|
}
|
|
456
482
|
|
|
483
|
+
guard self.inProgressPhotoCaptureDelegates.isEmpty else {
|
|
484
|
+
onError("Capture already in progress")
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
457
488
|
// Validate that the session is ready for photo capture
|
|
458
489
|
guard self.session.isRunning else {
|
|
459
490
|
print("Cannot capture photo: session is not running")
|
|
460
491
|
onError("Camera session is not running")
|
|
461
492
|
return
|
|
462
493
|
}
|
|
463
|
-
|
|
494
|
+
|
|
464
495
|
// Ensure photo output has an active video connection
|
|
465
496
|
guard let photoOutputConnection = self.photoOutput.connection(with: .video) else {
|
|
466
497
|
print("Cannot capture photo: no video connection available")
|
|
467
498
|
onError("Camera connection is not available")
|
|
468
499
|
return
|
|
469
500
|
}
|
|
470
|
-
|
|
501
|
+
|
|
471
502
|
// Verify the connection is active and enabled
|
|
472
503
|
guard photoOutputConnection.isActive && photoOutputConnection.isEnabled else {
|
|
473
504
|
print("Cannot capture photo: video connection is not active or enabled")
|
|
@@ -480,6 +511,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
480
511
|
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
|
481
512
|
}
|
|
482
513
|
|
|
514
|
+
// Pause OCR + barcode work on the session queue, right before capture.
|
|
515
|
+
self.pauseNonEssentialOutputs()
|
|
516
|
+
|
|
483
517
|
let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
|
|
484
518
|
if #available(iOS 13.0, *) {
|
|
485
519
|
settings.photoQualityPrioritization = self.photoOutput.maxPhotoQualityPrioritization
|
|
@@ -493,32 +527,30 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
493
527
|
with: settings,
|
|
494
528
|
onWillCapture: onWillCapture,
|
|
495
529
|
onCaptureSuccess: { [weak self] uniqueID, imageData, thumbnailData, dimensions in
|
|
496
|
-
|
|
497
|
-
self?.inProgressPhotoCaptureDelegates[uniqueID] = nil
|
|
530
|
+
self?.sessionQueue.async { self?.inProgressPhotoCaptureDelegates[uniqueID] = nil }
|
|
498
531
|
onSuccess(imageData, thumbnailData, dimensions)
|
|
499
532
|
self?.resumeNonEssentialOutputs()
|
|
500
533
|
},
|
|
501
534
|
onCaptureError: { [weak self] uniqueID, errorMessage in
|
|
502
|
-
|
|
503
|
-
self?.inProgressPhotoCaptureDelegates[uniqueID] = nil
|
|
535
|
+
self?.sessionQueue.async { self?.inProgressPhotoCaptureDelegates[uniqueID] = nil }
|
|
504
536
|
onError(errorMessage)
|
|
505
537
|
self?.resumeNonEssentialOutputs()
|
|
506
538
|
}
|
|
507
539
|
)
|
|
508
|
-
|
|
540
|
+
|
|
509
541
|
self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = photoCaptureDelegate
|
|
510
542
|
self.photoOutput.capturePhoto(with: settings, delegate: photoCaptureDelegate)
|
|
511
543
|
}
|
|
512
544
|
}
|
|
513
545
|
}
|
|
514
|
-
|
|
546
|
+
|
|
515
547
|
// MARK: - Barcode scanning
|
|
516
548
|
func isBarcodeScannerEnabled(_ isEnabled: Bool,
|
|
517
549
|
supportedBarcodeTypes supportedBarcodeType: [CodeFormat],
|
|
518
550
|
onBarcodeRead: ((_ barcode: String,_ codeFormat:CodeFormat) -> Void)?) {
|
|
519
551
|
sessionQueue.async {
|
|
520
552
|
self.onBarcodeRead = onBarcodeRead
|
|
521
|
-
|
|
553
|
+
|
|
522
554
|
let availableTypes = self.metadataOutput.availableMetadataObjectTypes
|
|
523
555
|
let newTypes: [AVMetadataObject.ObjectType]
|
|
524
556
|
if isEnabled && onBarcodeRead != nil {
|
|
@@ -535,11 +567,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
535
567
|
}
|
|
536
568
|
}
|
|
537
569
|
}
|
|
538
|
-
|
|
570
|
+
|
|
539
571
|
func update(barcodeFrameSize: CGSize?) {
|
|
540
572
|
self.barcodeFrameSize = barcodeFrameSize
|
|
541
573
|
}
|
|
542
|
-
|
|
574
|
+
|
|
543
575
|
func update(scannerFrameSize: CGRect?) {
|
|
544
576
|
guard self.scannerFrameSize != scannerFrameSize else { return }
|
|
545
577
|
self.sessionQueue.async {
|
|
@@ -547,11 +579,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
547
579
|
if !self.session.isRunning {
|
|
548
580
|
return
|
|
549
581
|
}
|
|
550
|
-
|
|
582
|
+
|
|
551
583
|
DispatchQueue.main.async {
|
|
552
584
|
var visibleRect: CGRect?
|
|
553
|
-
if scannerFrameSize
|
|
554
|
-
visibleRect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scannerFrameSize
|
|
585
|
+
if let scannerFrameSize, scannerFrameSize != .zero {
|
|
586
|
+
visibleRect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scannerFrameSize)
|
|
555
587
|
}
|
|
556
588
|
|
|
557
589
|
self.sessionQueue.async {
|
|
@@ -566,7 +598,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
566
598
|
}
|
|
567
599
|
}
|
|
568
600
|
}
|
|
569
|
-
|
|
601
|
+
|
|
570
602
|
|
|
571
603
|
func isTextDetectionEnabled(_ isEnabled: Bool, onTextRead: ((String) -> Void)?) {
|
|
572
604
|
sessionQueue.async {
|
|
@@ -581,26 +613,26 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
581
613
|
self.textRequest?.recognitionLanguages = ["en", "fr", "de", "es", "vi"]
|
|
582
614
|
self.textRequest?.recognitionLevel = .accurate
|
|
583
615
|
self.textRequest?.usesLanguageCorrection = false
|
|
584
|
-
|
|
616
|
+
|
|
585
617
|
self.videoDataOutput.alwaysDiscardsLateVideoFrames = true
|
|
586
618
|
self.videoDataOutput.setSampleBufferDelegate(self, queue: globalOCRQueue)
|
|
587
619
|
self.videoDataOutput.videoSettings = [
|
|
588
620
|
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
|
|
589
621
|
]
|
|
590
|
-
|
|
622
|
+
|
|
591
623
|
} else {
|
|
592
624
|
self.textRequest = nil
|
|
593
625
|
}
|
|
594
626
|
}
|
|
595
627
|
}
|
|
596
|
-
|
|
628
|
+
|
|
597
629
|
// AVCaptureVideoDataOutputSampleBufferDelegate
|
|
598
630
|
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
|
|
599
631
|
guard textDetectionEnabled, let request = textRequest else { return }
|
|
600
632
|
let now = Date()
|
|
601
633
|
if now.timeIntervalSince(lastTextProcess) < textThrottle { return }
|
|
602
634
|
lastTextProcess = now
|
|
603
|
-
|
|
635
|
+
|
|
604
636
|
globalOCRQueue.async {
|
|
605
637
|
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
|
|
606
638
|
var requestOptions: [VNImageOption: Any] = [:]
|
|
@@ -613,7 +645,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
613
645
|
}
|
|
614
646
|
}
|
|
615
647
|
}
|
|
616
|
-
|
|
648
|
+
|
|
617
649
|
// MARK: - AVCaptureMetadataOutputObjectsDelegate
|
|
618
650
|
|
|
619
651
|
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
|
@@ -627,7 +659,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
627
659
|
|
|
628
660
|
onBarcodeRead?(codeStringValue,barcodeType)
|
|
629
661
|
}
|
|
630
|
-
|
|
662
|
+
|
|
631
663
|
// MARK: - Private
|
|
632
664
|
|
|
633
665
|
private func videoOrientation(from deviceOrientation: UIDeviceOrientation) -> AVCaptureVideoOrientation? {
|
|
@@ -646,7 +678,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
646
678
|
@unknown default: return nil
|
|
647
679
|
}
|
|
648
680
|
}
|
|
649
|
-
|
|
681
|
+
|
|
650
682
|
private func videoOrientation(from interfaceOrientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation {
|
|
651
683
|
switch interfaceOrientation {
|
|
652
684
|
case .portrait:
|
|
@@ -661,14 +693,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
661
693
|
@unknown default: return .portrait
|
|
662
694
|
}
|
|
663
695
|
}
|
|
664
|
-
|
|
696
|
+
|
|
665
697
|
private func getBestDevice(for cameraType: CameraType) -> AVCaptureDevice? {
|
|
666
698
|
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraType.avPosition) {
|
|
667
699
|
return device // single-lens/physical device
|
|
668
700
|
}
|
|
669
701
|
return nil
|
|
670
702
|
}
|
|
671
|
-
|
|
703
|
+
|
|
672
704
|
private func defaultZoomFactor(for videoDevice: AVCaptureDevice) -> CGFloat {
|
|
673
705
|
let fallback = 1.0
|
|
674
706
|
guard #available(iOS 13.0, *) else { return fallback }
|
|
@@ -729,7 +761,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
729
761
|
motionManager = CMMotionManager()
|
|
730
762
|
motionManager?.accelerometerUpdateInterval = 0.2
|
|
731
763
|
motionManager?.gyroUpdateInterval = 0.2
|
|
732
|
-
|
|
764
|
+
|
|
765
|
+
motionManager?.startAccelerometerUpdates(to: OperationQueue(), withHandler: { [weak self] (accelerometerData, error) -> Void in
|
|
766
|
+
guard let self else { return }
|
|
733
767
|
guard error == nil else {
|
|
734
768
|
print("\(error!)")
|
|
735
769
|
return
|
|
@@ -740,12 +774,13 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
740
774
|
}
|
|
741
775
|
|
|
742
776
|
guard let newOrientation = self.deviceOrientation(from: accelerometerData.acceleration),
|
|
743
|
-
newOrientation != self.deviceOrientation
|
|
777
|
+
newOrientation != self.deviceOrientation,
|
|
778
|
+
let orientation = Orientation(from: newOrientation) else {
|
|
744
779
|
return
|
|
745
780
|
}
|
|
746
781
|
|
|
747
782
|
self.deviceOrientation = newOrientation
|
|
748
|
-
self.onOrientationChange?(["orientation":
|
|
783
|
+
self.onOrientationChange?(["orientation": orientation.rawValue])
|
|
749
784
|
})
|
|
750
785
|
#endif
|
|
751
786
|
}
|
|
@@ -770,30 +805,34 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
770
805
|
// MARK: Private observers
|
|
771
806
|
|
|
772
807
|
private func addObservers() {
|
|
773
|
-
|
|
808
|
+
if adjustingFocusObservation == nil {
|
|
809
|
+
adjustingFocusObservation = videoDeviceInput?.device.observe(\.isAdjustingFocus,
|
|
810
|
+
options: .new,
|
|
811
|
+
changeHandler: { [weak self] _, change in
|
|
812
|
+
guard let self, let isFocusing = change.newValue else { return }
|
|
774
813
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
guard let self, let isFocusing = change.newValue else { return }
|
|
779
|
-
|
|
780
|
-
self.isAdjustingFocus(isFocusing: isFocusing)
|
|
781
|
-
})
|
|
814
|
+
self.isAdjustingFocus(isFocusing: isFocusing)
|
|
815
|
+
})
|
|
816
|
+
}
|
|
782
817
|
|
|
783
|
-
|
|
784
|
-
object: videoDeviceInput?.device,
|
|
785
|
-
queue: nil,
|
|
786
|
-
using: { [weak self] notification in self?.subjectAreaDidChange(notification: notification) })
|
|
787
|
-
NotificationCenter.default.addObserver(forName: .AVCaptureSessionRuntimeError,
|
|
788
|
-
object: session,
|
|
789
|
-
queue: nil,
|
|
790
|
-
using: { [weak self] notification in self?.sessionRuntimeError(notification: notification) })
|
|
818
|
+
guard notificationObservers.isEmpty else { return }
|
|
791
819
|
|
|
820
|
+
let subjectAreaToken = NotificationCenter.default.addObserver(
|
|
821
|
+
forName: .AVCaptureDeviceSubjectAreaDidChange,
|
|
822
|
+
object: videoDeviceInput?.device,
|
|
823
|
+
queue: nil,
|
|
824
|
+
using: { [weak self] notification in self?.subjectAreaDidChange(notification: notification) })
|
|
825
|
+
let runtimeErrorToken = NotificationCenter.default.addObserver(
|
|
826
|
+
forName: .AVCaptureSessionRuntimeError,
|
|
827
|
+
object: session,
|
|
828
|
+
queue: nil,
|
|
829
|
+
using: { [weak self] notification in self?.sessionRuntimeError(notification: notification) })
|
|
830
|
+
notificationObservers = [subjectAreaToken, runtimeErrorToken]
|
|
792
831
|
}
|
|
793
832
|
|
|
794
833
|
private func removeObservers() {
|
|
795
|
-
|
|
796
|
-
|
|
834
|
+
notificationObservers.forEach { NotificationCenter.default.removeObserver($0) }
|
|
835
|
+
notificationObservers.removeAll()
|
|
797
836
|
|
|
798
837
|
adjustingFocusObservation?.invalidate()
|
|
799
838
|
adjustingFocusObservation = nil
|
|
@@ -829,39 +868,99 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
|
|
|
829
868
|
|
|
830
869
|
print("Capture session runtime error: \(error)")
|
|
831
870
|
|
|
832
|
-
// Automatically try to restart the session
|
|
871
|
+
// Automatically try to restart the session if media services were reset.
|
|
872
|
+
// (After a reset the session is NOT running, so the restart is routed
|
|
873
|
+
// through the guarded helper, which only starts when it is safe to.)
|
|
833
874
|
if error.code == .mediaServicesWereReset {
|
|
834
|
-
sessionQueue.async {
|
|
835
|
-
|
|
836
|
-
self.session.startRunning()
|
|
837
|
-
}
|
|
875
|
+
sessionQueue.async { [weak self] in
|
|
876
|
+
self?.startSessionIfNeeded()
|
|
838
877
|
}
|
|
839
878
|
}
|
|
840
879
|
}
|
|
841
880
|
|
|
842
881
|
func startCamera() {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
self.session.startRunning()
|
|
846
|
-
}
|
|
882
|
+
sessionQueue.async { [weak self] in
|
|
883
|
+
self?.startSessionIfNeeded()
|
|
847
884
|
}
|
|
848
885
|
}
|
|
849
886
|
|
|
850
887
|
func stopCamera() {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
888
|
+
sessionQueue.async { [weak self] in
|
|
889
|
+
self?.stopSessionIfNeeded()
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// MARK: - Session configuration transaction helpers
|
|
894
|
+
|
|
895
|
+
/// Debug-only tripwire enforcing the core invariant of this class: EVERY
|
|
896
|
+
/// AVCaptureSession mutation and start/stop happens on `sessionQueue`. The whole
|
|
897
|
+
/// "startRunning may not be called between beginConfiguration and
|
|
898
|
+
/// commitConfiguration" crash class is prevented precisely because nothing
|
|
899
|
+
/// touches the session off this serial queue. If a future change — or an
|
|
900
|
+
/// external caller such as a mini-app native bridge — ever reaches one of these
|
|
901
|
+
/// methods on the wrong queue, this fails loudly in DEBUG/QA instead of
|
|
902
|
+
/// crashing randomly in production. Compiled out of release builds, so it can
|
|
903
|
+
/// never add a production crash.
|
|
904
|
+
private func assertOnSessionQueue() {
|
|
905
|
+
#if DEBUG
|
|
906
|
+
dispatchPrecondition(condition: .onQueue(sessionQueue))
|
|
907
|
+
#endif
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/// Opens a session configuration transaction and tracks nesting depth.
|
|
911
|
+
/// MUST be balanced by `commitConfiguration()` (use `defer`) and MUST run on
|
|
912
|
+
/// `sessionQueue`.
|
|
913
|
+
private func beginConfiguration() {
|
|
914
|
+
assertOnSessionQueue()
|
|
915
|
+
configurationDepth += 1
|
|
916
|
+
session.beginConfiguration()
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/// Closes a session configuration transaction and flushes a stop that was
|
|
920
|
+
/// requested while the transaction was open. MUST run on `sessionQueue`.
|
|
921
|
+
private func commitConfiguration() {
|
|
922
|
+
assertOnSessionQueue()
|
|
923
|
+
session.commitConfiguration()
|
|
924
|
+
configurationDepth -= 1
|
|
925
|
+
handlePendingStopIfNeeded()
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/// Starts the session only when it is safe to do so. MUST run on `sessionQueue`.
|
|
929
|
+
/// Because every begin/commit pair is synchronous on the serial session queue,
|
|
930
|
+
/// `configurationDepth` is always 0 at the start of a fresh queue block — the
|
|
931
|
+
/// depth check is therefore defence-in-depth against ever calling this from
|
|
932
|
+
/// inside an open transaction.
|
|
933
|
+
private func startSessionIfNeeded() {
|
|
934
|
+
assertOnSessionQueue()
|
|
935
|
+
guard setupResult == .success,
|
|
936
|
+
configurationDepth == 0,
|
|
937
|
+
!pendingStop,
|
|
938
|
+
!session.isRunning else {
|
|
939
|
+
return
|
|
940
|
+
}
|
|
941
|
+
session.startRunning()
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/// Stops the session, deferring until the current transaction commits if one
|
|
945
|
+
/// is open. MUST run on `sessionQueue`.
|
|
946
|
+
private func stopSessionIfNeeded() {
|
|
947
|
+
assertOnSessionQueue()
|
|
948
|
+
guard session.isRunning else { return }
|
|
949
|
+
if configurationDepth > 0 {
|
|
950
|
+
pendingStop = true
|
|
951
|
+
return
|
|
855
952
|
}
|
|
953
|
+
session.stopRunning()
|
|
856
954
|
}
|
|
857
955
|
|
|
858
956
|
// MARK: - Private helper for safe session stop
|
|
859
|
-
|
|
957
|
+
|
|
860
958
|
private func handlePendingStopIfNeeded() {
|
|
861
959
|
// Must be called on sessionQueue after configuration completes
|
|
960
|
+
assertOnSessionQueue()
|
|
862
961
|
// Only execute if all configurations are done (depth == 0)
|
|
863
962
|
guard configurationDepth == 0 && pendingStop else { return }
|
|
864
|
-
|
|
963
|
+
|
|
865
964
|
pendingStop = false
|
|
866
965
|
if session.isRunning {
|
|
867
966
|
session.stopRunning()
|