@rejourneyco/react-native 1.0.0 → 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.
- package/README.md +29 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +47 -30
- 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 +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
- package/ios/Capture/RJCaptureEngine.m +72 -34
- package/ios/Capture/RJCaptureHeuristics.h +7 -5
- package/ios/Capture/RJCaptureHeuristics.m +138 -112
- package/ios/Capture/RJVideoEncoder.m +0 -26
- package/ios/Core/Rejourney.mm +64 -102
- package/ios/Utils/RJPerfTiming.m +0 -5
- package/ios/Utils/RJWindowUtils.m +0 -1
- package/lib/commonjs/components/Mask.js +1 -6
- package/lib/commonjs/index.js +12 -101
- package/lib/commonjs/sdk/autoTracking.js +55 -353
- package/lib/commonjs/sdk/constants.js +2 -13
- package/lib/commonjs/sdk/errorTracking.js +1 -29
- package/lib/commonjs/sdk/metricsTracking.js +3 -24
- package/lib/commonjs/sdk/navigation.js +3 -42
- package/lib/commonjs/sdk/networkInterceptor.js +7 -49
- package/lib/commonjs/sdk/utils.js +0 -5
- package/lib/module/components/Mask.js +1 -6
- package/lib/module/index.js +11 -105
- package/lib/module/sdk/autoTracking.js +55 -354
- package/lib/module/sdk/constants.js +2 -13
- package/lib/module/sdk/errorTracking.js +1 -29
- package/lib/module/sdk/index.js +0 -2
- package/lib/module/sdk/metricsTracking.js +3 -24
- package/lib/module/sdk/navigation.js +3 -42
- package/lib/module/sdk/networkInterceptor.js +7 -49
- package/lib/module/sdk/utils.js +0 -5
- package/lib/typescript/NativeRejourney.d.ts +2 -0
- package/lib/typescript/sdk/autoTracking.d.ts +5 -6
- package/lib/typescript/types/index.d.ts +0 -1
- package/package.json +11 -3
- package/src/NativeRejourney.ts +4 -0
- package/src/components/Mask.tsx +0 -3
- package/src/index.ts +11 -88
- package/src/sdk/autoTracking.ts +72 -331
- package/src/sdk/constants.ts +13 -13
- package/src/sdk/errorTracking.ts +1 -17
- package/src/sdk/index.ts +0 -2
- package/src/sdk/metricsTracking.ts +5 -33
- package/src/sdk/navigation.ts +8 -29
- package/src/sdk/networkInterceptor.ts +9 -33
- package/src/sdk/utils.ts +0 -5
- package/src/types/index.ts +0 -29
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
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
|
@@ -1005,7 +1011,14 @@ class RejourneyModuleImpl(
|
|
|
1005
1011
|
try {
|
|
1006
1012
|
val safeUserId = userId.ifEmpty { "anonymous" }
|
|
1007
1013
|
|
|
1008
|
-
//
|
|
1014
|
+
// KEY CHANGE: Persist directly to SharedPreferences (Native Storage)
|
|
1015
|
+
// This replaces the need for async-storage on the JS side
|
|
1016
|
+
reactContext.getSharedPreferences("rejourney", 0)
|
|
1017
|
+
.edit()
|
|
1018
|
+
.putString("rj_user_identity", safeUserId)
|
|
1019
|
+
.apply()
|
|
1020
|
+
|
|
1021
|
+
// Update in-memory state
|
|
1009
1022
|
this.userId = safeUserId
|
|
1010
1023
|
|
|
1011
1024
|
// Update upload manager
|
|
@@ -1030,6 +1043,10 @@ class RejourneyModuleImpl(
|
|
|
1030
1043
|
}
|
|
1031
1044
|
}
|
|
1032
1045
|
|
|
1046
|
+
fun getUserIdentity(promise: Promise) {
|
|
1047
|
+
promise.resolve(userId)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1033
1050
|
// ==================== Helper Methods ====================
|
|
1034
1051
|
|
|
1035
1052
|
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
|
|
@@ -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() {
|
|
@@ -171,6 +178,13 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
171
178
|
instance.setUserIdentity(userId, promise)
|
|
172
179
|
}
|
|
173
180
|
|
|
181
|
+
@ReactMethod
|
|
182
|
+
@DoNotStrip
|
|
183
|
+
override fun getUserIdentity(promise: Promise) {
|
|
184
|
+
val instance = getImplOrReject(promise) ?: return
|
|
185
|
+
instance.getUserIdentity(promise)
|
|
186
|
+
}
|
|
187
|
+
|
|
174
188
|
@ReactMethod
|
|
175
189
|
@DoNotStrip
|
|
176
190
|
override fun setDebugMode(enabled: Boolean, promise: Promise) {
|
|
@@ -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 {
|