@momo-kits/camerakit 0.161.1-beta.1 → 0.161.2-beta.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.
@@ -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
 
@@ -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
 
@@ -71,16 +71,22 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
71
71
  // MARK: - Lifecycle
72
72
 
73
73
  func cameraRemovedFromSuperview() {
74
- sessionQueue.async { [weak self] in
75
- 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
+ }
76
81
 
77
- // Stop now if idle/safe, or defer the stop until the open configuration
78
- // transaction commits (stopSessionIfNeeded sets pendingStop in that case).
79
- 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
+ }
80
88
 
81
- // If the stop was deferred, handlePendingStopIfNeeded() will remove the
82
- // observers once the transaction commits; otherwise remove them now.
83
- if !self.pendingStop {
89
+ self.session.stopRunning()
84
90
  self.removeObservers()
85
91
  }
86
92
  }
@@ -96,117 +102,96 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
96
102
 
97
103
  deinit {
98
104
  removeObservers()
99
-
100
- // Ensure the capture hardware is released even if cameraRemovedFromSuperview()
101
- // was never delivered (e.g. a forced React surface cleanup during rapid
102
- // mount/unmount). Capture only the session object so the block does not
103
- // resurrect `self`.
104
- let session = self.session
105
- sessionQueue.async {
106
- if session.isRunning {
107
- session.stopRunning()
108
- }
109
- }
110
105
  }
111
106
 
112
107
  // MARK: - Public
113
108
 
114
109
  func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
115
- sessionQueue.async { [weak self] in
116
- guard let self else { return }
117
-
118
- // Idempotency guard: a RealCamera configures its session exactly once.
119
- // A fresh instance is created on every mount / Fabric recycle, so this
120
- // never blocks a legitimate restart — resuming after stopCamera() is
121
- // handled by startCamera() -> startSessionIfNeeded().
122
- guard self.setupResult == .notStarted else { return }
123
-
124
- // Configure the WHOLE session (inputs, preset, outputs) inside a single
125
- // begin/commit transaction on the session queue. The preset is set here
126
- // and never on the main queue, so the startRunning() below can no longer
127
- // observe the session mid-configuration from another thread.
128
- //
129
- // This is the root-cause fix for:
130
- // "[AVCaptureSession startRunning] startRunning may not be called between
131
- // calls to beginConfiguration and commitConfiguration".
132
- self.setupResult = self.configureSession(cameraType: cameraType,
133
- supportedBarcodeType: supportedBarcodeType)
134
-
135
- guard self.setupResult == .success else { return }
136
-
137
- // Order matches the previous behaviour: start, then observe, then torch.
138
- self.startSessionIfNeeded()
139
- self.addObservers()
140
- 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()
121
+
122
+ self.update(torchMode: self.torchMode)
123
+ }
141
124
 
142
- // Only the preview LAYER is touched on the main queue. Connecting an
143
- // AVCaptureVideoPreviewLayer to a session from the main thread is
144
- // sanctioned by AVFoundation (see Apple's AVCam) and does NOT open a
145
- // session configuration transaction, so it cannot race startRunning().
146
- DispatchQueue.main.async { [weak self] in
147
- guard let self else { return }
125
+ DispatchQueue.main.async {
148
126
  self.cameraPreview.session = self.session
149
127
  self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill
128
+ self.session.sessionPreset = .photo
150
129
  self.setVideoOrientationToInterfaceOrientation()
151
130
  }
152
131
  }
153
-
154
- DispatchQueue.global(qos: .utility).async { [weak self] in
155
- self?.initializeMotionManager()
132
+
133
+ DispatchQueue.global(qos: .utility).async {
134
+ self.initializeMotionManager()
156
135
  }
136
+
157
137
  }
158
138
 
159
139
  // MARK: - Private optimization methods
160
-
161
- /// Configures the video input, session preset and every output in ONE atomic
162
- /// transaction. Must be called on `sessionQueue`. Order is significant:
163
- /// beginConfiguration -> addInput -> set preset -> addOutputs -> commitConfiguration.
164
- private func configureSession(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) -> SetupResult {
165
- assertOnSessionQueue()
140
+
141
+ private func setupBasicVideoInput(cameraType: CameraType) -> SetupResult {
166
142
  guard let videoDevice = self.getBestDevice(for: cameraType),
167
143
  let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
168
144
  return .sessionConfigurationFailed
169
145
  }
170
-
171
- beginConfiguration()
172
- defer { commitConfiguration() }
173
-
174
- // 1. Video input
175
- guard session.canAddInput(videoDeviceInput) else {
176
- return .sessionConfigurationFailed
146
+
147
+ configurationDepth += 1
148
+ session.beginConfiguration()
149
+ defer {
150
+ session.commitConfiguration()
151
+ configurationDepth -= 1
152
+ handlePendingStopIfNeeded()
177
153
  }
178
- session.addInput(videoDeviceInput)
179
- self.videoDeviceInput = videoDeviceInput
180
- self.resetZoom(forDevice: videoDevice)
181
-
182
- // 2. Preset — set inside the transaction with the device input already
183
- // present, so `canSetSessionPreset` reflects the real device. `.photo` is
184
- // supported by every camera device; if it somehow is not, we keep whatever
185
- // preset the session defaults to rather than crashing.
186
- if session.canSetSessionPreset(.photo) {
187
- session.sessionPreset = .photo
154
+
155
+ if session.canAddInput(videoDeviceInput) {
156
+ session.addInput(videoDeviceInput)
157
+ self.videoDeviceInput = videoDeviceInput
158
+ self.resetZoom(forDevice: videoDevice)
159
+ return .success
188
160
  }
189
-
190
- // 3. Photo output
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()
171
+ }
172
+
173
+ // Add photo output
191
174
  if #available(iOS 13.0, *) {
192
175
  if let maxPhotoQualityPrioritization = maxPhotoQualityPrioritization {
193
176
  photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
194
177
  }
195
178
  }
179
+
196
180
  if session.canAddOutput(photoOutput) {
197
181
  session.addOutput(photoOutput)
198
- // Connection only exists after the output is added to the session.
199
- if let photoOutputConnection = self.photoOutput.connection(with: .video),
200
- photoOutputConnection.isVideoStabilizationSupported {
201
- photoOutputConnection.preferredVideoStabilizationMode = .auto
182
+
183
+ if let photoOutputConnection = self.photoOutput.connection(with: .video) {
184
+ if photoOutputConnection.isVideoStabilizationSupported {
185
+ photoOutputConnection.preferredVideoStabilizationMode = .auto
186
+ }
202
187
  }
203
188
  }
204
-
205
- // 4. Metadata output for barcode scanning
189
+
190
+ // Add metadata output for barcode scanning
206
191
  if self.session.canAddOutput(metadataOutput) {
207
192
  self.session.addOutput(metadataOutput)
208
193
  metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
209
-
194
+
210
195
  let availableTypes = self.metadataOutput.availableMetadataObjectTypes
211
196
  let filteredTypes = supportedBarcodeType
212
197
  .map { $0.toAVMetadataObjectType() }
@@ -214,49 +199,29 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
214
199
 
215
200
  metadataOutput.metadataObjectTypes = filteredTypes
216
201
  }
217
-
218
- // 5. Video data output for text / MRZ detection
219
- if textRequest != nil && self.session.canAddOutput(self.videoDataOutput) {
202
+
203
+ // add for text detections
204
+ if (textRequest != nil && self.session.canAddOutput(self.videoDataOutput)) {
220
205
  self.session.addOutput(self.videoDataOutput)
221
206
  }
222
-
223
- return .success
224
207
  }
225
208
 
226
209
  // MARK: - Pause / Resume non-essential outputs
227
-
228
- /// Disables OCR + barcode work while a photo is captured.
229
- /// MUST be called on `sessionQueue` (these are capture-output mutations).
230
210
  private func pauseNonEssentialOutputs() {
231
- assertOnSessionQueue()
232
211
  videoDataOutput.setSampleBufferDelegate(nil, queue: nil)
233
212
  metadataOutput.rectOfInterest = CGRect(x: 0, y: 0, width: 0, height: 0)
234
213
  }
235
-
236
- /// Re-enables OCR + barcode work after a capture. May be called from any
237
- /// thread (photo-capture completion handlers run on an arbitrary queue), so it
238
- /// hops to the main queue only to read preview-layer geometry and applies all
239
- /// output mutations back on `sessionQueue`.
214
+
240
215
  private func resumeNonEssentialOutputs() {
241
- sessionQueue.async { [weak self] in
242
- guard let self else { return }
243
- if self.textDetectionEnabled {
244
- self.videoDataOutput.setSampleBufferDelegate(self, queue: globalOCRQueue)
245
- }
246
-
247
- guard let scanner = self.scannerFrameSize, scanner != .zero else {
248
- self.metadataOutput.rectOfInterest = CGRect(x: 0, y: 0, width: 1, height: 1)
249
- return
250
- }
251
-
252
- // metadataOutputRectConverted must be read on the main thread.
253
- DispatchQueue.main.async { [weak self] in
254
- guard let self else { return }
255
- let rect = self.cameraPreview.previewLayer.metadataOutputRectConverted(fromLayerRect: scanner)
256
- self.sessionQueue.async { [weak self] in
257
- self?.metadataOutput.rectOfInterest = rect
258
- }
259
- }
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)
260
225
  }
261
226
  }
262
227
 
@@ -396,18 +361,21 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
396
361
  func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
397
362
  guard #available(iOS 13.0, *) else { return }
398
363
  guard maxPhotoQualityPrioritization != self.maxPhotoQualityPrioritization else { return }
399
- sessionQueue.async { [weak self] in
400
- guard let self else { return }
401
- self.beginConfiguration()
402
- defer { self.commitConfiguration() }
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
+ }
403
372
  self.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization
404
373
  self.photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization?.avQualityPrioritization ?? .balanced
405
374
  }
406
375
  }
407
376
 
408
377
  func update(cameraType: CameraType) {
409
- sessionQueue.async { [weak self] in
410
- guard let self else { return }
378
+ sessionQueue.async {
411
379
  if self.videoDeviceInput?.device.position == cameraType.avPosition {
412
380
  return
413
381
  }
@@ -419,10 +387,15 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
419
387
  let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
420
388
  return
421
389
  }
422
-
390
+
423
391
  self.removeObservers()
424
- self.beginConfiguration()
425
- 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
+ }
426
399
 
427
400
  // Remove the existing device input first, since using the front and back camera simultaneously is not supported.
428
401
  self.session.removeInput(currentViewDeviceInput)
@@ -463,6 +436,9 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
463
436
  the main thread and session configuration is done on the session queue.
464
437
  */
465
438
 
439
+ // Pause OCR + barcode before capturing
440
+ self.pauseNonEssentialOutputs()
441
+
466
442
  DispatchQueue.main.async { [weak self] in
467
443
  guard let self = self else {
468
444
  onError("Camera was deallocated")
@@ -504,9 +480,6 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
504
480
  photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
505
481
  }
506
482
 
507
- // Pause OCR + barcode work on the session queue, right before capture.
508
- self.pauseNonEssentialOutputs()
509
-
510
483
  let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
511
484
  if #available(iOS 13.0, *) {
512
485
  settings.photoQualityPrioritization = self.photoOutput.maxPhotoQualityPrioritization
@@ -856,99 +829,39 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
856
829
 
857
830
  print("Capture session runtime error: \(error)")
858
831
 
859
- // Automatically try to restart the session if media services were reset.
860
- // (After a reset the session is NOT running, so the restart is routed
861
- // 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
862
833
  if error.code == .mediaServicesWereReset {
863
- sessionQueue.async { [weak self] in
864
- self?.startSessionIfNeeded()
834
+ sessionQueue.async {
835
+ if self.session.isRunning {
836
+ self.session.startRunning()
837
+ }
865
838
  }
866
839
  }
867
840
  }
868
841
 
869
842
  func startCamera() {
870
- sessionQueue.async { [weak self] in
871
- self?.startSessionIfNeeded()
843
+ self.sessionQueue.async {
844
+ if !self.session.isRunning {
845
+ self.session.startRunning()
846
+ }
872
847
  }
873
848
  }
874
849
 
875
850
  func stopCamera() {
876
- sessionQueue.async { [weak self] in
877
- self?.stopSessionIfNeeded()
878
- }
879
- }
880
-
881
- // MARK: - Session configuration transaction helpers
882
-
883
- /// Debug-only tripwire enforcing the core invariant of this class: EVERY
884
- /// AVCaptureSession mutation and start/stop happens on `sessionQueue`. The whole
885
- /// "startRunning may not be called between beginConfiguration and
886
- /// commitConfiguration" crash class is prevented precisely because nothing
887
- /// touches the session off this serial queue. If a future change — or an
888
- /// external caller such as a mini-app native bridge — ever reaches one of these
889
- /// methods on the wrong queue, this fails loudly in DEBUG/QA instead of
890
- /// crashing randomly in production. Compiled out of release builds, so it can
891
- /// never add a production crash.
892
- private func assertOnSessionQueue() {
893
- #if DEBUG
894
- dispatchPrecondition(condition: .onQueue(sessionQueue))
895
- #endif
896
- }
897
-
898
- /// Opens a session configuration transaction and tracks nesting depth.
899
- /// MUST be balanced by `commitConfiguration()` (use `defer`) and MUST run on
900
- /// `sessionQueue`.
901
- private func beginConfiguration() {
902
- assertOnSessionQueue()
903
- configurationDepth += 1
904
- session.beginConfiguration()
905
- }
906
-
907
- /// Closes a session configuration transaction and flushes a stop that was
908
- /// requested while the transaction was open. MUST run on `sessionQueue`.
909
- private func commitConfiguration() {
910
- assertOnSessionQueue()
911
- session.commitConfiguration()
912
- configurationDepth -= 1
913
- handlePendingStopIfNeeded()
914
- }
915
-
916
- /// Starts the session only when it is safe to do so. MUST run on `sessionQueue`.
917
- /// Because every begin/commit pair is synchronous on the serial session queue,
918
- /// `configurationDepth` is always 0 at the start of a fresh queue block — the
919
- /// depth check is therefore defence-in-depth against ever calling this from
920
- /// inside an open transaction.
921
- private func startSessionIfNeeded() {
922
- assertOnSessionQueue()
923
- guard setupResult == .success,
924
- configurationDepth == 0,
925
- !pendingStop,
926
- !session.isRunning else {
927
- return
928
- }
929
- session.startRunning()
930
- }
931
-
932
- /// Stops the session, deferring until the current transaction commits if one
933
- /// is open. MUST run on `sessionQueue`.
934
- private func stopSessionIfNeeded() {
935
- assertOnSessionQueue()
936
- guard session.isRunning else { return }
937
- if configurationDepth > 0 {
938
- pendingStop = true
939
- return
851
+ self.sessionQueue.async {
852
+ if self.session.isRunning {
853
+ self.session.stopRunning()
854
+ }
940
855
  }
941
- session.stopRunning()
942
856
  }
943
857
 
944
858
  // MARK: - Private helper for safe session stop
945
-
859
+
946
860
  private func handlePendingStopIfNeeded() {
947
861
  // Must be called on sessionQueue after configuration completes
948
- assertOnSessionQueue()
949
862
  // Only execute if all configurations are done (depth == 0)
950
863
  guard configurationDepth == 0 && pendingStop else { return }
951
-
864
+
952
865
  pendingStop = false
953
866
  if session.isRunning {
954
867
  session.stopRunning()
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
1
  {
2
- "name": "@momo-kits/camerakit",
3
- "version": "0.161.1-beta.1",
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.1",
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
+ }