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

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.
@@ -71,6 +71,10 @@ public class CameraView: UIView {
71
71
 
72
72
  // This is used to delay camera setup until we have both granted permission & received default props
73
73
  var hasCameraBeenSetup = false
74
+ // [CKCameraKit][perf] TEMP measurement — remove before publish. Captured when the
75
+ // view is constructed so we can see how long the OUTSIDE path (permission grant +
76
+ // first props) takes before camera.setup() is even called.
77
+ private let viewCreatedAt = CFAbsoluteTimeGetCurrent()
74
78
  var hasPropBeenSetup = false {
75
79
  didSet {
76
80
  setupCamera()
@@ -84,6 +88,8 @@ public class CameraView: UIView {
84
88
  private func setupCamera() {
85
89
  if hasPropBeenSetup && hasPermissionBeenGranted && !hasCameraBeenSetup {
86
90
  hasCameraBeenSetup = true
91
+ // [CKCameraKit][perf] TEMP measurement — remove before publish.
92
+ print(String(format: "[CKCameraKit][perf] view→setupCalled=%.0fms (permission+props before setup)", (CFAbsoluteTimeGetCurrent() - viewCreatedAt) * 1000))
87
93
  #if targetEnvironment(macCatalyst)
88
94
  // Force front camera on Mac Catalyst during initial setup
89
95
  camera.setup(cameraType: .front, supportedBarcodeType: scanBarcode && onReadCode != nil ? supportedBarcodeType : [])
@@ -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 }
@@ -114,8 +131,11 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
114
131
  // MARK: - Public
115
132
 
116
133
  func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
134
+ // [CKCameraKit][perf] TEMP measurement — remove before publish.
135
+ let tCall = CFAbsoluteTimeGetCurrent()
117
136
  sessionQueue.async { [weak self] in
118
137
  guard let self else { return }
138
+ let tBlock = CFAbsoluteTimeGetCurrent()
119
139
 
120
140
  // Idempotency guard: a RealCamera configures its session exactly once.
121
141
  // A fresh instance is created on every mount / Fabric recycle, so this
@@ -123,38 +143,41 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
123
143
  // handled by startCamera() -> startSessionIfNeeded().
124
144
  guard self.setupResult == .notStarted else { return }
125
145
 
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)
146
+ // Configure the WHOLE session (inputs, preset, outputs) inside a single
147
+ // begin/commit transaction on the session queue. The preset is set here
148
+ // and never on the main queue, so the startRunning() below can no longer
149
+ // observe the session mid-configuration from another thread.
150
+ //
151
+ // This is the root-cause fix for:
152
+ // "[AVCaptureSession startRunning] startRunning may not be called between
153
+ // calls to beginConfiguration and commitConfiguration".
154
+ self.setupResult = self.configureSession(cameraType: cameraType,
155
+ supportedBarcodeType: supportedBarcodeType)
156
+ let tConfigured = CFAbsoluteTimeGetCurrent()
157
+
133
158
  guard self.setupResult == .success else { return }
134
159
 
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().
160
+ // Set the preview orientation on the main queue NOW BEFORE the (~145ms)
161
+ // startRunning below so the very first frame renders at the correct
162
+ // orientation instead of appearing once and then snapping ("giật 1 nhịp").
163
+ // The preview's `.session` and gravity are already wired at init; this is a
164
+ // pure connection-property write (no session config transaction no race
165
+ // with startRunning). The connection exists because configureSession has
166
+ // committed the video input above.
140
167
  DispatchQueue.main.async { [weak self] in
141
168
  guard let self else { return }
142
- self.cameraPreview.session = self.session
143
- self.cameraPreview.previewLayer.videoGravity = .resizeAspectFill
144
169
  self.setVideoOrientationToInterfaceOrientation()
145
170
  }
146
171
 
147
- // Start running with just the preview-friendly config fast first frame.
172
+ // Order matches the previous behaviour: start, then observe, then torch.
148
173
  self.startSessionIfNeeded()
149
-
150
- // PHASE 2add 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
-
174
+ let tStarted = CFAbsoluteTimeGetCurrent()
175
+ // [CKCameraKit][perf] TEMP measurement remove before publish.
176
+ print(String(format: "[CKCameraKit][perf] queueWait=%.0fms configure=%.0fms startRunning=%.0fms setupTotal=%.0fms",
177
+ (tBlock - tCall) * 1000,
178
+ (tConfigured - tBlock) * 1000,
179
+ (tStarted - tConfigured) * 1000,
180
+ (tStarted - tCall) * 1000))
158
181
  self.addObservers()
159
182
  self.update(torchMode: self.torchMode)
160
183
  }
@@ -166,11 +189,10 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
166
189
 
167
190
  // MARK: - Private optimization methods
168
191
 
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 {
192
+ /// Configures the video input, session preset and every output in ONE atomic
193
+ /// transaction. Must be called on `sessionQueue`. Order is significant:
194
+ /// beginConfiguration -> addInput -> set preset -> addOutputs -> commitConfiguration.
195
+ private func configureSession(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) -> SetupResult {
174
196
  assertOnSessionQueue()
175
197
  guard let videoDevice = self.getBestDevice(for: cameraType),
176
198
  let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice) else {
@@ -180,7 +202,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
180
202
  beginConfiguration()
181
203
  defer { commitConfiguration() }
182
204
 
183
- // Video input
205
+ // 1. Video input
184
206
  guard session.canAddInput(videoDeviceInput) else {
185
207
  return .sessionConfigurationFailed
186
208
  }
@@ -188,33 +210,15 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
188
210
  self.videoDeviceInput = videoDeviceInput
189
211
  self.resetZoom(forDevice: videoDevice)
190
212
 
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.
213
+ // 2. Preset set inside the transaction with the device input already
214
+ // present, so `canSetSessionPreset` reflects the real device. `.photo` is
215
+ // supported by every camera device; if it somehow is not, we keep whatever
216
+ // preset the session defaults to rather than crashing.
213
217
  if session.canSetSessionPreset(.photo) {
214
218
  session.sessionPreset = .photo
215
219
  }
216
220
 
217
- // Photo output
221
+ // 3. Photo output
218
222
  if #available(iOS 13.0, *) {
219
223
  if let maxPhotoQualityPrioritization = maxPhotoQualityPrioritization {
220
224
  photoOutput.maxPhotoQualityPrioritization = maxPhotoQualityPrioritization.avQualityPrioritization
@@ -229,7 +233,7 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
229
233
  }
230
234
  }
231
235
 
232
- // Metadata output for barcode scanning
236
+ // 4. Metadata output for barcode scanning
233
237
  if self.session.canAddOutput(metadataOutput) {
234
238
  self.session.addOutput(metadataOutput)
235
239
  metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
@@ -242,10 +246,12 @@ class RealCamera: NSObject, CameraProtocol, AVCaptureMetadataOutputObjectsDelega
242
246
  metadataOutput.metadataObjectTypes = filteredTypes
243
247
  }
244
248
 
245
- // Video data output for text / MRZ detection
249
+ // 5. Video data output for text / MRZ detection
246
250
  if textRequest != nil && self.session.canAddOutput(self.videoDataOutput) {
247
251
  self.session.addOutput(self.videoDataOutput)
248
252
  }
253
+
254
+ return .success
249
255
  }
250
256
 
251
257
  // 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.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/teslamotors/react-native-camera-kit.git"