@rejourneyco/react-native 1.0.1 → 1.0.2

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.
Files changed (43) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +35 -29
  2. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
  3. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
  4. package/ios/Capture/RJCaptureEngine.m +3 -34
  5. package/ios/Capture/RJVideoEncoder.m +0 -26
  6. package/ios/Core/Rejourney.mm +32 -95
  7. package/ios/Utils/RJPerfTiming.m +0 -5
  8. package/ios/Utils/RJWindowUtils.m +0 -1
  9. package/lib/commonjs/components/Mask.js +1 -6
  10. package/lib/commonjs/index.js +4 -87
  11. package/lib/commonjs/sdk/autoTracking.js +39 -310
  12. package/lib/commonjs/sdk/constants.js +2 -13
  13. package/lib/commonjs/sdk/errorTracking.js +1 -29
  14. package/lib/commonjs/sdk/metricsTracking.js +3 -24
  15. package/lib/commonjs/sdk/navigation.js +3 -42
  16. package/lib/commonjs/sdk/networkInterceptor.js +7 -49
  17. package/lib/commonjs/sdk/utils.js +0 -5
  18. package/lib/module/components/Mask.js +1 -6
  19. package/lib/module/index.js +3 -91
  20. package/lib/module/sdk/autoTracking.js +39 -311
  21. package/lib/module/sdk/constants.js +2 -13
  22. package/lib/module/sdk/errorTracking.js +1 -29
  23. package/lib/module/sdk/index.js +0 -2
  24. package/lib/module/sdk/metricsTracking.js +3 -24
  25. package/lib/module/sdk/navigation.js +3 -42
  26. package/lib/module/sdk/networkInterceptor.js +7 -49
  27. package/lib/module/sdk/utils.js +0 -5
  28. package/lib/typescript/NativeRejourney.d.ts +1 -0
  29. package/lib/typescript/sdk/autoTracking.d.ts +4 -4
  30. package/lib/typescript/types/index.d.ts +0 -1
  31. package/package.json +2 -8
  32. package/src/NativeRejourney.ts +2 -0
  33. package/src/components/Mask.tsx +0 -3
  34. package/src/index.ts +3 -73
  35. package/src/sdk/autoTracking.ts +51 -282
  36. package/src/sdk/constants.ts +13 -13
  37. package/src/sdk/errorTracking.ts +1 -17
  38. package/src/sdk/index.ts +0 -2
  39. package/src/sdk/metricsTracking.ts +5 -33
  40. package/src/sdk/navigation.ts +8 -29
  41. package/src/sdk/networkInterceptor.ts +9 -33
  42. package/src/sdk/utils.ts +0 -5
  43. package/src/types/index.ts +0 -29
@@ -57,7 +57,6 @@ import java.io.File
57
57
  import java.util.*
58
58
  import java.util.concurrent.CopyOnWriteArrayList
59
59
 
60
- // Enum for session end reasons
61
60
  enum class EndReason {
62
61
  SESSION_TIMEOUT,
63
62
  MANUAL_STOP,
@@ -107,7 +106,6 @@ class RejourneyModuleImpl(
107
106
  }
108
107
  }
109
108
 
110
- // Core components
111
109
  private var captureEngine: CaptureEngine? = null
112
110
  private var uploadManager: UploadManager? = null
113
111
  private var touchInterceptor: TouchInterceptor? = null
@@ -116,7 +114,6 @@ class RejourneyModuleImpl(
116
114
  private var keyboardTracker: KeyboardTracker? = null
117
115
  private var textInputTracker: TextInputTracker? = null
118
116
 
119
- // Session state
120
117
  private var currentSessionId: String? = null
121
118
  private var userId: String? = null
122
119
  @Volatile private var isRecording: Boolean = false
@@ -134,59 +131,43 @@ class RejourneyModuleImpl(
134
131
  private var maxRecordingMinutes: Int = 10
135
132
  @Volatile private var sessionEndSent: Boolean = false
136
133
 
137
- // Keyboard state
138
134
  private var keyPressCount: Int = 0
139
135
  private var isKeyboardVisible: Boolean = false
140
136
  private var lastKeyboardHeight: Int = 0
141
137
 
142
- // Session state saved on background - used to restore on foreground if within timeout
143
138
  private var savedApiUrl: String = ""
144
139
  private var savedPublicKey: String = ""
145
140
  private var savedDeviceHash: String = ""
146
141
 
147
- // Events buffer
148
142
  private val sessionEvents = CopyOnWriteArrayList<Map<String, Any?>>()
149
143
 
150
- // Throttle immediate upload kicks (ms)
151
144
  @Volatile private var lastImmediateUploadKickMs: Long = 0
152
145
 
153
- // Write-first event buffer for crash-safe persistence
154
146
  private var eventBuffer: EventBuffer? = null
155
147
 
156
- // Coroutine scope for async operations
157
148
  private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
158
149
 
159
- // Dedicated scope for background flush - survives independently of main scope
160
- // This prevents cancellation when app goes to background
161
150
  private val backgroundScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
162
151
 
163
- // Timer jobs
164
152
  private var batchUploadJob: Job? = null
165
153
  private var durationLimitJob: Job? = null
166
154
 
167
- // Main thread handler for posting delayed tasks
168
155
  private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
169
156
 
170
- // Debounced background detection (prevents transient pauses from ending sessions)
171
157
  private var scheduledBackgroundRunnable: Runnable? = null
172
158
  private var backgroundScheduled: Boolean = false
173
159
 
174
- // Safety flag
175
160
  @Volatile private var isShuttingDown = false
176
161
 
177
- // Auth resilience - retry mechanism
178
162
  private var authRetryCount = 0
179
163
  private var authPermanentlyFailed = false
180
164
  private var authRetryJob: Job? = null
181
165
 
182
166
  init {
183
167
  // DO NOT initialize anything here that could throw exceptions
184
- // React Native needs the module constructor to complete cleanly
185
- // All initialization will happen lazily on first method call
186
168
  Logger.debug("RejourneyModuleImpl constructor completed")
187
169
  }
188
170
 
189
- // Lazy initialization flag
190
171
  @Volatile
191
172
  private var isInitialized = false
192
173
  private val initLock = Any()
@@ -237,21 +218,19 @@ class RejourneyModuleImpl(
237
218
  Logger.error("Failed to schedule recovery upload (non-critical)", e)
238
219
  }
239
220
 
240
- // Check if app was killed in previous session (Android 11+)
221
+ // Che ck if app was killed in previous session (Android 11+)
241
222
  try {
242
223
  checkPreviousAppKill()
243
224
  } catch (e: Exception) {
244
225
  Logger.error("Failed to check previous app kill (non-critical)", e)
245
226
  }
246
227
 
247
- // Check for unclosed sessions from previous launch
248
228
  try {
249
229
  checkForUnclosedSessions()
250
230
  } catch (e: Exception) {
251
231
  Logger.error("Failed to check for unclosed sessions (non-critical)", e)
252
232
  }
253
233
 
254
- // Log OEM information for debugging
255
234
  val oem = OEMDetector.getOEM()
256
235
  Logger.debug("Device OEM: $oem")
257
236
  Logger.debug("OEM Recommendations: ${OEMDetector.getRecommendations()}")
@@ -291,7 +270,6 @@ class RejourneyModuleImpl(
291
270
  Logger.error("Failed to set up task removed listener (non-critical)", e)
292
271
  }
293
272
 
294
- // Use lifecycle log - only shown in debug builds
295
273
  Logger.logInitSuccess(Constants.SDK_VERSION)
296
274
 
297
275
  isInitialized = true
@@ -313,8 +291,6 @@ class RejourneyModuleImpl(
313
291
 
314
292
  Logger.debug("[Rejourney] addEventWithPersistence: type=$eventType, sessionId=$sessionId, inMemoryCount=${sessionEvents.size + 1}")
315
293
 
316
- // CRITICAL: Write to disk immediately for crash safety
317
- // This ensures events are never lost even if app is force-killed
318
294
  val bufferSuccess = eventBuffer?.appendEvent(event) ?: false
319
295
  if (!bufferSuccess) {
320
296
  Logger.warning("[Rejourney] addEventWithPersistence: Failed to append event to buffer: type=$eventType")
@@ -322,7 +298,6 @@ class RejourneyModuleImpl(
322
298
  Logger.debug("[Rejourney] addEventWithPersistence: Event appended to buffer: type=$eventType")
323
299
  }
324
300
 
325
- // Also add to in-memory buffer for batched upload
326
301
  sessionEvents.add(event)
327
302
  Logger.debug("[Rejourney] addEventWithPersistence: Event added to in-memory list: type=$eventType, totalInMemory=${sessionEvents.size}")
328
303
  }
@@ -332,14 +307,12 @@ class RejourneyModuleImpl(
332
307
  * This is more reliable than Activity lifecycle callbacks.
333
308
  */
334
309
  private fun registerProcessLifecycleObserver() {
335
- // Must run on main thread
336
310
  Handler(Looper.getMainLooper()).post {
337
311
  try {
338
312
  ProcessLifecycleOwner.get().lifecycle.addObserver(this)
339
313
  Logger.debug("ProcessLifecycleOwner observer registered")
340
314
  } catch (e: Exception) {
341
315
  Logger.error("Failed to register ProcessLifecycleOwner observer (non-critical)", e)
342
- // This is non-critical - we can still use Activity lifecycle callbacks as fallback
343
316
  }
344
317
  }
345
318
  }
@@ -472,7 +445,40 @@ class RejourneyModuleImpl(
472
445
  }
473
446
  }
474
447
 
475
- // ==================== React Native Methods ====================
448
+ fun getDeviceInfo(promise: Promise) {
449
+ try {
450
+ val map = Arguments.createMap()
451
+ map.putString("model", android.os.Build.MODEL)
452
+ map.putString("brand", android.os.Build.MANUFACTURER)
453
+ map.putString("systemName", "Android")
454
+ map.putString("systemVersion", android.os.Build.VERSION.RELEASE)
455
+ map.putString("bundleId", reactContext.packageName)
456
+
457
+ try {
458
+ val pInfo = reactContext.packageManager.getPackageInfo(reactContext.packageName, 0)
459
+ map.putString("appVersion", pInfo.versionName)
460
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
461
+ map.putString("buildNumber", pInfo.longVersionCode.toString())
462
+ } else {
463
+ @Suppress("DEPRECATION")
464
+ map.putString("buildNumber", pInfo.versionCode.toString())
465
+ }
466
+ } catch (e: Exception) {
467
+ map.putString("appVersion", "unknown")
468
+ }
469
+
470
+ map.putBoolean("isTablet", isTablet())
471
+ promise.resolve(map)
472
+ } catch (e: Exception) {
473
+ promise.reject("DEVICE_INFO_ERROR", e)
474
+ }
475
+ }
476
+
477
+ private fun isTablet(): Boolean {
478
+ val configuration = reactContext.resources.configuration
479
+ return (configuration.screenLayout and android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK) >=
480
+ android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
481
+ }
476
482
 
477
483
  fun startSession(userId: String, apiUrl: String, publicKey: String, promise: Promise) {
478
484
  ensureInitialized() // Lazy init on first call
@@ -132,6 +132,13 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
132
132
  instance.getSDKMetrics(promise)
133
133
  }
134
134
 
135
+ @ReactMethod
136
+ @DoNotStrip
137
+ override fun getDeviceInfo(promise: Promise) {
138
+ val instance = getImplOrReject(promise) ?: return
139
+ instance.getDeviceInfo(promise)
140
+ }
141
+
135
142
  @ReactMethod
136
143
  @DoNotStrip
137
144
  override fun debugCrash() {
@@ -128,6 +128,15 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
128
128
  }
129
129
  }
130
130
 
131
+ @ReactMethod
132
+ fun getDeviceInfo(promise: Promise) {
133
+ try {
134
+ impl.getDeviceInfo(promise)
135
+ } catch (e: Exception) {
136
+ promise.resolve(Arguments.createMap())
137
+ }
138
+ }
139
+
131
140
  @ReactMethod
132
141
  fun debugCrash() {
133
142
  try {
@@ -1155,9 +1155,6 @@ typedef struct {
1155
1155
  NSTimeInterval now = CACurrentMediaTime();
1156
1156
 
1157
1157
  if (self.pendingCapture) {
1158
- // If we have a pending capture, force it to resolve now.
1159
- // If the new request is High importance, we effectively "upgrade" the
1160
- // current cycle by ensuring it runs immediately.
1161
1158
  self.pendingCapture.deadline = now;
1162
1159
  [self attemptPendingCapture:self.pendingCapture fullScan:NO];
1163
1160
  }
@@ -1173,8 +1170,6 @@ typedef struct {
1173
1170
  grace = MIN(grace, 0.3);
1174
1171
  }
1175
1172
 
1176
- // High importance requests should have a shorter grace, forcing faster
1177
- // resolution
1178
1173
  if (isCritical) {
1179
1174
  grace = MIN(grace, 0.1);
1180
1175
  }
@@ -1731,7 +1726,6 @@ typedef struct {
1731
1726
  }
1732
1727
 
1733
1728
  if (self.internalPerformanceLevel == RJPerformanceLevelMinimal) {
1734
- // Minimal mode trades quality for speed
1735
1729
  CGContextSetInterpolationQuality(context, kCGInterpolationNone);
1736
1730
  CGContextSetShouldAntialias(context, false);
1737
1731
  CGContextSetAllowsAntialiasing(context, false);
@@ -1741,17 +1735,13 @@ typedef struct {
1741
1735
  CGContextSetAllowsAntialiasing(context, true);
1742
1736
  }
1743
1737
 
1744
- // Set up context transform (flip for UIKit coordinates)
1745
1738
  CGContextScaleCTM(context, contextScale, -contextScale);
1746
1739
  CGContextTranslateCTM(context, 0, -sizePoints.height);
1747
1740
 
1748
- // Optimization #9: Fast Memset Clear (White = 0xFF)
1749
- // Much faster than CGContextFillRect
1750
1741
  memset(baseAddress, 0xFF, bytesPerRow * height);
1751
1742
 
1752
1743
  UIGraphicsPushContext(context);
1753
1744
 
1754
- // ===== RENDERING: ALWAYS USE drawViewHierarchyInRect =====
1755
1745
  RJ_TIME_START_NAMED(render);
1756
1746
  BOOL didDraw = NO;
1757
1747
  @try {
@@ -1772,10 +1762,6 @@ typedef struct {
1772
1762
  return NULL;
1773
1763
  }
1774
1764
 
1775
- // Recalculate effective scale so consumers (PrivacyMask) know the real
1776
- // mapping Used by caller to pass to applyToPixelBuffer Note: we don't need to
1777
- // return it, caller has it.
1778
-
1779
1765
  return pixelBuffer;
1780
1766
  }
1781
1767
 
@@ -1815,9 +1801,6 @@ typedef struct {
1815
1801
  }
1816
1802
 
1817
1803
  - (NSTimeInterval)currentTimestamp {
1818
- // Always use wall clock time for session timestamps
1819
- // CACurrentMediaTime optimization removed - it causes drift after
1820
- // background periods The ~1ms overhead is acceptable for 1fps capture
1821
1804
  return [[NSDate date] timeIntervalSince1970] * 1000.0;
1822
1805
  }
1823
1806
 
@@ -1829,8 +1812,6 @@ typedef struct {
1829
1812
  endTime:(NSTimeInterval)endTime
1830
1813
  frameCount:(NSInteger)frameCount {
1831
1814
 
1832
- // Ensure we are on our own encoding queue to protect hierarchySnapshots
1833
- // and maintain thread safety (callback comes from VideoEncoder queue)
1834
1815
  dispatch_async(self.encodingQueue, ^{
1835
1816
  RJLogDebug(@"CaptureEngine: videoEncoderDidFinishSegment: %@ (%ld frames, "
1836
1817
  @"%.1fs), sessionId=%@",
@@ -1872,9 +1853,6 @@ typedef struct {
1872
1853
 
1873
1854
  [self uploadCurrentHierarchySnapshots];
1874
1855
 
1875
- // NUCLEAR FIX: Do NOT call startSegmentWithSize here!
1876
- // The encoder's appendFrame method will auto-start a segment with the
1877
- // correct PIXEL dimensions when the next frame is captured.
1878
1856
  if (self.internalIsRecording && !self.isShuttingDown) {
1879
1857
  RJLogDebug(
1880
1858
  @"CaptureEngine: Segment finished, auto-start new on next frame");
@@ -1912,10 +1890,8 @@ typedef struct {
1912
1890
 
1913
1891
  RJLogInfo(@"CaptureEngine: Pausing video capture (sync=%d)", synchronous);
1914
1892
 
1915
- // Reset capture-in-progress flag immediately to prevent stuck state
1916
1893
  self.captureInProgress = NO;
1917
1894
 
1918
- // Invalidate timer synchronously if in sync mode
1919
1895
  if (synchronous) {
1920
1896
  [self teardownDisplayLink];
1921
1897
  } else {
@@ -1925,7 +1901,7 @@ typedef struct {
1925
1901
  }
1926
1902
 
1927
1903
  if (self.internalVideoEncoder) {
1928
- self.internalIsRecording = NO; // prevent any new frames from being enqueued
1904
+ self.internalIsRecording = NO;
1929
1905
 
1930
1906
  if (synchronous) {
1931
1907
  void (^finishSync)(void) = ^{
@@ -1964,19 +1940,15 @@ typedef struct {
1964
1940
  return;
1965
1941
  }
1966
1942
 
1967
- // Set recording back to YES to allow captureVideoFrame to proceed
1968
1943
  self.internalIsRecording = YES;
1969
1944
 
1970
1945
  RJLogInfo(@"CaptureEngine: Resuming video capture");
1971
1946
 
1972
- // Reset capture state to ensure clean resumption
1973
- // These flags may have been left in an inconsistent state when going to
1974
- // background
1975
1947
  self.captureInProgress = NO;
1976
- self.lastIntentTime = 0; // Allow immediate capture on resume
1948
+ self.lastIntentTime = 0;
1977
1949
 
1978
1950
  self.internalPerformanceLevel =
1979
- RJPerformanceLevelNormal; // Reset perf level on resume
1951
+ RJPerformanceLevelNormal;
1980
1952
 
1981
1953
  self.pendingCapture = nil;
1982
1954
  self.pendingCaptureGeneration = 0;
@@ -1995,7 +1967,6 @@ typedef struct {
1995
1967
  if (window && self.internalVideoEncoder) {
1996
1968
  RJLogInfo(@"CaptureEngine: Resuming capture...");
1997
1969
 
1998
- // Use the optimized Display Link
1999
1970
  [self setupDisplayLink];
2000
1971
 
2001
1972
  } else {
@@ -2012,14 +1983,12 @@ typedef struct {
2012
1983
  if (!self.internalIsRecording)
2013
1984
  return;
2014
1985
 
2015
- // Force update if screen changed
2016
1986
  if (![screenName isEqualToString:self.currentScreenName]) {
2017
1987
  NSTimeInterval now = CACurrentMediaTime();
2018
1988
  self.currentScreenName = screenName;
2019
1989
  RJLogDebug(@"Navigation to screen: %@ (forcing layout refresh)",
2020
1990
  screenName);
2021
1991
 
2022
- // Force layout change detection on next frame
2023
1992
  [self.captureHeuristics invalidateSignature];
2024
1993
  [self.captureHeuristics recordNavigationEventAtTime:now];
2025
1994
  self.lastSerializedSignature = nil;
@@ -520,16 +520,7 @@
520
520
  }
521
521
 
522
522
  - (void)cleanup {
523
- // Only cancel the current in-progress segment, don't delete the entire temp
524
- // directory. Completed segments may still be uploading and must not be
525
- // deleted here. The RJSegmentUploader handles file cleanup after successful
526
- // upload.
527
523
  [self cancelSegment];
528
-
529
- // NOTE: Do NOT delete rj_segments directory here!
530
- // Other segments may be in the middle of uploading.
531
- // Old orphaned segments are cleaned up by
532
- // RJSegmentUploader.cleanupOrphanedSegments()
533
524
  }
534
525
 
535
526
  #pragma mark - Private Methods
@@ -561,14 +552,9 @@
561
552
  size_t width = (size_t)self.currentFrameSize.width;
562
553
  size_t height = (size_t)self.currentFrameSize.height;
563
554
 
564
- // CRITICAL FIX: Validate incoming image dimensions match expected size
565
- // During keyboard/rotation transitions, image size may temporarily differ
566
- // from currentFrameSize, causing CGBitmapContextCreate bytesPerRow mismatch
567
555
  size_t imageWidth = CGImageGetWidth(cgImage);
568
556
  size_t imageHeight = CGImageGetHeight(cgImage);
569
557
 
570
- // Allow small variance (1-2 pixels) due to rounding, but reject major
571
- // mismatches
572
558
  if (labs((long)imageWidth - (long)width) > 2 ||
573
559
  labs((long)imageHeight - (long)height) > 2) {
574
560
  RJLogDebug(@"Video encoder: Skipping frame - size mismatch (got %zux%zu, "
@@ -639,9 +625,6 @@
639
625
  colorSpace = CGColorSpaceCreateDeviceRGB();
640
626
  }
641
627
 
642
- // CRITICAL: Validate bytesPerRow is sufficient for the target width
643
- // Error "CGBitmapContextCreate: invalid data bytes/row" occurs when
644
- // bytesPerRow < width * 4 (4 bytes per pixel for BGRA)
645
628
  size_t requiredBytesPerRow = width * 4;
646
629
  if (bytesPerRow < requiredBytesPerRow) {
647
630
  CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
@@ -663,7 +646,6 @@
663
646
  return NULL;
664
647
  }
665
648
 
666
- // Use fastest interpolation for pixel buffer drawing
667
649
  CGContextSetInterpolationQuality(context, kCGInterpolationNone);
668
650
  CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
669
651
  CGContextRelease(context);
@@ -681,14 +663,10 @@
681
663
  }
682
664
 
683
665
  - (void)prewarmPixelBufferPool {
684
- // Pre-warm the VideoToolbox H.264 encoder by creating a minimal AVAssetWriter
685
- // and encoding a single dummy frame. This eliminates the ~1.5s spike on first
686
- // real frame encode by front-loading the hardware encoder initialization.
687
666
  dispatch_async(self.encodingQueue, ^{
688
667
  @autoreleasepool {
689
668
  NSTimeInterval startTime = CACurrentMediaTime();
690
669
 
691
- // Use a small size for fast prewarm (H.264 requires even dimensions)
692
670
  CGSize warmupSize = CGSizeMake(100, 100);
693
671
 
694
672
  // Create temp file for dummy segment
@@ -808,15 +786,12 @@ static dispatch_once_t sPrewarmOnceToken;
808
786
  return;
809
787
  sEncoderPrewarmed = YES;
810
788
 
811
- // Run prewarm on a low-priority background queue
812
789
  dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
813
790
  @autoreleasepool {
814
791
  NSTimeInterval startTime = CACurrentMediaTime();
815
792
 
816
- // Use a small size for fast prewarm (H.264 requires even dimensions)
817
793
  CGSize warmupSize = CGSizeMake(100, 100);
818
794
 
819
- // Create temp file for dummy segment
820
795
  NSURL *tempDir = [NSURL fileURLWithPath:NSTemporaryDirectory()];
821
796
  NSURL *warmupURL =
822
797
  [tempDir URLByAppendingPathComponent:@"rj_encoder_prewarm.mp4"];
@@ -873,7 +848,6 @@ static dispatch_once_t sPrewarmOnceToken;
873
848
 
874
849
  [warmupWriter startSessionAtSourceTime:kCMTimeZero];
875
850
 
876
- // Create and encode a single dummy frame to trigger H.264 encoder init
877
851
  CVPixelBufferRef dummyBuffer = NULL;
878
852
  NSDictionary *pixelBufferOpts = @{
879
853
  (id)kCVPixelBufferCGImageCompatibilityKey : @YES,