@momo-kits/camerakit 0.161.2-beta.2 → 0.161.2-beta.7

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 = [zoomMode isEqualToString:@"on"] ? CKZoomModeOn : CKZoomModeOff;
235
+ _view.zoomMode = [focusMode 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,12 +260,6 @@ 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
-
269
263
  [self prepareView];
270
264
  }
271
265
 
@@ -18,8 +18,12 @@ import Foundation
18
18
  return CameraView()
19
19
  }
20
20
 
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) })
21
+ @objc public static func capture(camera: CameraView,
22
+ options: NSDictionary,
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) })
23
27
  }
24
28
 
25
29
  @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 let messageString = firstFeature.messageString {
365
- self.onBarcodeRead(barcode: messageString, codeFormat: CodeFormat.qr)
364
+ if (firstFeature.messageString != nil ) {
365
+ self.onBarcodeRead(barcode: firstFeature.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
- private static let sharedSessionQueue = DispatchQueue(label: "com.tesla.react-native-camera-kit.session")
26
- private let sessionQueue = RealCamera.sharedSessionQueue
27
-
25
+ // Communicate with the session and other session objects on this queue.
26
+ private let sessionQueue = DispatchQueue(label: "com.tesla.react-native-camera-kit")
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,40 +49,44 @@ 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
-
61
59
  // Keep delegate objects in memory to avoid collecting them before photo capturing finishes
62
60
  private var inProgressPhotoCaptureDelegates = [Int64: PhotoCaptureDelegate]()
63
-
61
+
64
62
  private var onTextRead: ((_ text: String) -> Void)?
65
63
  private let videoDataOutput = AVCaptureVideoDataOutput()
66
64
  private var textRequest: VNRecognizeTextRequest?
67
65
  private var textDetectionEnabled = false
68
66
  private var lastTextProcess = Date.distantPast
69
67
  private let textThrottle: TimeInterval = 0.35 // seconds
70
-
68
+
71
69
  private var zoomStartedAt: Double = 1.0
72
-
70
+
73
71
  // MARK: - Lifecycle
74
72
 
75
73
  func cameraRemovedFromSuperview() {
76
- sessionQueue.async { [weak self] in
77
- guard let self, self.setupResult == .success else { return }
74
+ sessionQueue.async {
75
+ if self.setupResult == .success {
76
+ // Only stop if session is running
77
+ guard self.session.isRunning else {
78
+ self.removeObservers()
79
+ return
80
+ }
78
81
 
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()
82
+ // If we're currently configuring the session, mark pending stop
83
+ // and let the configuration completion handle it
84
+ if self.configurationDepth > 0 {
85
+ self.pendingStop = true
86
+ return
87
+ }
82
88
 
83
- // If the stop was deferred, handlePendingStopIfNeeded() will remove the
84
- // observers once the transaction commits; otherwise remove them now.
85
- if !self.pendingStop {
89
+ self.session.stopRunning()
86
90
  self.removeObservers()
87
91
  }
88
92
  }
@@ -95,120 +99,99 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
95
99
  UIDevice.current.endGeneratingDeviceOrientationNotifications()
96
100
  #endif
97
101
  }
98
-
102
+
99
103
  deinit {
100
104
  removeObservers()
101
-
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
107
- sessionQueue.async {
108
- if session.isRunning {
109
- session.stopRunning()
110
- }
111
- }
112
105
  }
113
-
106
+
114
107
  // MARK: - Public
115
108
 
116
109
  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)
110
+
111
+ // Setup the capture session with priority on basic video preview first
112
+ sessionQueue.async {
113
+ self.setupResult = self.setupBasicVideoInput(cameraType: cameraType)
114
+
115
+ if self.setupResult == .success {
116
+ self.session.startRunning()
117
+
118
+ self.setupAdditionalOutputs(supportedBarcodeType: supportedBarcodeType)
119
+
120
+ self.addObservers()
143
121
 
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 }
122
+ self.update(torchMode: self.torchMode)
123
+ }
124
+
125
+ DispatchQueue.main.async {
150
126
  self.cameraPreview.session = self.session
151
127
  self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill
128
+ self.session.sessionPreset = .photo
152
129
  self.setVideoOrientationToInterfaceOrientation()
153
130
  }
154
131
  }
155
-
156
- DispatchQueue.global(qos: .utility).async { [weak self] in
157
- self?.initializeMotionManager()
132
+
133
+ DispatchQueue.global(qos: .utility).async {
134
+ self.initializeMotionManager()
158
135
  }
136
+
159
137
  }
160
138
 
161
139
  // MARK: - Private optimization methods
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()
140
+
141
+ private func setupBasicVideoInput(cameraType: CameraType) -> SetupResult {
168
142
  guard let videoDevice = self.getBestDevice(for: cameraType),
169
143
  let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
170
144
  return .sessionConfigurationFailed
171
145
  }
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
146
+
147
+ configurationDepth += 1
148
+ session.beginConfiguration()
149
+ defer {
150
+ session.commitConfiguration()
151
+ configurationDepth -= 1
152
+ handlePendingStopIfNeeded()
153
+ }
154
+
155
+ if session.canAddInput(videoDeviceInput) {
156
+ session.addInput(videoDeviceInput)
157
+ self.videoDeviceInput = videoDeviceInput
158
+ self.resetZoom(forDevice: videoDevice)
159
+ return .success
160
+ }
161
+ return .sessionConfigurationFailed
162
+ }
163
+
164
+ private func setupAdditionalOutputs(supportedBarcodeType: [CodeFormat]) {
165
+ configurationDepth += 1
166
+ session.beginConfiguration()
167
+ defer {
168
+ session.commitConfiguration()
169
+ configurationDepth -= 1
170
+ handlePendingStopIfNeeded()
190
171
  }
191
-
192
- // 3. Photo output
172
+
173
+ // Add photo output
193
174
  if #available(iOS 13.0, *) {
194
175
  if let maxPhotoQualityPrioritization = maxPhotoQualityPrioritization {
195
176
  photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
196
177
  }
197
178
  }
179
+
198
180
  if session.canAddOutput(photoOutput) {
199
181
  session.addOutput(photoOutput)
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
182
+
183
+ if let photoOutputConnection = self.photoOutput.connection(with: .video) {
184
+ if photoOutputConnection.isVideoStabilizationSupported {
185
+ photoOutputConnection.preferredVideoStabilizationMode = .auto
186
+ }
204
187
  }
205
188
  }
206
-
207
- // 4. Metadata output for barcode scanning
189
+
190
+ // Add metadata output for barcode scanning
208
191
  if self.session.canAddOutput(metadataOutput) {
209
192
  self.session.addOutput(metadataOutput)
210
193
  metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
211
-
194
+
212
195
  let availableTypes = self.metadataOutput.availableMetadataObjectTypes
213
196
  let filteredTypes = supportedBarcodeType
214
197
  .map { $0.toAVMetadataObjectType() }
@@ -216,59 +199,39 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
216
199
 
217
200
  metadataOutput.metadataObjectTypes = filteredTypes
218
201
  }
219
-
220
- // 5. Video data output for text / MRZ detection
221
- if textRequest != nil && self.session.canAddOutput(self.videoDataOutput) {
202
+
203
+ // add for text detections
204
+ if (textRequest != nil && self.session.canAddOutput(self.videoDataOutput)) {
222
205
  self.session.addOutput(self.videoDataOutput)
223
206
  }
224
-
225
- return .success
226
207
  }
227
-
208
+
228
209
  // 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).
232
210
  private func pauseNonEssentialOutputs() {
233
- assertOnSessionQueue()
234
211
  videoDataOutput.setSampleBufferDelegate(nil, queue: nil)
235
212
  metadataOutput.rectOfInterest = CGRect(x: 0, y: 0, width: 0, height: 0)
236
213
  }
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`.
214
+
242
215
  private func resumeNonEssentialOutputs() {
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
- }
216
+ if textDetectionEnabled {
217
+ videoDataOutput.setSampleBufferDelegate(self, queue: globalOCRQueue)
218
+ }
219
+ // Restore real rect of interest
220
+ if let scanner = scannerFrameSize, scanner != .zero {
221
+ metadataOutput.rectOfInterest =
222
+ cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scanner)
223
+ } else {
224
+ metadataOutput.rectOfInterest = CGRect(x: 0, y: 0, width: 1, height: 1)
262
225
  }
263
226
  }
264
-
227
+
265
228
  func zoomPinchStart() {
266
229
  sessionQueue.async {
267
230
  guard let videoDevice = self.videoDeviceInput?.device else { return }
268
231
  self.zoomStartedAt = videoDevice.videoZoomFactor
269
232
  }
270
233
  }
271
-
234
+
272
235
  func zoomPinchChange(pinchScale: CGFloat) {
273
236
  guard !pinchScale.isNaN else { return }
274
237
 
@@ -277,7 +240,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
277
240
 
278
241
  let desiredZoomFactor = (self.zoomStartedAt / self.defaultZoomFactor(for: videoDevice)) * pinchScale
279
242
  let zoomForDevice = self.getValidZoom(forDevice: videoDevice, zoom: desiredZoomFactor)
280
-
243
+
281
244
  if zoomForDevice != self.normalizedZoom(for: videoDevice) {
282
245
  // Only trigger zoom changes if it's an uncontrolled component (zoom isn't manually set)
283
246
  // otherwise it's likely to cause issues inf. loops
@@ -288,14 +251,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
288
251
  }
289
252
  }
290
253
  }
291
-
254
+
292
255
  func update(maxZoom: Double?) {
293
256
  self.maxZoom = maxZoom
294
257
 
295
258
  // Re-update zoom value in case the max was increased
296
259
  self.update(zoom: self.zoom)
297
260
  }
298
-
261
+
299
262
  func update(zoom: Double?) {
300
263
  sessionQueue.async {
301
264
  self.zoom = zoom
@@ -306,7 +269,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
306
269
  self.setZoomFor(videoDevice, to: zoomForDevice)
307
270
  }
308
271
  }
309
-
272
+
310
273
  /**
311
274
  `desiredZoom` can be nil when we want to notify what the zoom factor really is
312
275
  */
@@ -327,11 +290,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
327
290
  lastOnZoom = desiredOrCameraZoom
328
291
  self.onZoomCallback?(["zoom": desiredOrCameraZoom])
329
292
  }
330
-
293
+
331
294
  func update(onZoom: RCTDirectEventBlock?) {
332
295
  self.onZoomCallback = onZoom
333
296
  }
334
-
297
+
335
298
  func focus(at touchPoint: CGPoint, focusBehavior: FocusBehavior) {
336
299
  DispatchQueue.main.async {
337
300
  let devicePoint = self.cameraPreview.previewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)
@@ -346,7 +309,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
346
309
  self.resetFocus = nil
347
310
  self.focusFinished = nil
348
311
  }
349
-
312
+
350
313
  do {
351
314
  try videoDevice.lockForConfiguration()
352
315
 
@@ -369,11 +332,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
369
332
  }
370
333
  }
371
334
  }
372
-
335
+
373
336
  func update(onOrientationChange: RCTDirectEventBlock?) {
374
337
  self.onOrientationChange = onOrientationChange
375
338
  }
376
-
339
+
377
340
  func update(torchMode: TorchMode) {
378
341
  sessionQueue.async {
379
342
  self.torchMode = torchMode
@@ -390,26 +353,29 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
390
353
  }
391
354
  }
392
355
  }
393
-
356
+
394
357
  func update(flashMode: FlashMode) {
395
358
  self.flashMode = flashMode
396
359
  }
397
-
360
+
398
361
  func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
399
362
  guard #available(iOS 13.0, *) else { return }
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() }
363
+ guard maxPhotoQualityPrioritization != self.maxPhotoQualityPrioritization else { return }
364
+ sessionQueue.async {
365
+ self.configurationDepth += 1
366
+ self.session.beginConfiguration()
367
+ defer {
368
+ self.session.commitConfiguration()
369
+ self.configurationDepth -= 1
370
+ self.handlePendingStopIfNeeded()
371
+ }
405
372
  self.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization
406
373
  self.photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization?.avQualityPrioritization ?? .balanced
407
374
  }
408
375
  }
409
-
376
+
410
377
  func update(cameraType: CameraType) {
411
- sessionQueue.async { [weak self] in
412
- guard let self else { return }
378
+ sessionQueue.async {
413
379
  if self.videoDeviceInput?.device.position == cameraType.avPosition {
414
380
  return
415
381
  }
@@ -421,14 +387,19 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
421
387
  let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
422
388
  return
423
389
  }
424
-
390
+
425
391
  self.removeObservers()
426
- self.beginConfiguration()
427
- defer { self.commitConfiguration() }
392
+ self.configurationDepth += 1
393
+ self.session.beginConfiguration()
394
+ defer {
395
+ self.session.commitConfiguration()
396
+ self.configurationDepth -= 1
397
+ self.handlePendingStopIfNeeded()
398
+ }
428
399
 
429
400
  // Remove the existing device input first, since using the front and back camera simultaneously is not supported.
430
401
  self.session.removeInput(currentViewDeviceInput)
431
-
402
+
432
403
  if self.session.canAddInput(videoDeviceInput) {
433
404
  self.session.addInput(videoDeviceInput)
434
405
  self.resetZoom(forDevice: videoDevice)
@@ -437,14 +408,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
437
408
  // If it fails, put back current camera
438
409
  self.session.addInput(currentViewDeviceInput)
439
410
  }
440
-
411
+
441
412
  self.addObservers()
442
413
 
443
414
  // We need to reapply the configuration after reloading the camera
444
415
  self.update(torchMode: self.torchMode)
445
416
  }
446
417
  }
447
-
418
+
448
419
  func update(resizeMode: ResizeMode) {
449
420
  DispatchQueue.main.async {
450
421
  switch resizeMode {
@@ -455,7 +426,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
455
426
  }
456
427
  }
457
428
  }
458
-
429
+
459
430
  func capturePicture(onWillCapture: @escaping () -> Void,
460
431
  onSuccess: @escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions) -> Void,
461
432
  onError: @escaping (_ message: String) -> Void) {
@@ -464,41 +435,39 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
464
435
  entering the session queue. Do this to ensure that UI elements are accessed on
465
436
  the main thread and session configuration is done on the session queue.
466
437
  */
467
-
438
+
439
+ // Pause OCR + barcode before capturing
440
+ self.pauseNonEssentialOutputs()
441
+
468
442
  DispatchQueue.main.async { [weak self] in
469
443
  guard let self = self else {
470
444
  onError("Camera was deallocated")
471
445
  return
472
446
  }
473
-
447
+
474
448
  let videoPreviewLayerOrientation =
475
449
  self.videoOrientation(from: self.deviceOrientation) ?? self.cameraPreview.previewLayer.connection?.videoOrientation
476
-
450
+
477
451
  self.sessionQueue.async { [weak self] in
478
452
  guard let self = self else {
479
453
  onError("Camera was deallocated")
480
454
  return
481
455
  }
482
456
 
483
- guard self.inProgressPhotoCaptureDelegates.isEmpty else {
484
- onError("Capture already in progress")
485
- return
486
- }
487
-
488
457
  // Validate that the session is ready for photo capture
489
458
  guard self.session.isRunning else {
490
459
  print("Cannot capture photo: session is not running")
491
460
  onError("Camera session is not running")
492
461
  return
493
462
  }
494
-
463
+
495
464
  // Ensure photo output has an active video connection
496
465
  guard let photoOutputConnection = self.photoOutput.connection(with: .video) else {
497
466
  print("Cannot capture photo: no video connection available")
498
467
  onError("Camera connection is not available")
499
468
  return
500
469
  }
501
-
470
+
502
471
  // Verify the connection is active and enabled
503
472
  guard photoOutputConnection.isActive && photoOutputConnection.isEnabled else {
504
473
  print("Cannot capture photo: video connection is not active or enabled")
@@ -511,9 +480,6 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
511
480
  photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
512
481
  }
513
482
 
514
- // Pause OCR + barcode work on the session queue, right before capture.
515
- self.pauseNonEssentialOutputs()
516
-
517
483
  let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
518
484
  if #available(iOS 13.0, *) {
519
485
  settings.photoQualityPrioritization = self.photoOutput.maxPhotoQualityPrioritization
@@ -527,30 +493,32 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
527
493
  with: settings,
528
494
  onWillCapture: onWillCapture,
529
495
  onCaptureSuccess: { [weak self] uniqueID, imageData, thumbnailData, dimensions in
530
- self?.sessionQueue.async { self?.inProgressPhotoCaptureDelegates[uniqueID] = nil }
496
+ // Use weak self to prevent crash if camera is deallocated during capture
497
+ self?.inProgressPhotoCaptureDelegates[uniqueID] = nil
531
498
  onSuccess(imageData, thumbnailData, dimensions)
532
499
  self?.resumeNonEssentialOutputs()
533
500
  },
534
501
  onCaptureError: { [weak self] uniqueID, errorMessage in
535
- self?.sessionQueue.async { self?.inProgressPhotoCaptureDelegates[uniqueID] = nil }
502
+ // Use weak self to prevent crash if camera is deallocated during capture
503
+ self?.inProgressPhotoCaptureDelegates[uniqueID] = nil
536
504
  onError(errorMessage)
537
505
  self?.resumeNonEssentialOutputs()
538
506
  }
539
507
  )
540
-
508
+
541
509
  self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = photoCaptureDelegate
542
510
  self.photoOutput.capturePhoto(with: settings, delegate: photoCaptureDelegate)
543
511
  }
544
512
  }
545
513
  }
546
-
514
+
547
515
  // MARK: - Barcode scanning
548
516
  func isBarcodeScannerEnabled(_ isEnabled: Bool,
549
517
  supportedBarcodeTypes supportedBarcodeType: [CodeFormat],
550
518
  onBarcodeRead: ((_ barcode: String,_ codeFormat:CodeFormat) -> Void)?) {
551
519
  sessionQueue.async {
552
520
  self.onBarcodeRead = onBarcodeRead
553
-
521
+
554
522
  let availableTypes = self.metadataOutput.availableMetadataObjectTypes
555
523
  let newTypes: [AVMetadataObject.ObjectType]
556
524
  if isEnabled && onBarcodeRead != nil {
@@ -567,11 +535,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
567
535
  }
568
536
  }
569
537
  }
570
-
538
+
571
539
  func update(barcodeFrameSize: CGSize?) {
572
540
  self.barcodeFrameSize = barcodeFrameSize
573
541
  }
574
-
542
+
575
543
  func update(scannerFrameSize: CGRect?) {
576
544
  guard self.scannerFrameSize != scannerFrameSize else { return }
577
545
  self.sessionQueue.async {
@@ -579,11 +547,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
579
547
  if !self.session.isRunning {
580
548
  return
581
549
  }
582
-
550
+
583
551
  DispatchQueue.main.async {
584
552
  var visibleRect: CGRect?
585
- if let scannerFrameSize, scannerFrameSize != .zero {
586
- visibleRect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scannerFrameSize)
553
+ if scannerFrameSize != nil && scannerFrameSize != .zero {
554
+ visibleRect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scannerFrameSize!)
587
555
  }
588
556
 
589
557
  self.sessionQueue.async {
@@ -598,7 +566,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
598
566
  }
599
567
  }
600
568
  }
601
-
569
+
602
570
 
603
571
  func isTextDetectionEnabled(_ isEnabled: Bool, onTextRead: ((String) -> Void)?) {
604
572
  sessionQueue.async {
@@ -613,26 +581,26 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
613
581
  self.textRequest?.recognitionLanguages = ["en", "fr", "de", "es", "vi"]
614
582
  self.textRequest?.recognitionLevel = .accurate
615
583
  self.textRequest?.usesLanguageCorrection = false
616
-
584
+
617
585
  self.videoDataOutput.alwaysDiscardsLateVideoFrames = true
618
586
  self.videoDataOutput.setSampleBufferDelegate(self, queue: globalOCRQueue)
619
587
  self.videoDataOutput.videoSettings = [
620
588
  kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
621
589
  ]
622
-
590
+
623
591
  } else {
624
592
  self.textRequest = nil
625
593
  }
626
594
  }
627
595
  }
628
-
596
+
629
597
  // AVCaptureVideoDataOutputSampleBufferDelegate
630
598
  func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
631
599
  guard textDetectionEnabled, let request = textRequest else { return }
632
600
  let now = Date()
633
601
  if now.timeIntervalSince(lastTextProcess) < textThrottle { return }
634
602
  lastTextProcess = now
635
-
603
+
636
604
  globalOCRQueue.async {
637
605
  guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
638
606
  var requestOptions: [VNImageOption: Any] = [:]
@@ -645,7 +613,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
645
613
  }
646
614
  }
647
615
  }
648
-
616
+
649
617
  // MARK: - AVCaptureMetadataOutputObjectsDelegate
650
618
 
651
619
  func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
@@ -659,7 +627,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
659
627
 
660
628
  onBarcodeRead?(codeStringValue,barcodeType)
661
629
  }
662
-
630
+
663
631
  // MARK: - Private
664
632
 
665
633
  private func videoOrientation(from deviceOrientation: UIDeviceOrientation) -> AVCaptureVideoOrientation? {
@@ -678,7 +646,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
678
646
  @unknown default: return nil
679
647
  }
680
648
  }
681
-
649
+
682
650
  private func videoOrientation(from interfaceOrientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation {
683
651
  switch interfaceOrientation {
684
652
  case .portrait:
@@ -693,14 +661,14 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
693
661
  @unknown default: return .portrait
694
662
  }
695
663
  }
696
-
664
+
697
665
  private func getBestDevice(for cameraType: CameraType) -> AVCaptureDevice? {
698
666
  if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraType.avPosition) {
699
667
  return device // single-lens/physical device
700
668
  }
701
669
  return nil
702
670
  }
703
-
671
+
704
672
  private func defaultZoomFactor(for videoDevice: AVCaptureDevice) -> CGFloat {
705
673
  let fallback = 1.0
706
674
  guard #available(iOS 13.0, *) else { return fallback }
@@ -761,9 +729,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
761
729
  motionManager = CMMotionManager()
762
730
  motionManager?.accelerometerUpdateInterval = 0.2
763
731
  motionManager?.gyroUpdateInterval = 0.2
764
-
765
- motionManager?.startAccelerometerUpdates(to: OperationQueue(), withHandler: { [weak self] (accelerometerData, error) -> Void in
766
- guard let self else { return }
732
+ motionManager?.startAccelerometerUpdates(to: OperationQueue(), withHandler: { (accelerometerData, error) -> Void in
767
733
  guard error == nil else {
768
734
  print("\(error!)")
769
735
  return
@@ -774,13 +740,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
774
740
  }
775
741
 
776
742
  guard let newOrientation = self.deviceOrientation(from: accelerometerData.acceleration),
777
- newOrientation != self.deviceOrientation,
778
- let orientation = Orientation(from: newOrientation) else {
743
+ newOrientation != self.deviceOrientation else {
779
744
  return
780
745
  }
781
746
 
782
747
  self.deviceOrientation = newOrientation
783
- self.onOrientationChange?(["orientation": orientation.rawValue])
748
+ self.onOrientationChange?(["orientation": Orientation.init(from: newOrientation)!.rawValue])
784
749
  })
785
750
  #endif
786
751
  }
@@ -805,34 +770,30 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
805
770
  // MARK: Private observers
806
771
 
807
772
  private func addObservers() {
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 }
773
+ guard adjustingFocusObservation == nil else { return }
813
774
 
814
- self.isAdjustingFocus(isFocusing: isFocusing)
815
- })
816
- }
775
+ adjustingFocusObservation = videoDeviceInput?.device.observe(\.isAdjustingFocus,
776
+ options: .new,
777
+ changeHandler: { [weak self] _, change in
778
+ guard let self, let isFocusing = change.newValue else { return }
817
779
 
818
- guard notificationObservers.isEmpty else { return }
780
+ self.isAdjustingFocus(isFocusing: isFocusing)
781
+ })
782
+
783
+ NotificationCenter.default.addObserver(forName: .AVCaptureDeviceSubjectAreaDidChange,
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) })
819
791
 
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]
831
792
  }
832
793
 
833
794
  private func removeObservers() {
834
- notificationObservers.forEach { NotificationCenter.default.removeObserver($0) }
835
- notificationObservers.removeAll()
795
+ // swiftlint:disable:next notification_center_detachment
796
+ NotificationCenter.default.removeObserver(self)
836
797
 
837
798
  adjustingFocusObservation?.invalidate()
838
799
  adjustingFocusObservation = nil
@@ -868,99 +829,39 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
868
829
 
869
830
  print("Capture session runtime error: \(error)")
870
831
 
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.)
832
+ // Automatically try to restart the session running if media services were reset
874
833
  if error.code == .mediaServicesWereReset {
875
- sessionQueue.async { [weak self] in
876
- self?.startSessionIfNeeded()
834
+ sessionQueue.async {
835
+ if self.session.isRunning {
836
+ self.session.startRunning()
837
+ }
877
838
  }
878
839
  }
879
840
  }
880
841
 
881
842
  func startCamera() {
882
- sessionQueue.async { [weak self] in
883
- self?.startSessionIfNeeded()
843
+ self.sessionQueue.async {
844
+ if !self.session.isRunning {
845
+ self.session.startRunning()
846
+ }
884
847
  }
885
848
  }
886
849
 
887
850
  func stopCamera() {
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
851
+ self.sessionQueue.async {
852
+ if self.session.isRunning {
853
+ self.session.stopRunning()
854
+ }
952
855
  }
953
- session.stopRunning()
954
856
  }
955
857
 
956
858
  // MARK: - Private helper for safe session stop
957
-
859
+
958
860
  private func handlePendingStopIfNeeded() {
959
861
  // Must be called on sessionQueue after configuration completes
960
- assertOnSessionQueue()
961
862
  // Only execute if all configurations are done (depth == 0)
962
863
  guard configurationDepth == 0 && pendingStop else { return }
963
-
864
+
964
865
  pendingStop = false
965
866
  if session.isRunning {
966
867
  session.stopRunning()
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
1
  {
2
- "name": "@momo-kits/camerakit",
3
- "version": "0.161.2-beta.2",
4
- "repository": {
5
- "type": "git",
6
- "url": "https://github.com/teslamotors/react-native-camera-kit.git"
7
- },
8
- "publishConfig": {
9
- "registry": "https://registry.npmjs.org/"
10
- },
11
- "description": "A high performance, fully featured, rock solid camera library for React Native applications",
12
- "nativePackage": true,
13
- "scripts": {
14
- "build": "echo",
15
- "test": "jest",
16
- "lint": "yarn eslint -c .eslintrc.js"
17
- },
18
- "main": "./src/index.ts",
19
- "dependencies": {},
20
- "license": "MIT",
21
- "devDependencies": {
22
- "react": "19.0.0",
23
- "react-native": "0.80.1"
24
- },
25
- "peerDependencies": {
26
- "react": "*",
27
- "react-native": "*"
28
- },
29
- "engines": {
30
- "node": ">=18"
31
- },
32
- "codegenConfig": {
33
- "name": "rncamerakit_specs",
34
- "type": "all",
35
- "jsSrcsDir": "src/specs",
36
- "android": {
37
- "javaPackageName": "com.rncamerakit"
38
- }
39
- },
40
- "packageManager": "yarn@1.22.22"
41
- }
2
+ "name": "@momo-kits/camerakit",
3
+ "version": "0.161.2-beta.7",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/teslamotors/react-native-camera-kit.git"
7
+ },
8
+ "publishConfig": {
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "description": "A high performance, fully featured, rock solid camera library for React Native applications",
12
+ "nativePackage": true,
13
+ "scripts": {
14
+ "build": "echo",
15
+ "test": "jest",
16
+ "lint": "yarn eslint -c .eslintrc.js"
17
+ },
18
+ "main": "./src/index.ts",
19
+ "dependencies": {},
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "react": "19.0.0",
23
+ "react-native": "0.80.1"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "*",
27
+ "react-native": "*"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "codegenConfig": {
33
+ "name": "rncamerakit_specs",
34
+ "type": "all",
35
+ "jsSrcsDir": "src/specs",
36
+ "android": {
37
+ "javaPackageName": "com.rncamerakit"
38
+ }
39
+ },
40
+ "packageManager": "yarn@1.22.22"
41
+ }