@rejourneyco/react-native 1.0.2 → 1.0.4
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/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +38 -363
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +14 -27
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
- package/ios/Capture/RJCaptureEngine.m +9 -61
- package/ios/Capture/RJViewHierarchyScanner.m +68 -51
- package/ios/Core/RJLifecycleManager.m +0 -14
- package/ios/Core/Rejourney.mm +24 -37
- package/ios/Network/RJDeviceAuthManager.m +0 -2
- package/ios/Network/RJUploadManager.h +8 -0
- package/ios/Network/RJUploadManager.m +45 -0
- package/ios/Privacy/RJPrivacyMask.m +5 -31
- package/ios/Rejourney.h +0 -14
- package/ios/Touch/RJTouchInterceptor.m +21 -15
- package/ios/Utils/RJEventBuffer.m +57 -69
- package/ios/Utils/RJWindowUtils.m +87 -86
- package/lib/commonjs/index.js +44 -31
- package/lib/commonjs/sdk/autoTracking.js +0 -3
- package/lib/commonjs/sdk/constants.js +1 -1
- package/lib/commonjs/sdk/networkInterceptor.js +0 -11
- package/lib/commonjs/sdk/utils.js +73 -14
- package/lib/module/index.js +44 -31
- package/lib/module/sdk/autoTracking.js +0 -3
- package/lib/module/sdk/constants.js +1 -1
- package/lib/module/sdk/networkInterceptor.js +0 -11
- package/lib/module/sdk/utils.js +73 -14
- package/lib/typescript/sdk/constants.d.ts +1 -1
- package/lib/typescript/sdk/utils.d.ts +31 -1
- package/package.json +16 -4
- package/src/index.ts +42 -20
- package/src/sdk/autoTracking.ts +0 -2
- package/src/sdk/constants.ts +14 -14
- package/src/sdk/networkInterceptor.ts +0 -9
- package/src/sdk/utils.ts +76 -14
|
@@ -85,14 +85,13 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
85
85
|
var lastScanTime: Long = 0L
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
// Configuration
|
|
89
88
|
var captureScale: Float = Constants.DEFAULT_CAPTURE_SCALE
|
|
90
89
|
var minFrameInterval: Double = Constants.DEFAULT_MIN_FRAME_INTERVAL
|
|
91
90
|
var maxFramesPerMinute: Int = Constants.DEFAULT_MAX_FRAMES_PER_MINUTE
|
|
92
91
|
var framesPerSegment: Int = Constants.DEFAULT_FRAMES_PER_SEGMENT
|
|
93
92
|
var targetBitrate: Int = Constants.DEFAULT_VIDEO_BITRATE
|
|
94
93
|
var targetFps: Int = Constants.DEFAULT_VIDEO_FPS
|
|
95
|
-
var hierarchyCaptureInterval: Int = 5
|
|
94
|
+
var hierarchyCaptureInterval: Int = 5
|
|
96
95
|
var adaptiveQualityEnabled: Boolean = true
|
|
97
96
|
var thermalThrottleEnabled: Boolean = true
|
|
98
97
|
var batteryAwareEnabled: Boolean = true
|
|
@@ -121,60 +120,46 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
121
120
|
viewScanner?.config?.detectVideoLayers = value
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
// Delegate for segment upload coordination
|
|
125
123
|
var delegate: CaptureEngineDelegate? = null
|
|
126
124
|
|
|
127
|
-
// Read-only state
|
|
128
125
|
var currentPerformanceLevel: PerformanceLevel = PerformanceLevel.NORMAL
|
|
129
126
|
private set
|
|
130
127
|
val frameCount: Int
|
|
131
128
|
get() = videoEncoder?.currentFrameCount ?: 0
|
|
132
129
|
|
|
133
|
-
// FIX: Change this property getter to use explicit function body
|
|
134
130
|
val isRecording: Boolean
|
|
135
131
|
get() {
|
|
136
132
|
return _isRecording && !isShuttingDown.get()
|
|
137
133
|
}
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
// Hierarchy tracking (matching iOS)
|
|
141
135
|
private var framesSinceHierarchy: Int = 0
|
|
142
136
|
|
|
143
|
-
// Hierarchy snapshots accumulator
|
|
144
137
|
private val hierarchySnapshots = mutableListOf<Map<String, Any?>>()
|
|
145
138
|
|
|
146
|
-
// Video encoder
|
|
147
139
|
private var videoEncoder: VideoEncoder? = null
|
|
148
140
|
private val segmentDir: File by lazy {
|
|
149
141
|
File(context.filesDir, "rejourney/segments").apply { mkdirs() }
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
// Motion tracker
|
|
153
144
|
private val motionTracker = MotionTracker()
|
|
154
145
|
|
|
155
146
|
private val captureHeuristics = CaptureHeuristics()
|
|
156
147
|
|
|
157
|
-
// Handler for main thread operations
|
|
158
148
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
159
149
|
|
|
160
|
-
// Executor for background image processing
|
|
161
150
|
private val processingExecutor = Executors.newSingleThreadExecutor()
|
|
162
151
|
|
|
163
|
-
// Coroutine scope for async operations
|
|
164
152
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
165
153
|
|
|
166
|
-
// Capture timer (1 FPS = every 1000ms, matches iOS RJCaptureEngine defaults)
|
|
167
154
|
private var captureRunnable: Runnable? = null
|
|
168
155
|
private val captureIntervalMs: Long
|
|
169
156
|
get() = (1000L / targetFps).coerceAtLeast(100)
|
|
170
157
|
|
|
171
|
-
// Optimized bitmap object pool to reduce GC churn
|
|
172
158
|
private val bitmapPool = ConcurrentLinkedQueue<Bitmap>()
|
|
173
|
-
private val MAX_POOL_SIZE = 8
|
|
159
|
+
private val MAX_POOL_SIZE = 8
|
|
174
160
|
|
|
175
161
|
var onFrameCaptured: (() -> Unit)? = null
|
|
176
162
|
|
|
177
|
-
// -- STATE PROPERTIES (Restored) --
|
|
178
163
|
private val isShuttingDown = AtomicBoolean(false)
|
|
179
164
|
private var _isRecording: Boolean = false
|
|
180
165
|
private val captureInProgress = AtomicBoolean(false)
|
|
@@ -195,7 +180,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
195
180
|
private var lastSafeBitmap: Bitmap? = null
|
|
196
181
|
private var lastCapturedHadBlockedSurface = false
|
|
197
182
|
|
|
198
|
-
// Throttling & Adaptive Quality State
|
|
199
183
|
private var framesSinceSessionStart = 0
|
|
200
184
|
private var framesThisMinute = 0
|
|
201
185
|
private var minuteStartTime: Long = 0
|
|
@@ -204,24 +188,20 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
204
188
|
private var cooldownUntil: Long = 0
|
|
205
189
|
private var didPrewarmViewCaches = false
|
|
206
190
|
|
|
207
|
-
// Battery Awareness
|
|
208
191
|
private var cachedBatteryLevel: Float = 1.0f
|
|
209
192
|
private var lastBatteryCheckTime: Long = 0L
|
|
210
193
|
|
|
211
|
-
// Device dimensions
|
|
212
194
|
private var deviceWidth: Int = 0
|
|
213
195
|
private var deviceHeight: Int = 0
|
|
214
196
|
private var lastMapPresenceTimeMs: Long = 0L
|
|
215
197
|
|
|
216
198
|
init {
|
|
217
|
-
// Pre-warm H.264 encoder in background
|
|
218
199
|
try {
|
|
219
200
|
VideoEncoder.prewarmEncoderAsync()
|
|
220
201
|
} catch (e: Exception) {
|
|
221
202
|
Logger.warning("[CaptureEngine] Encoder prewarm failed (non-critical): ${e.message}")
|
|
222
203
|
}
|
|
223
204
|
|
|
224
|
-
// Pre-warm render server (Optimization 4)
|
|
225
205
|
mainHandler.post {
|
|
226
206
|
prewarmRenderServer()
|
|
227
207
|
}
|
|
@@ -275,15 +255,12 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
275
255
|
PrivacyMask.maskWebViews = privacyMaskWebViews
|
|
276
256
|
PrivacyMask.maskVideoLayers = privacyMaskVideoLayers
|
|
277
257
|
|
|
278
|
-
// Pre-warm privacy scanner class caches (matching iOS prewarmClassCaches)
|
|
279
|
-
// This eliminates ~10-15ms of cold-cache class lookups on first frame
|
|
280
258
|
if (!didPrewarmViewCaches) {
|
|
281
259
|
viewScanner?.prewarmClassCaches()
|
|
282
260
|
PrivacyMask.prewarmClassCaches()
|
|
283
261
|
didPrewarmViewCaches = true
|
|
284
262
|
}
|
|
285
263
|
|
|
286
|
-
// Initialize video encoder
|
|
287
264
|
videoEncoder = VideoEncoder(segmentDir).apply {
|
|
288
265
|
this.targetBitrate = this@CaptureEngine.targetBitrate
|
|
289
266
|
this.framesPerSegment = this@CaptureEngine.framesPerSegment
|
|
@@ -294,14 +271,11 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
294
271
|
this.keyframeInterval = Constants.DEFAULT_KEYFRAME_INTERVAL
|
|
295
272
|
this.delegate = this@CaptureEngine
|
|
296
273
|
setSessionId(sessionId)
|
|
297
|
-
prewarm()
|
|
274
|
+
prewarm()
|
|
298
275
|
}
|
|
299
276
|
|
|
300
|
-
// Clean up any orphaned segments from previous sessions
|
|
301
277
|
cleanupOldSegments()
|
|
302
278
|
|
|
303
|
-
// Start the first video segment
|
|
304
|
-
// This is required to set isRecording=true in the encoder so frames can be appended
|
|
305
279
|
mainHandler.post {
|
|
306
280
|
val window = getCurrentWindow()
|
|
307
281
|
if (window != null) {
|
|
@@ -310,13 +284,11 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
310
284
|
videoEncoder?.startSegment(decorView.width, decorView.height)
|
|
311
285
|
Logger.debug("[CaptureEngine] Started first segment: ${decorView.width}x${decorView.height}")
|
|
312
286
|
} else {
|
|
313
|
-
// Defer segment start until we have valid dimensions
|
|
314
287
|
Logger.debug("[CaptureEngine] Deferring segment start - waiting for valid dimensions")
|
|
315
288
|
}
|
|
316
289
|
}
|
|
317
290
|
}
|
|
318
291
|
|
|
319
|
-
// Start capture timer
|
|
320
292
|
startCaptureTimer()
|
|
321
293
|
|
|
322
294
|
Logger.debug("[CaptureEngine] Session started: $sessionId")
|
|
@@ -333,13 +305,10 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
333
305
|
_isRecording = false
|
|
334
306
|
stopCaptureTimer()
|
|
335
307
|
|
|
336
|
-
// Finish current segment (will trigger delegate callback)
|
|
337
308
|
videoEncoder?.finishSegment()
|
|
338
309
|
|
|
339
|
-
// Upload any remaining hierarchy snapshots
|
|
340
310
|
uploadCurrentHierarchySnapshots()
|
|
341
311
|
|
|
342
|
-
// OPTIMIZATION: Clean up resources
|
|
343
312
|
cleanup()
|
|
344
313
|
|
|
345
314
|
Logger.debug("[CaptureEngine] Session stopped")
|
|
@@ -349,14 +318,10 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
349
318
|
* OPTIMIZATION: Comprehensive cleanup to prevent memory leaks.
|
|
350
319
|
*/
|
|
351
320
|
private fun cleanup() {
|
|
352
|
-
// Clean up video encoder
|
|
353
321
|
videoEncoder = null
|
|
354
322
|
|
|
355
|
-
// Clean up bitmap resources
|
|
356
|
-
// Clear caches
|
|
357
323
|
hierarchySnapshots.clear()
|
|
358
324
|
|
|
359
|
-
// Reset counters
|
|
360
325
|
framesSinceSessionStart = 0
|
|
361
326
|
framesSinceHierarchy = 0
|
|
362
327
|
sessionId = null
|
|
@@ -371,7 +336,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
371
336
|
captureHeuristics.reset()
|
|
372
337
|
resetCachedFrames()
|
|
373
338
|
|
|
374
|
-
// Cancel any pending operations
|
|
375
339
|
scope.coroutineContext.cancelChildren()
|
|
376
340
|
}
|
|
377
341
|
|
|
@@ -393,10 +357,8 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
393
357
|
|
|
394
358
|
stopCaptureTimer()
|
|
395
359
|
|
|
396
|
-
// Finish current segment
|
|
397
360
|
videoEncoder?.finishSegment()
|
|
398
361
|
|
|
399
|
-
// Upload hierarchy snapshots
|
|
400
362
|
uploadCurrentHierarchySnapshots()
|
|
401
363
|
}
|
|
402
364
|
|
|
@@ -408,9 +370,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
408
370
|
|
|
409
371
|
Logger.info("[CaptureEngine] Resuming video capture")
|
|
410
372
|
|
|
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
373
|
isWarmingUp.set(true)
|
|
415
374
|
Logger.debug("[CaptureEngine] Warmup started (200ms)")
|
|
416
375
|
|
|
@@ -419,7 +378,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
419
378
|
isWarmingUp.set(false)
|
|
420
379
|
Logger.debug("[CaptureEngine] Warmup complete")
|
|
421
380
|
|
|
422
|
-
// Trigger immediate capture check
|
|
423
381
|
if (_isRecording) {
|
|
424
382
|
requestCapture(CaptureImportance.MEDIUM, "warmup_complete", forceCapture = false)
|
|
425
383
|
}
|
|
@@ -434,7 +392,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
434
392
|
idleCapturePending = false
|
|
435
393
|
idleCaptureGeneration = 0
|
|
436
394
|
|
|
437
|
-
// Get window and start new segment
|
|
438
395
|
mainHandler.post {
|
|
439
396
|
val window = getCurrentWindow()
|
|
440
397
|
if (window != null && videoEncoder != null) {
|
|
@@ -577,27 +534,12 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
577
534
|
return videoEncoder?.emergencyFlushSync() ?: false
|
|
578
535
|
}
|
|
579
536
|
|
|
580
|
-
// ==================== VideoEncoderDelegate ====================
|
|
581
|
-
|
|
582
537
|
override fun onSegmentFinished(segmentFile: File, startTime: Long, endTime: Long, frameCount: Int) {
|
|
583
538
|
Logger.info("[CaptureEngine] Segment ready: ${segmentFile.name} ($frameCount frames, ${(endTime - startTime) / 1000.0}s)")
|
|
584
539
|
|
|
585
|
-
// Notify delegate of segment completion
|
|
586
540
|
delegate?.onSegmentReady(segmentFile, startTime, endTime, frameCount)
|
|
587
541
|
|
|
588
|
-
// Upload accumulated hierarchy snapshots
|
|
589
542
|
uploadCurrentHierarchySnapshots()
|
|
590
|
-
|
|
591
|
-
// NOTE: Do NOT start a new segment here!
|
|
592
|
-
// VideoEncoder.finishSegmentAndContinue() already handles starting the next segment
|
|
593
|
-
// during automatic rotation (when segment reaches framesPerSegment limit).
|
|
594
|
-
// Starting a segment here causes a race condition where we try to finish an
|
|
595
|
-
// encoder that was just created, crashing with IllegalStateException in
|
|
596
|
-
// signalEndOfInputStream().
|
|
597
|
-
//
|
|
598
|
-
// New segments are now started by:
|
|
599
|
-
// 1. VideoEncoder.finishSegmentAndContinue() during rotation (internal)
|
|
600
|
-
// 2. captureFrameInternal() when recording but encoder not started yet
|
|
601
543
|
}
|
|
602
544
|
|
|
603
545
|
override fun onEncodingError(error: Exception) {
|
|
@@ -605,8 +547,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
605
547
|
delegate?.onCaptureError(error)
|
|
606
548
|
}
|
|
607
549
|
|
|
608
|
-
// ==================== Private Methods ====================
|
|
609
|
-
|
|
610
550
|
private fun captureFrame(importance: CaptureImportance, reason: String) {
|
|
611
551
|
requestCapture(importance, reason, forceCapture = false)
|
|
612
552
|
}
|
|
@@ -865,18 +805,15 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
865
805
|
return
|
|
866
806
|
}
|
|
867
807
|
|
|
868
|
-
// Check cooldown
|
|
869
808
|
val nowMs = System.currentTimeMillis()
|
|
870
809
|
if (nowMs < cooldownUntil) {
|
|
871
810
|
captureInProgress.set(false)
|
|
872
811
|
return
|
|
873
812
|
}
|
|
874
813
|
|
|
875
|
-
// Store device dimensions for reference
|
|
876
814
|
deviceWidth = decorView.width
|
|
877
815
|
deviceHeight = decorView.height
|
|
878
816
|
|
|
879
|
-
// Calculate scaled dimensions for bitmap
|
|
880
817
|
var effectiveScale = captureScale
|
|
881
818
|
if (!effectiveScale.isFinite() || effectiveScale <= 0f) {
|
|
882
819
|
effectiveScale = Constants.DEFAULT_CAPTURE_SCALE
|
|
@@ -930,7 +867,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
930
867
|
returnBitmap(bitmap)
|
|
931
868
|
}
|
|
932
869
|
} else {
|
|
933
|
-
// PixelCopy not available pre-O; skip to avoid incorrect GPU capture
|
|
934
870
|
captureInProgress.set(false)
|
|
935
871
|
returnBitmap(bitmap)
|
|
936
872
|
}
|
|
@@ -995,24 +931,18 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
995
931
|
|
|
996
932
|
try {
|
|
997
933
|
PerfTiming.time(PerfMetric.FRAME) {
|
|
998
|
-
// NUCLEAR FIX: Disable frame deduplication
|
|
999
|
-
// Previously we would skip frames with matching hashes, but this caused
|
|
1000
|
-
// issues with MapView and other GPU-rendered content where the hash might
|
|
1001
|
-
// not reflect visual changes. Always process every frame now.
|
|
1002
934
|
val now = System.currentTimeMillis()
|
|
1003
935
|
|
|
1004
|
-
// OPTIMIZATION 2: Lazy privacy masking - only apply if sensitive rects exist
|
|
1005
936
|
val shouldMask = privacyMaskTextInputs || privacyMaskCameraViews ||
|
|
1006
937
|
privacyMaskWebViews || privacyMaskVideoLayers
|
|
1007
938
|
val maskedBitmap = if (sensitiveRects.isNotEmpty() && shouldMask) {
|
|
1008
939
|
PrivacyMask.applyMasksToBitmap(bitmap, sensitiveRects, rootWidth, rootHeight)
|
|
1009
940
|
} else {
|
|
1010
|
-
bitmap
|
|
941
|
+
bitmap
|
|
1011
942
|
}
|
|
1012
943
|
|
|
1013
944
|
val timestamp = System.currentTimeMillis()
|
|
1014
945
|
|
|
1015
|
-
// Append frame to video encoder
|
|
1016
946
|
val success = encoder.appendFrame(maskedBitmap, timestamp)
|
|
1017
947
|
|
|
1018
948
|
if (success) {
|
|
@@ -1028,7 +958,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1028
958
|
try {
|
|
1029
959
|
onFrameCaptured?.invoke()
|
|
1030
960
|
} catch (_: Exception) {
|
|
1031
|
-
// Best-effort only
|
|
1032
961
|
}
|
|
1033
962
|
}
|
|
1034
963
|
|
|
@@ -1144,9 +1073,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1144
1073
|
return rects
|
|
1145
1074
|
}
|
|
1146
1075
|
|
|
1147
|
-
|
|
1148
|
-
// Legacy bitmap hash - removed
|
|
1149
|
-
|
|
1150
1076
|
/**
|
|
1151
1077
|
* OPTIMIZATION 4: Render Server Pre-warming
|
|
1152
1078
|
* Performs a dummy render to initialize the graphics subsystem/driver
|
|
@@ -1157,8 +1083,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1157
1083
|
val window = getCurrentWindow() ?: return
|
|
1158
1084
|
val bitmap = getBitmap(1, 1)
|
|
1159
1085
|
val canvas = Canvas(bitmap)
|
|
1160
|
-
// Draw a tiny part of the view hierarchy to warm up the rendering pipeline
|
|
1161
|
-
// This pays the "first draw" cost (~50-100ms) now instead of during first capture
|
|
1162
1086
|
window.decorView.draw(canvas)
|
|
1163
1087
|
returnBitmap(bitmap)
|
|
1164
1088
|
Logger.debug("[CaptureEngine] Render server pre-warmed")
|
|
@@ -1226,7 +1150,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1226
1150
|
try {
|
|
1227
1151
|
Logger.debug("[CaptureEngine] uploadCurrentHierarchySnapshots: Converting ${snapshotsToUpload.size} snapshots to JSON")
|
|
1228
1152
|
|
|
1229
|
-
// Convert to JSON
|
|
1230
1153
|
val jsonArray = JSONArray()
|
|
1231
1154
|
for (snapshot in snapshotsToUpload) {
|
|
1232
1155
|
try {
|
|
@@ -1289,7 +1212,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1289
1212
|
}
|
|
1290
1213
|
|
|
1291
1214
|
private fun updatePerformanceLevel() {
|
|
1292
|
-
// Check thermal state first (matching iOS RJPerformanceManager behavior)
|
|
1293
1215
|
if (thermalThrottleEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1294
1216
|
val thermalLevel = getThermalStatus()
|
|
1295
1217
|
when (thermalLevel) {
|
|
@@ -1307,17 +1229,12 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1307
1229
|
currentPerformanceLevel = PerformanceLevel.REDUCED
|
|
1308
1230
|
return
|
|
1309
1231
|
}
|
|
1310
|
-
// THERMAL_STATUS_NONE, THERMAL_STATUS_LIGHT - continue to battery check
|
|
1311
1232
|
}
|
|
1312
1233
|
}
|
|
1313
1234
|
|
|
1314
|
-
// OPTIMIZATION: Battery-aware capture rate adjustment
|
|
1315
|
-
// Reduces capture rate when battery is low to extend battery life
|
|
1316
1235
|
currentPerformanceLevel = when {
|
|
1317
1236
|
isLowBattery() && batteryAwareEnabled -> {
|
|
1318
|
-
// Below 15% battery: reduce to minimal capture (0.25 FPS effective)
|
|
1319
1237
|
if (cachedBatteryLevel < 0.15f) PerformanceLevel.MINIMAL
|
|
1320
|
-
// Below 30% battery: reduce capture rate (0.5 FPS effective)
|
|
1321
1238
|
else if (cachedBatteryLevel < 0.30f) PerformanceLevel.REDUCED
|
|
1322
1239
|
else PerformanceLevel.NORMAL
|
|
1323
1240
|
}
|
|
@@ -1343,7 +1260,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1343
1260
|
}
|
|
1344
1261
|
|
|
1345
1262
|
private fun isLowBattery(): Boolean {
|
|
1346
|
-
// Cache battery level for 15 seconds to avoid frequent calls
|
|
1347
1263
|
val now = System.currentTimeMillis()
|
|
1348
1264
|
if (now - lastBatteryCheckTime > 15000) {
|
|
1349
1265
|
lastBatteryCheckTime = now
|
|
@@ -1362,23 +1278,19 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1362
1278
|
private fun shouldCapture(importance: CaptureImportance): Boolean {
|
|
1363
1279
|
val now = System.currentTimeMillis()
|
|
1364
1280
|
|
|
1365
|
-
// Always allow critical captures
|
|
1366
1281
|
if (importance == CaptureImportance.CRITICAL) {
|
|
1367
1282
|
return true
|
|
1368
1283
|
}
|
|
1369
1284
|
|
|
1370
|
-
// Check minimum interval
|
|
1371
1285
|
val elapsed = (now - lastCaptureTime) / 1000.0
|
|
1372
1286
|
if (elapsed < minFrameInterval) {
|
|
1373
1287
|
return false
|
|
1374
1288
|
}
|
|
1375
1289
|
|
|
1376
|
-
// Check frames per minute limit
|
|
1377
1290
|
if (framesThisMinute >= maxFramesPerMinute) {
|
|
1378
1291
|
return importance.value >= CaptureImportance.HIGH.value
|
|
1379
1292
|
}
|
|
1380
1293
|
|
|
1381
|
-
// Performance level checks
|
|
1382
1294
|
return when (currentPerformanceLevel) {
|
|
1383
1295
|
PerformanceLevel.PAUSED -> importance == CaptureImportance.CRITICAL
|
|
1384
1296
|
PerformanceLevel.MINIMAL -> importance.value >= CaptureImportance.HIGH.value
|
|
@@ -1387,8 +1299,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1387
1299
|
}
|
|
1388
1300
|
}
|
|
1389
1301
|
|
|
1390
|
-
// Legacy methods removed: checkLayoutSignature, isAnyGestureActiveInView, hasSpecialViewsInView
|
|
1391
|
-
|
|
1392
1302
|
private fun updateFrameRateTracking() {
|
|
1393
1303
|
val now = System.currentTimeMillis()
|
|
1394
1304
|
if (now - minuteStartTime >= 60_000) {
|
|
@@ -1435,8 +1345,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1435
1345
|
return false
|
|
1436
1346
|
}
|
|
1437
1347
|
|
|
1438
|
-
// Legacy Detection Logic (Gesture/Special Views) Removed
|
|
1439
|
-
|
|
1440
1348
|
private fun startCaptureTimer() {
|
|
1441
1349
|
stopCaptureTimer()
|
|
1442
1350
|
captureRunnable = object : Runnable {
|
|
@@ -1456,20 +1364,15 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1456
1364
|
}
|
|
1457
1365
|
|
|
1458
1366
|
private fun getBitmap(width: Int, height: Int): Bitmap {
|
|
1459
|
-
// OPTIMIZATION: Improved bitmap pool with size matching
|
|
1460
1367
|
val pooled = bitmapPool.poll()
|
|
1461
1368
|
if (pooled != null && !pooled.isRecycled) {
|
|
1462
|
-
// Check if pooled bitmap can be reused (same or larger size)
|
|
1463
1369
|
if (pooled.width >= width && pooled.height >= height) {
|
|
1464
|
-
// Create a subset if the pooled bitmap is larger
|
|
1465
1370
|
if (pooled.width == width && pooled.height == height) {
|
|
1466
1371
|
return pooled
|
|
1467
1372
|
} else {
|
|
1468
|
-
// Return oversized bitmap to pool and create exact size
|
|
1469
1373
|
bitmapPool.offer(pooled)
|
|
1470
1374
|
}
|
|
1471
1375
|
} else {
|
|
1472
|
-
// Pooled bitmap too small, recycle it
|
|
1473
1376
|
pooled.recycle()
|
|
1474
1377
|
}
|
|
1475
1378
|
}
|
|
@@ -1477,10 +1380,9 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1477
1380
|
return try {
|
|
1478
1381
|
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
1479
1382
|
} catch (e: OutOfMemoryError) {
|
|
1480
|
-
// Clear pool and try again
|
|
1481
1383
|
clearBitmapPool()
|
|
1482
|
-
System.gc()
|
|
1483
|
-
Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
|
1384
|
+
System.gc()
|
|
1385
|
+
Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
|
1484
1386
|
}
|
|
1485
1387
|
}
|
|
1486
1388
|
|
|
@@ -1491,23 +1393,20 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1491
1393
|
private fun returnBitmap(bitmap: Bitmap) {
|
|
1492
1394
|
if (bitmap.isRecycled) return
|
|
1493
1395
|
|
|
1494
|
-
// OPTIMIZATION: Stricter pool size limits based on memory pressure
|
|
1495
1396
|
val maxPoolSize = when (currentPerformanceLevel) {
|
|
1496
|
-
PerformanceLevel.MINIMAL, PerformanceLevel.PAUSED -> 2
|
|
1497
|
-
PerformanceLevel.REDUCED -> 4
|
|
1498
|
-
PerformanceLevel.NORMAL -> MAX_POOL_SIZE
|
|
1397
|
+
PerformanceLevel.MINIMAL, PerformanceLevel.PAUSED -> 2
|
|
1398
|
+
PerformanceLevel.REDUCED -> 4
|
|
1399
|
+
PerformanceLevel.NORMAL -> MAX_POOL_SIZE
|
|
1499
1400
|
}
|
|
1500
1401
|
|
|
1501
1402
|
if (bitmapPool.size < maxPoolSize) {
|
|
1502
|
-
// Only pool bitmaps of reasonable size to avoid memory bloat
|
|
1503
1403
|
val bitmapBytes = bitmap.byteCount
|
|
1504
|
-
if (bitmapBytes < 2 * 1024 * 1024) {
|
|
1404
|
+
if (bitmapBytes < 2 * 1024 * 1024) {
|
|
1505
1405
|
bitmapPool.offer(bitmap)
|
|
1506
1406
|
return
|
|
1507
1407
|
}
|
|
1508
1408
|
}
|
|
1509
1409
|
|
|
1510
|
-
// Pool full or bitmap too large - recycle immediately
|
|
1511
1410
|
bitmap.recycle()
|
|
1512
1411
|
}
|
|
1513
1412
|
|
|
@@ -1523,7 +1422,6 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1523
1422
|
}
|
|
1524
1423
|
if (recycledCount > 0) {
|
|
1525
1424
|
Logger.debug("[CaptureEngine] Recycled $recycledCount pooled bitmaps")
|
|
1526
|
-
// Suggest GC after clearing pool to reclaim memory quickly
|
|
1527
1425
|
System.gc()
|
|
1528
1426
|
}
|
|
1529
1427
|
}
|
|
@@ -1547,7 +1445,7 @@ class CaptureEngine(private val context: Context) : VideoEncoderDelegate {
|
|
|
1547
1445
|
private fun cleanupOldSegments() {
|
|
1548
1446
|
scope.launch {
|
|
1549
1447
|
try {
|
|
1550
|
-
val cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000)
|
|
1448
|
+
val cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000)
|
|
1551
1449
|
segmentDir.listFiles()?.forEach { file ->
|
|
1552
1450
|
if (file.isFile && file.name.endsWith(".mp4") && file.lastModified() < cutoffTime) {
|
|
1553
1451
|
file.delete()
|
|
@@ -71,7 +71,6 @@ class SegmentUploader(
|
|
|
71
71
|
private val pendingUploadCount = AtomicInteger(0)
|
|
72
72
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
73
73
|
|
|
74
|
-
// Use shared client with longer timeouts for video uploads (SSL pinning removed)
|
|
75
74
|
private val client = HttpClientProvider.shared.newBuilder()
|
|
76
75
|
.connectTimeout(60, TimeUnit.SECONDS)
|
|
77
76
|
.readTimeout(120, TimeUnit.SECONDS)
|
|
@@ -114,7 +113,6 @@ class SegmentUploader(
|
|
|
114
113
|
pendingUploadCount.incrementAndGet()
|
|
115
114
|
|
|
116
115
|
try {
|
|
117
|
-
// Step 1: Request presigned URL from backend
|
|
118
116
|
Logger.debug("[SegmentUploader] Requesting presigned URL for segment")
|
|
119
117
|
val presignResult = requestPresignedURL(
|
|
120
118
|
sessionId = sessionId,
|
|
@@ -136,7 +134,6 @@ class SegmentUploader(
|
|
|
136
134
|
|
|
137
135
|
Logger.debug("[SegmentUploader] Got presigned URL, segmentId=$segmentId")
|
|
138
136
|
|
|
139
|
-
// Step 2: Upload directly to S3
|
|
140
137
|
Logger.debug("[SegmentUploader] Uploading to S3...")
|
|
141
138
|
val uploadResult = uploadFileToS3(segmentFile, presignedUrl, "video/mp4")
|
|
142
139
|
|
|
@@ -147,7 +144,6 @@ class SegmentUploader(
|
|
|
147
144
|
|
|
148
145
|
Logger.debug("[SegmentUploader] S3 upload SUCCESS, calling segment/complete")
|
|
149
146
|
|
|
150
|
-
// Step 3: Notify backend of completion
|
|
151
147
|
val completeResult = notifySegmentComplete(
|
|
152
148
|
segmentId = segmentId!!,
|
|
153
149
|
sessionId = sessionId,
|
|
@@ -198,10 +194,8 @@ class SegmentUploader(
|
|
|
198
194
|
pendingUploadCount.incrementAndGet()
|
|
199
195
|
|
|
200
196
|
try {
|
|
201
|
-
// Compress data
|
|
202
197
|
val compressedData = gzipData(hierarchyData)
|
|
203
198
|
|
|
204
|
-
// Request presigned URL for hierarchy upload with gzip compression
|
|
205
199
|
val presignResult = requestPresignedURL(
|
|
206
200
|
sessionId = sessionId,
|
|
207
201
|
kind = "hierarchy",
|
|
@@ -220,7 +214,6 @@ class SegmentUploader(
|
|
|
220
214
|
val presignedUrl = presignResult.presignedUrl
|
|
221
215
|
val segmentId = presignResult.segmentId
|
|
222
216
|
|
|
223
|
-
// Upload compressed data to S3
|
|
224
217
|
val uploadResult = uploadDataToS3(compressedData, presignedUrl, "application/gzip")
|
|
225
218
|
|
|
226
219
|
if (!uploadResult.success) {
|
|
@@ -228,7 +221,6 @@ class SegmentUploader(
|
|
|
228
221
|
return@withContext SegmentUploadResult(false, uploadResult.error)
|
|
229
222
|
}
|
|
230
223
|
|
|
231
|
-
// Notify backend of completion
|
|
232
224
|
val completeResult = notifySegmentComplete(
|
|
233
225
|
segmentId = segmentId!!,
|
|
234
226
|
sessionId = sessionId,
|
|
@@ -264,7 +256,7 @@ class SegmentUploader(
|
|
|
264
256
|
*/
|
|
265
257
|
fun cleanupOrphanedSegments(segmentDir: File) {
|
|
266
258
|
try {
|
|
267
|
-
val cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000)
|
|
259
|
+
val cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000)
|
|
268
260
|
segmentDir.listFiles()?.forEach { file ->
|
|
269
261
|
if (file.isFile && file.lastModified() < cutoffTime) {
|
|
270
262
|
file.delete()
|
|
@@ -276,7 +268,6 @@ class SegmentUploader(
|
|
|
276
268
|
}
|
|
277
269
|
}
|
|
278
270
|
|
|
279
|
-
// ==================== Private Methods ====================
|
|
280
271
|
|
|
281
272
|
private data class PresignResult(
|
|
282
273
|
val success: Boolean,
|
|
@@ -358,7 +349,6 @@ class SegmentUploader(
|
|
|
358
349
|
attempt: Int = 1
|
|
359
350
|
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
360
351
|
try {
|
|
361
|
-
// Read file content directly (no gzip)
|
|
362
352
|
val fileData = file.readBytes()
|
|
363
353
|
|
|
364
354
|
val mediaType = contentType.toMediaType()
|
|
@@ -368,7 +358,6 @@ class SegmentUploader(
|
|
|
368
358
|
.url(presignedUrl)
|
|
369
359
|
.put(body)
|
|
370
360
|
.header("Content-Type", contentType)
|
|
371
|
-
// Removed Content-Encoding: gzip
|
|
372
361
|
.build()
|
|
373
362
|
|
|
374
363
|
val response = client.newCall(request).execute()
|
|
@@ -401,7 +390,6 @@ class SegmentUploader(
|
|
|
401
390
|
attempt: Int = 1
|
|
402
391
|
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
403
392
|
try {
|
|
404
|
-
// Upload data directly (no gzip)
|
|
405
393
|
val mediaType = contentType.toMediaType()
|
|
406
394
|
val body = data.toRequestBody(mediaType)
|
|
407
395
|
|
|
@@ -409,7 +397,6 @@ class SegmentUploader(
|
|
|
409
397
|
.url(presignedUrl)
|
|
410
398
|
.put(body)
|
|
411
399
|
.header("Content-Type", contentType)
|
|
412
|
-
// Removed Content-Encoding: gzip
|
|
413
400
|
.build()
|
|
414
401
|
|
|
415
402
|
val response = client.newCall(request).execute()
|
|
@@ -506,7 +493,6 @@ class SegmentUploader(
|
|
|
506
493
|
}
|
|
507
494
|
|
|
508
495
|
private fun calculateBackoff(attempt: Int): Long {
|
|
509
|
-
// Exponential backoff: 1s, 2s, 4s, etc.
|
|
510
496
|
return (1000L * (1 shl (attempt - 1))).coerceAtMost(30000L)
|
|
511
497
|
}
|
|
512
498
|
}
|