@momo-kits/camerakit 0.161.2-beta.25 → 0.161.2-beta.27

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.
@@ -72,6 +72,23 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
72
72
 
73
73
  // MARK: - Lifecycle
74
74
 
75
+ override init() {
76
+ super.init()
77
+ #if DEBUG
78
+ assert(Thread.isMainThread, "RealCamera must be initialized on the main thread (it wires the preview CALayer).")
79
+ #endif
80
+ // Wire the preview to the (idle, never-started) session AND set its gravity
81
+ // ONCE here at init, on the main thread, before anything starts. Benefits:
82
+ // (a) the preview renders the instant the first frame arrives (~camera warm-up)
83
+ // instead of waiting for a late post-setup attach → less black.
84
+ // (b) assigning previewLayer.session opens an implicit AVFoundation config
85
+ // transaction; doing it now (session idle, no startRunning yet, never
86
+ // reassigned) makes it impossible to race startRunning() → the begin/commit
87
+ // crash class is closed by construction, on every device.
88
+ cameraPreview.session = session
89
+ cameraPreview.previewLayer.videoGravity = .resizeAspectFill
90
+ }
91
+
75
92
  func cameraRemovedFromSuperview() {
76
93
  sessionQueue.async { [weak self] in
77
94
  guard let self, self.setupResult == .success else { return }
@@ -123,38 +140,33 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
123
140
  // handled by startCamera() -> startSessionIfNeeded().
124
141
  guard self.setupResult == .notStarted else { return }
125
142
 
126
- // PHASE 1 minimal config so the preview renders ASAP. Only the video
127
- // input + a lightweight (.high) preview preset go in this first
128
- // transaction. The heavier work (.photo preset + photo / barcode / OCR
129
- // outputs) is deferred to phase 2 below, AFTER the session is already
130
- // running and the preview layer is attached. This restores the
131
- // "camera shows up instantly" feel without weakening the crash fix.
132
- self.setupResult = self.configureBaseSession(cameraType: cameraType)
143
+ // Configure the WHOLE session (inputs, preset, outputs) inside a single
144
+ // begin/commit transaction on the session queue. The preset is set here
145
+ // and never on the main queue, so the startRunning() below can no longer
146
+ // observe the session mid-configuration from another thread.
147
+ //
148
+ // This is the root-cause fix for:
149
+ // "[AVCaptureSession startRunning] startRunning may not be called between
150
+ // calls to beginConfiguration and commitConfiguration".
151
+ self.setupResult = self.configureSession(cameraType: cameraType,
152
+ supportedBarcodeType: supportedBarcodeType)
153
+
133
154
  guard self.setupResult == .success else { return }
134
155
 
135
- // Attach the preview LAYER on the main queue BEFORE startRunning so the
136
- // first frames render the moment the session starts. Connecting an
137
- // AVCaptureVideoPreviewLayer to a session from the main thread is
138
- // sanctioned by AVFoundation (see Apple's AVCam), does NOT open a
139
- // session configuration transaction, and cannot race startRunning().
156
+ // Set the preview orientation on the main queue NOW BEFORE the (~145ms)
157
+ // startRunning below so the very first frame renders at the correct
158
+ // orientation instead of appearing once and then snapping ("giật 1 nhịp").
159
+ // The preview's `.session` and gravity are already wired at init; this is a
160
+ // pure connection-property write (no session config transaction no race
161
+ // with startRunning). The connection exists because configureSession has
162
+ // committed the video input above.
140
163
  DispatchQueue.main.async { [weak self] in
141
164
  guard let self else { return }
142
- self.cameraPreview.session = self.session
143
- self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill
144
165
  self.setVideoOrientationToInterfaceOrientation()
145
166
  }
146
167
 
147
- // Start running with just the preview-friendly config fast first frame.
168
+ // Order matches the previous behaviour: start, then observe, then torch.
148
169
  self.startSessionIfNeeded()
149
-
150
- // PHASE 2 — add the heavier outputs and upgrade to the .photo preset in a
151
- // SECOND transaction on this same serial queue, now that preview is live.
152
- // Adding outputs to an already-running session inside begin/commit is
153
- // legal; because startRunning() is NEVER called while configurationDepth
154
- // > 0 (see startSessionIfNeeded), the "startRunning may not be called
155
- // between beginConfiguration and commitConfiguration" crash stays fixed.
156
- self.configureAdditionalOutputs(supportedBarcodeType: supportedBarcodeType)
157
-
158
170
  self.addObservers()
159
171
  self.update(torchMode: self.torchMode)
160
172
  }
@@ -166,11 +178,10 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
166
178
 
167
179
  // MARK: - Private optimization methods
168
180
 
169
- /// Phase 1 of setup: video input + a lightweight (.high) preview preset, in ONE
170
- /// transaction. Must run on `sessionQueue`. Kept deliberately minimal so
171
- /// `startRunning()` and therefore the first preview frame is not delayed by
172
- /// wiring up the photo / barcode / OCR outputs (that is `configureAdditionalOutputs`).
173
- private func configureBaseSession(cameraType: CameraType) -> SetupResult {
181
+ /// Configures the video input, session preset and every output in ONE atomic
182
+ /// transaction. Must be called on `sessionQueue`. Order is significant:
183
+ /// beginConfiguration -> addInput -> set preset -> addOutputs -> commitConfiguration.
184
+ private func configureSession(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) -> SetupResult {
174
185
  assertOnSessionQueue()
175
186
  guard let videoDevice = self.getBestDevice(for: cameraType),
176
187
  let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
@@ -180,7 +191,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
180
191
  beginConfiguration()
181
192
  defer { commitConfiguration() }
182
193
 
183
- // Video input
194
+ // 1. Video input
184
195
  guard session.canAddInput(videoDeviceInput) else {
185
196
  return .sessionConfigurationFailed
186
197
  }
@@ -188,33 +199,15 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
188
199
  self.videoDeviceInput = videoDeviceInput
189
200
  self.resetZoom(forDevice: videoDevice)
190
201
 
191
- // Lightweight preset for the fastest possible first frame. Upgraded to
192
- // `.photo` in configureAdditionalOutputs() once the preview is live.
193
- if session.canSetSessionPreset(.high) {
194
- session.sessionPreset = .high
195
- }
196
-
197
- return .success
198
- }
199
-
200
- /// Phase 2 of setup: the .photo preset + photo / barcode / OCR outputs, in ONE
201
- /// transaction on `sessionQueue`. Runs AFTER the session is already running and
202
- /// the preview is attached, so none of this work delays the first frame. Adding
203
- /// outputs to a running session inside begin/commit is legal; the crash fix is
204
- /// preserved because startRunning() is never called while a transaction is open.
205
- private func configureAdditionalOutputs(supportedBarcodeType: [CodeFormat]) {
206
- assertOnSessionQueue()
207
- beginConfiguration()
208
- defer { commitConfiguration() }
209
-
210
- // Upgrade to the full still-capture preset now that preview is rendering.
211
- // `.photo` is supported by every camera device; if it somehow is not, we
212
- // keep the `.high` preset from phase 1 rather than crashing.
202
+ // 2. Preset set inside the transaction with the device input already
203
+ // present, so `canSetSessionPreset` reflects the real device. `.photo` is
204
+ // supported by every camera device; if it somehow is not, we keep whatever
205
+ // preset the session defaults to rather than crashing.
213
206
  if session.canSetSessionPreset(.photo) {
214
207
  session.sessionPreset = .photo
215
208
  }
216
209
 
217
- // Photo output
210
+ // 3. Photo output
218
211
  if #available(iOS 13.0, *) {
219
212
  if let maxPhotoQualityPrioritization = maxPhotoQualityPrioritization {
220
213
  photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
@@ -229,7 +222,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
229
222
  }
230
223
  }
231
224
 
232
- // Metadata output for barcode scanning
225
+ // 4. Metadata output for barcode scanning
233
226
  if self.session.canAddOutput(metadataOutput) {
234
227
  self.session.addOutput(metadataOutput)
235
228
  metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
@@ -242,10 +235,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
242
235
  metadataOutput.metadataObjectTypes = filteredTypes
243
236
  }
244
237
 
245
- // Video data output for text / MRZ detection
238
+ // 5. Video data output for text / MRZ detection
246
239
  if textRequest != nil && self.session.canAddOutput(self.videoDataOutput) {
247
240
  self.session.addOutput(self.videoDataOutput)
248
241
  }
242
+
243
+ return .success
249
244
  }
250
245
 
251
246
  // MARK: - Pause / Resume non-essential outputs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/camerakit",
3
- "version": "0.161.2-beta.25",
3
+ "version": "0.161.2-beta.27",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/teslamotors/react-native-camera-kit.git"