@rejourneyco/react-native 1.0.0 → 1.0.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.
- package/README.md +29 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +12 -1
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +25 -1
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +70 -32
- package/android/src/main/java/com/rejourney/core/Constants.kt +4 -4
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
- package/ios/Capture/RJCaptureEngine.m +74 -5
- package/ios/Capture/RJCaptureHeuristics.h +7 -5
- package/ios/Capture/RJCaptureHeuristics.m +138 -112
- package/ios/Core/Rejourney.mm +35 -10
- package/lib/commonjs/index.js +8 -14
- package/lib/commonjs/sdk/autoTracking.js +21 -48
- package/lib/module/index.js +8 -14
- package/lib/module/sdk/autoTracking.js +21 -48
- package/lib/typescript/NativeRejourney.d.ts +1 -0
- package/lib/typescript/sdk/autoTracking.d.ts +1 -2
- package/package.json +17 -3
- package/src/NativeRejourney.ts +2 -0
- package/src/index.ts +8 -15
- package/src/sdk/autoTracking.ts +26 -54
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @rejourneyco/react-native
|
|
2
|
+
|
|
3
|
+
Lightweight session replay and observability SDK for React Native. Pixel-perfect video capture with real-time incident detection.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rejourneyco/react-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { initRejourney, startRejourney } from '@rejourneyco/react-native';
|
|
15
|
+
|
|
16
|
+
// Initialize with your public key
|
|
17
|
+
initRejourney('pk_live_xxxxxxxxxxxx');
|
|
18
|
+
|
|
19
|
+
// Start recording after obtaining user consent
|
|
20
|
+
startRejourney();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
Full integration guides and API reference: https://rejourney.co/docs/reactnative/overview
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
Licensed under Apache 2.0
|
|
@@ -1005,7 +1005,14 @@ class RejourneyModuleImpl(
|
|
|
1005
1005
|
try {
|
|
1006
1006
|
val safeUserId = userId.ifEmpty { "anonymous" }
|
|
1007
1007
|
|
|
1008
|
-
//
|
|
1008
|
+
// KEY CHANGE: Persist directly to SharedPreferences (Native Storage)
|
|
1009
|
+
// This replaces the need for async-storage on the JS side
|
|
1010
|
+
reactContext.getSharedPreferences("rejourney", 0)
|
|
1011
|
+
.edit()
|
|
1012
|
+
.putString("rj_user_identity", safeUserId)
|
|
1013
|
+
.apply()
|
|
1014
|
+
|
|
1015
|
+
// Update in-memory state
|
|
1009
1016
|
this.userId = safeUserId
|
|
1010
1017
|
|
|
1011
1018
|
// Update upload manager
|
|
@@ -1030,6 +1037,10 @@ class RejourneyModuleImpl(
|
|
|
1030
1037
|
}
|
|
1031
1038
|
}
|
|
1032
1039
|
|
|
1040
|
+
fun getUserIdentity(promise: Promise) {
|
|
1041
|
+
promise.resolve(userId)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1033
1044
|
// ==================== Helper Methods ====================
|
|
1034
1045
|
|
|
1035
1046
|
private fun createResultMap(success: Boolean, sessionId: String, error: String? = null): WritableMap {
|
|
@@ -178,6 +178,7 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
178
178
|
private val isShuttingDown = AtomicBoolean(false)
|
|
179
179
|
private var _isRecording: Boolean = false
|
|
180
180
|
private val captureInProgress = AtomicBoolean(false)
|
|
181
|
+
private val isWarmingUp = AtomicBoolean(false)
|
|
181
182
|
private var sessionId: String? = null
|
|
182
183
|
private var currentScreenName: String? = null
|
|
183
184
|
private var viewScanner: ViewHierarchyScanner? = null
|
|
@@ -407,6 +408,23 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
407
408
|
|
|
408
409
|
Logger.info("[CaptureEngine] Resuming video capture")
|
|
409
410
|
|
|
411
|
+
// DEFENSIVE FIX: Warmup period
|
|
412
|
+
// When returning from background, the view hierarchy and layout may not be stable immediately.
|
|
413
|
+
// This is primarily an iOS issue but we apply it here for consistency and safety.
|
|
414
|
+
isWarmingUp.set(true)
|
|
415
|
+
Logger.debug("[CaptureEngine] Warmup started (200ms)")
|
|
416
|
+
|
|
417
|
+
mainHandler.postDelayed({
|
|
418
|
+
if (isShuttingDown.get()) return@postDelayed
|
|
419
|
+
isWarmingUp.set(false)
|
|
420
|
+
Logger.debug("[CaptureEngine] Warmup complete")
|
|
421
|
+
|
|
422
|
+
// Trigger immediate capture check
|
|
423
|
+
if (_isRecording) {
|
|
424
|
+
requestCapture(CaptureImportance.MEDIUM, "warmup_complete", forceCapture = false)
|
|
425
|
+
}
|
|
426
|
+
}, 200)
|
|
427
|
+
|
|
410
428
|
captureHeuristics.reset()
|
|
411
429
|
resetCachedFrames()
|
|
412
430
|
pendingCapture = null
|
|
@@ -594,6 +612,11 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
594
612
|
}
|
|
595
613
|
private fun requestCapture(importance: CaptureImportance, reason: String, forceCapture: Boolean) {
|
|
596
614
|
if (!_isRecording || isShuttingDown.get()) return
|
|
615
|
+
|
|
616
|
+
if (isWarmingUp.get()) {
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
597
620
|
if (!forceCapture && !shouldCapture(importance)) {
|
|
598
621
|
Logger.debug("[CaptureEngine] Capture throttled: $reason (importance: $importance)")
|
|
599
622
|
return
|
|
@@ -687,7 +710,8 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
687
710
|
val decision = captureHeuristics.decisionForSignature(
|
|
688
711
|
pending.layoutSignature,
|
|
689
712
|
now,
|
|
690
|
-
hasLastFrame = lastCapturedBitmap != null
|
|
713
|
+
hasLastFrame = lastCapturedBitmap != null,
|
|
714
|
+
importance = pending.importance
|
|
691
715
|
)
|
|
692
716
|
|
|
693
717
|
if (decision.action == CaptureAction.Defer) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.rejourney.capture
|
|
2
2
|
|
|
3
|
+
import com.rejourney.core.CaptureImportance
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
enum class CaptureAction {
|
|
5
7
|
RenderNow,
|
|
@@ -216,53 +218,89 @@ class CaptureHeuristics {
|
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
|
|
219
|
-
fun decisionForSignature(signature: String?, nowMs: Long, hasLastFrame: Boolean): CaptureDecision {
|
|
221
|
+
fun decisionForSignature(signature: String?, nowMs: Long, hasLastFrame: Boolean, importance: CaptureImportance): CaptureDecision {
|
|
220
222
|
var earliestSafeTime = nowMs
|
|
221
223
|
var blockerReason = CaptureReason.RenderNow
|
|
222
224
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
// Check importance to potentially bypass heuristics
|
|
226
|
+
val isUrgent = importance == CaptureImportance.HIGH || importance == CaptureImportance.CRITICAL
|
|
227
|
+
|
|
228
|
+
// Touch - Usually want smooth input, but CRITICAL updates (like navigation) take precedence
|
|
229
|
+
if (!isUrgent) {
|
|
230
|
+
considerBlockerSince(lastTouchTime, QUIET_TOUCH_MS, nowMs, earliestSafeTime)?.let {
|
|
231
|
+
earliestSafeTime = it
|
|
232
|
+
blockerReason = CaptureReason.DeferTouch
|
|
233
|
+
}
|
|
226
234
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
|
|
236
|
+
// Scroll - High jank risk
|
|
237
|
+
// Even urgent captures should respect scroll to avoid visible hitching, unless CRITICAL
|
|
238
|
+
if (importance != CaptureImportance.CRITICAL) {
|
|
239
|
+
considerBlockerSince(lastScrollTime, QUIET_SCROLL_MS, nowMs, earliestSafeTime)?.let {
|
|
240
|
+
earliestSafeTime = it
|
|
241
|
+
blockerReason = CaptureReason.DeferScroll
|
|
242
|
+
}
|
|
230
243
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
|
|
245
|
+
// Bounce/Rubber-banding
|
|
246
|
+
if (!isUrgent) {
|
|
247
|
+
considerBlockerSince(lastBounceTime, QUIET_BOUNCE_MS, nowMs, earliestSafeTime)?.let {
|
|
248
|
+
earliestSafeTime = it
|
|
249
|
+
blockerReason = CaptureReason.DeferBounce
|
|
250
|
+
}
|
|
234
251
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
|
|
253
|
+
// Refresh
|
|
254
|
+
if (!isUrgent) {
|
|
255
|
+
considerBlockerSince(lastRefreshTime, QUIET_REFRESH_MS, nowMs, earliestSafeTime)?.let {
|
|
256
|
+
earliestSafeTime = it
|
|
257
|
+
blockerReason = CaptureReason.DeferRefresh
|
|
258
|
+
}
|
|
238
259
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
|
|
261
|
+
// Transition - KEY FIX: Urgent captures (NAVIGATION) must bypass this!
|
|
262
|
+
if (!isUrgent) {
|
|
263
|
+
considerBlockerSince(lastTransitionTime, QUIET_TRANSITION_MS, nowMs, earliestSafeTime)?.let {
|
|
264
|
+
earliestSafeTime = it
|
|
265
|
+
blockerReason = CaptureReason.DeferTransition
|
|
266
|
+
}
|
|
242
267
|
}
|
|
243
268
|
|
|
244
269
|
if (keyboardAnimating) {
|
|
245
270
|
lastKeyboardTime = nowMs
|
|
246
271
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
272
|
+
// Keyboard animations can be jerky
|
|
273
|
+
if (!isUrgent) {
|
|
274
|
+
considerBlockerSince(lastKeyboardTime, QUIET_KEYBOARD_MS, nowMs, earliestSafeTime)?.let {
|
|
275
|
+
earliestSafeTime = it
|
|
276
|
+
blockerReason = CaptureReason.DeferKeyboard
|
|
277
|
+
}
|
|
250
278
|
}
|
|
251
279
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
280
|
+
// Map - Always defer map motion as it's very expensive and glitchy
|
|
281
|
+
// Maps are special; even CRITICAL captures might want to wait for map settle if possible,
|
|
282
|
+
// but we'll allow CRITICAL to force it if absolutely needed.
|
|
283
|
+
if (importance != CaptureImportance.CRITICAL) {
|
|
284
|
+
considerBlockerSince(lastMapTime, QUIET_MAP_MS, nowMs, earliestSafeTime)?.let {
|
|
285
|
+
earliestSafeTime = it
|
|
286
|
+
blockerReason = CaptureReason.DeferMap
|
|
287
|
+
}
|
|
256
288
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
289
|
+
if (mapSettleUntilMs > nowMs && mapSettleUntilMs > earliestSafeTime) {
|
|
290
|
+
earliestSafeTime = mapSettleUntilMs
|
|
291
|
+
blockerReason = CaptureReason.DeferMap
|
|
292
|
+
}
|
|
260
293
|
}
|
|
261
294
|
|
|
262
295
|
if (animationBlocking) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
296
|
+
// Big animations (Lottie etc).
|
|
297
|
+
// If urgent, we might want to capture the final state of an animation or screen change
|
|
298
|
+
// regardless of the animation loop.
|
|
299
|
+
if (!isUrgent) {
|
|
300
|
+
considerBlockerSince(lastAnimationTime, QUIET_ANIMATION_MS, nowMs, earliestSafeTime)?.let {
|
|
301
|
+
earliestSafeTime = it
|
|
302
|
+
blockerReason = CaptureReason.DeferBigAnimation
|
|
303
|
+
}
|
|
266
304
|
}
|
|
267
305
|
}
|
|
268
306
|
|
|
@@ -278,11 +316,11 @@ class CaptureHeuristics {
|
|
|
278
316
|
val staleOnly = stale && hasLastFrame && !signatureChanged && !keyframeDue
|
|
279
317
|
val suppressStaleRender = staleOnly && (hasVideoSurface || hasWebSurface || hasCameraSurface)
|
|
280
318
|
|
|
281
|
-
if (suppressStaleRender) {
|
|
319
|
+
if (suppressStaleRender && !isUrgent) {
|
|
282
320
|
return CaptureDecision(CaptureAction.ReuseLast, CaptureReason.ReuseSignatureUnchanged)
|
|
283
321
|
}
|
|
284
322
|
|
|
285
|
-
if (!hasLastFrame || signatureChanged || stale || keyframeDue) {
|
|
323
|
+
if (!hasLastFrame || signatureChanged || stale || keyframeDue || isUrgent) {
|
|
286
324
|
return CaptureDecision(CaptureAction.RenderNow, CaptureReason.RenderNow)
|
|
287
325
|
}
|
|
288
326
|
|
|
@@ -351,7 +389,7 @@ class CaptureHeuristics {
|
|
|
351
389
|
private const val QUIET_BOUNCE_MS = 200L
|
|
352
390
|
private const val QUIET_REFRESH_MS = 220L
|
|
353
391
|
private const val QUIET_MAP_MS = 550L
|
|
354
|
-
private const val QUIET_TRANSITION_MS =
|
|
392
|
+
private const val QUIET_TRANSITION_MS = 100L
|
|
355
393
|
private const val QUIET_KEYBOARD_MS = 250L
|
|
356
394
|
private const val QUIET_ANIMATION_MS = 250L
|
|
357
395
|
|
|
@@ -11,11 +11,11 @@ object Constants {
|
|
|
11
11
|
// Video Capture Configuration (H.264 Segment Mode)
|
|
12
12
|
// These match iOS RJCaptureEngine defaults for performance
|
|
13
13
|
|
|
14
|
-
/** Default capture scale factor (0.
|
|
15
|
-
const val DEFAULT_CAPTURE_SCALE = 0.
|
|
14
|
+
/** Default capture scale factor (0.25 = 25% of original size) - matches iOS */
|
|
15
|
+
const val DEFAULT_CAPTURE_SCALE = 0.25f
|
|
16
16
|
|
|
17
|
-
/** Target video bitrate in bits per second (1.
|
|
18
|
-
const val DEFAULT_VIDEO_BITRATE =
|
|
17
|
+
/** Target video bitrate in bits per second (1.0 Mbps) - matches iOS */
|
|
18
|
+
const val DEFAULT_VIDEO_BITRATE = 1_000_000
|
|
19
19
|
|
|
20
20
|
/** Maximum video dimension in pixels (longest edge) - matches iOS */
|
|
21
21
|
const val MAX_VIDEO_DIMENSION = 1920
|
|
@@ -171,6 +171,13 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
171
171
|
instance.setUserIdentity(userId, promise)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
@ReactMethod
|
|
175
|
+
@DoNotStrip
|
|
176
|
+
override fun getUserIdentity(promise: Promise) {
|
|
177
|
+
val instance = getImplOrReject(promise) ?: return
|
|
178
|
+
instance.getUserIdentity(promise)
|
|
179
|
+
}
|
|
180
|
+
|
|
174
181
|
@ReactMethod
|
|
175
182
|
@DoNotStrip
|
|
176
183
|
override fun setDebugMode(enabled: Boolean, promise: Promise) {
|
|
@@ -61,6 +61,7 @@ typedef struct {
|
|
|
61
61
|
@property(nonatomic, assign) NSInteger generation;
|
|
62
62
|
@property(nonatomic, strong, nullable) RJViewHierarchyScanResult *scanResult;
|
|
63
63
|
@property(nonatomic, copy, nullable) NSString *layoutSignature;
|
|
64
|
+
@property(nonatomic, assign) RJCaptureImportance importance;
|
|
64
65
|
|
|
65
66
|
@end
|
|
66
67
|
|
|
@@ -168,6 +169,8 @@ typedef struct {
|
|
|
168
169
|
@property(nonatomic, assign) CFRunLoopObserverRef runLoopObserver;
|
|
169
170
|
@property(nonatomic, assign) BOOL runLoopCapturePending;
|
|
170
171
|
|
|
172
|
+
@property(nonatomic, assign) BOOL isWarmingUp;
|
|
173
|
+
|
|
171
174
|
@end
|
|
172
175
|
|
|
173
176
|
#pragma mark - Implementation
|
|
@@ -300,6 +303,7 @@ typedef struct {
|
|
|
300
303
|
_isShuttingDown = NO;
|
|
301
304
|
_uiReadyForCapture = NO;
|
|
302
305
|
_framesSinceHierarchy = 0;
|
|
306
|
+
_isWarmingUp = NO;
|
|
303
307
|
|
|
304
308
|
[self applyDefaultConfiguration];
|
|
305
309
|
|
|
@@ -494,7 +498,38 @@ typedef struct {
|
|
|
494
498
|
|
|
495
499
|
- (void)appDidBecomeActive:(NSNotification *)notification {
|
|
496
500
|
self.isInBackground = NO;
|
|
497
|
-
|
|
501
|
+
|
|
502
|
+
// DEFENSIVE FIX: Warmup period
|
|
503
|
+
// When returning from background, the view hierarchy and layout may not be
|
|
504
|
+
// stable immediately. This can cause privacy masks to be drawn in the wrong
|
|
505
|
+
// position relative to the content (race condition). We impose a short
|
|
506
|
+
// "warmup" period where we skip capture to allow AutoLayout to settle.
|
|
507
|
+
self.isWarmingUp = YES;
|
|
508
|
+
|
|
509
|
+
// Clear stale caches to force fresh scan
|
|
510
|
+
if (self.lastCapturedPixelBuffer) {
|
|
511
|
+
CVPixelBufferRelease(self.lastCapturedPixelBuffer);
|
|
512
|
+
self.lastCapturedPixelBuffer = NULL;
|
|
513
|
+
}
|
|
514
|
+
self.lastMaskScanResult = nil;
|
|
515
|
+
self.lastSafeMaskScanResult = nil;
|
|
516
|
+
|
|
517
|
+
RJLogDebug(@"CaptureEngine: App became active - starting warmup (0.2s)");
|
|
518
|
+
|
|
519
|
+
__weak typeof(self) weakSelf = self;
|
|
520
|
+
dispatch_after(
|
|
521
|
+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)),
|
|
522
|
+
dispatch_get_main_queue(), ^{
|
|
523
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
524
|
+
if (!strongSelf)
|
|
525
|
+
return;
|
|
526
|
+
|
|
527
|
+
strongSelf.isWarmingUp = NO;
|
|
528
|
+
RJLogDebug(@"CaptureEngine: Warmup complete - resuming capture");
|
|
529
|
+
|
|
530
|
+
// Trigger an immediate capture check
|
|
531
|
+
[strongSelf captureVideoFrame];
|
|
532
|
+
});
|
|
498
533
|
}
|
|
499
534
|
|
|
500
535
|
#pragma mark - System Monitoring
|
|
@@ -1087,14 +1122,31 @@ typedef struct {
|
|
|
1087
1122
|
}
|
|
1088
1123
|
|
|
1089
1124
|
- (void)captureVideoFrame {
|
|
1125
|
+
[self captureVideoFrameWithImportance:RJCaptureImportanceLow reason:@"timer"];
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
- (void)captureVideoFrameWithImportance:(RJCaptureImportance)importance
|
|
1129
|
+
reason:(NSString *)reason {
|
|
1090
1130
|
if (!self.internalIsRecording || self.isShuttingDown)
|
|
1091
1131
|
return;
|
|
1132
|
+
|
|
1133
|
+
if (self.isWarmingUp) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Critical events (like navigation) should bypass UI ready checks if
|
|
1138
|
+
// possible, but we still need a valid UI.
|
|
1092
1139
|
if (!self.uiReadyForCapture) {
|
|
1093
1140
|
if (self.framesSinceSessionStart % 60 == 0)
|
|
1094
1141
|
RJLogDebug(@"Skipping capture: UI not ready");
|
|
1095
1142
|
return;
|
|
1096
1143
|
}
|
|
1097
|
-
|
|
1144
|
+
|
|
1145
|
+
// Critical events override paused state
|
|
1146
|
+
BOOL isCritical = (importance == RJCaptureImportanceCritical ||
|
|
1147
|
+
importance == RJCaptureImportanceHigh);
|
|
1148
|
+
if (self.internalPerformanceLevel == RJPerformanceLevelPaused &&
|
|
1149
|
+
!isCritical) {
|
|
1098
1150
|
static NSInteger pauseSkipCount = 0;
|
|
1099
1151
|
if (++pauseSkipCount % 60 == 0)
|
|
1100
1152
|
RJLogDebug(@"Skipping capture: Performance Paused");
|
|
@@ -1103,18 +1155,30 @@ typedef struct {
|
|
|
1103
1155
|
NSTimeInterval now = CACurrentMediaTime();
|
|
1104
1156
|
|
|
1105
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.
|
|
1106
1161
|
self.pendingCapture.deadline = now;
|
|
1107
1162
|
[self attemptPendingCapture:self.pendingCapture fullScan:NO];
|
|
1108
1163
|
}
|
|
1109
1164
|
|
|
1110
1165
|
RJCapturePendingCapture *pending = [[RJCapturePendingCapture alloc] init];
|
|
1111
1166
|
pending.wantedAt = now;
|
|
1167
|
+
pending.importance = importance;
|
|
1168
|
+
|
|
1112
1169
|
NSTimeInterval grace = self.captureHeuristics.captureGraceSeconds;
|
|
1113
1170
|
if (self.captureHeuristics.animationBlocking ||
|
|
1114
1171
|
self.captureHeuristics.scrollActive ||
|
|
1115
1172
|
self.captureHeuristics.keyboardAnimating) {
|
|
1116
1173
|
grace = MIN(grace, 0.3);
|
|
1117
1174
|
}
|
|
1175
|
+
|
|
1176
|
+
// High importance requests should have a shorter grace, forcing faster
|
|
1177
|
+
// resolution
|
|
1178
|
+
if (isCritical) {
|
|
1179
|
+
grace = MIN(grace, 0.1);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1118
1182
|
pending.deadline = now + grace;
|
|
1119
1183
|
pending.timestamp = [self currentTimestamp];
|
|
1120
1184
|
pending.generation = ++self.pendingCaptureGeneration;
|
|
@@ -1207,7 +1271,8 @@ typedef struct {
|
|
|
1207
1271
|
RJCaptureHeuristicsDecision *decision = [self.captureHeuristics
|
|
1208
1272
|
decisionForSignature:pending.layoutSignature
|
|
1209
1273
|
now:now
|
|
1210
|
-
hasLastFrame:(self.lastCapturedPixelBuffer != NULL)
|
|
1274
|
+
hasLastFrame:(self.lastCapturedPixelBuffer != NULL)
|
|
1275
|
+
importance:pending.importance];
|
|
1211
1276
|
|
|
1212
1277
|
if (decision.action == RJCaptureHeuristicsActionRenderNow && !fullScan) {
|
|
1213
1278
|
RJ_TIME_START_NAMED(viewScan);
|
|
@@ -1241,7 +1306,8 @@ typedef struct {
|
|
|
1241
1306
|
decision = [self.captureHeuristics
|
|
1242
1307
|
decisionForSignature:pending.layoutSignature
|
|
1243
1308
|
now:now
|
|
1244
|
-
hasLastFrame:(self.lastCapturedPixelBuffer != NULL)
|
|
1309
|
+
hasLastFrame:(self.lastCapturedPixelBuffer != NULL)
|
|
1310
|
+
importance:pending.importance];
|
|
1245
1311
|
}
|
|
1246
1312
|
|
|
1247
1313
|
if (decision.action == RJCaptureHeuristicsActionDefer && fullScan) {
|
|
@@ -1342,7 +1408,10 @@ typedef struct {
|
|
|
1342
1408
|
}
|
|
1343
1409
|
self.pendingDefensiveCaptureTime = 0;
|
|
1344
1410
|
self.lastIntentTime = CACurrentMediaTime();
|
|
1345
|
-
|
|
1411
|
+
// Defensive capture triggered by heuristics (e.g.
|
|
1412
|
+
// navigation) is High importance
|
|
1413
|
+
[self captureVideoFrameWithImportance:RJCaptureImportanceHigh
|
|
1414
|
+
reason:reason];
|
|
1346
1415
|
});
|
|
1347
1416
|
}
|
|
1348
1417
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#import <Foundation/Foundation.h>
|
|
11
11
|
#import <UIKit/UIKit.h>
|
|
12
12
|
|
|
13
|
+
#import "../Core/RJTypes.h"
|
|
13
14
|
#import "RJViewHierarchyScanner.h"
|
|
14
15
|
|
|
15
16
|
NS_ASSUME_NONNULL_BEGIN
|
|
@@ -59,7 +60,7 @@ typedef NS_ENUM(NSInteger, RJCaptureHeuristicsReason) {
|
|
|
59
60
|
- (void)recordMapInteractionAtTime:(NSTimeInterval)time;
|
|
60
61
|
- (void)recordNavigationEventAtTime:(NSTimeInterval)time;
|
|
61
62
|
- (void)recordRenderedSignature:(nullable NSString *)signature
|
|
62
|
-
|
|
63
|
+
atTime:(NSTimeInterval)time;
|
|
63
64
|
|
|
64
65
|
- (void)updateWithScanResult:(RJViewHierarchyScanResult *)scanResult
|
|
65
66
|
window:(UIWindow *)window
|
|
@@ -68,10 +69,11 @@ typedef NS_ENUM(NSInteger, RJCaptureHeuristicsReason) {
|
|
|
68
69
|
- (void)updateWithStabilityProbeForWindow:(UIWindow *)window
|
|
69
70
|
now:(NSTimeInterval)now;
|
|
70
71
|
|
|
71
|
-
- (RJCaptureHeuristicsDecision *)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
- (RJCaptureHeuristicsDecision *)
|
|
73
|
+
decisionForSignature:(nullable NSString *)signature
|
|
74
|
+
now:(NSTimeInterval)now
|
|
75
|
+
hasLastFrame:(BOOL)hasLastFrame
|
|
76
|
+
importance:(RJCaptureImportance)importance;
|
|
75
77
|
|
|
76
78
|
+ (NSString *)stringForReason:(RJCaptureHeuristicsReason)reason;
|
|
77
79
|
|