@rejourneyco/react-native 1.0.0
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/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +739 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uploads finished video segments to S3/R2 storage.
|
|
3
|
+
* Ported from iOS RJSegmentUploader.
|
|
4
|
+
*
|
|
5
|
+
* Uses the presigned URL flow:
|
|
6
|
+
* 1. Request presigned URL from backend
|
|
7
|
+
* 2. Upload directly to S3/R2
|
|
8
|
+
* 3. Notify backend of completion
|
|
9
|
+
*
|
|
10
|
+
* ## Features
|
|
11
|
+
* - Background upload support
|
|
12
|
+
* - Retry with exponential backoff
|
|
13
|
+
* - Queue management for multiple segments
|
|
14
|
+
* - Automatic cleanup of uploaded files
|
|
15
|
+
*
|
|
16
|
+
* Licensed under the Apache License, Version 2.0
|
|
17
|
+
* Copyright (c) 2026 Rejourney
|
|
18
|
+
*/
|
|
19
|
+
package com.rejourney.capture
|
|
20
|
+
|
|
21
|
+
import com.rejourney.core.Logger
|
|
22
|
+
import com.rejourney.network.HttpClientProvider
|
|
23
|
+
import kotlinx.coroutines.*
|
|
24
|
+
import okhttp3.*
|
|
25
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
26
|
+
import okhttp3.RequestBody.Companion.asRequestBody
|
|
27
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
28
|
+
import org.json.JSONObject
|
|
29
|
+
import java.io.ByteArrayOutputStream
|
|
30
|
+
import java.io.File
|
|
31
|
+
import java.io.IOException
|
|
32
|
+
import java.util.concurrent.TimeUnit
|
|
33
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
34
|
+
import java.util.zip.GZIPOutputStream
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Result type for segment upload operations.
|
|
38
|
+
*/
|
|
39
|
+
data class SegmentUploadResult(
|
|
40
|
+
val success: Boolean,
|
|
41
|
+
val error: String? = null
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Uploads video segments and hierarchy snapshots to cloud storage.
|
|
46
|
+
*/
|
|
47
|
+
class SegmentUploader(
|
|
48
|
+
/** Base URL for the Rejourney API */
|
|
49
|
+
var baseURL: String
|
|
50
|
+
) {
|
|
51
|
+
|
|
52
|
+
/** API key (public key rj_...) for authentication */
|
|
53
|
+
var apiKey: String? = null
|
|
54
|
+
|
|
55
|
+
/** Project ID for the current recording session */
|
|
56
|
+
var projectId: String? = null
|
|
57
|
+
|
|
58
|
+
/** Upload token from device auth for authenticated uploads */
|
|
59
|
+
var uploadToken: String? = null
|
|
60
|
+
|
|
61
|
+
/** Maximum number of retry attempts. Default: 3 */
|
|
62
|
+
var maxRetries: Int = 3
|
|
63
|
+
|
|
64
|
+
/** Whether to delete local files after successful upload. Default: true */
|
|
65
|
+
var deleteAfterUpload: Boolean = true
|
|
66
|
+
|
|
67
|
+
/** Number of uploads currently in progress */
|
|
68
|
+
val pendingUploads: Int
|
|
69
|
+
get() = pendingUploadCount.get()
|
|
70
|
+
|
|
71
|
+
private val pendingUploadCount = AtomicInteger(0)
|
|
72
|
+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
73
|
+
|
|
74
|
+
// Use shared client with longer timeouts for video uploads (SSL pinning removed)
|
|
75
|
+
private val client = HttpClientProvider.shared.newBuilder()
|
|
76
|
+
.connectTimeout(60, TimeUnit.SECONDS)
|
|
77
|
+
.readTimeout(120, TimeUnit.SECONDS)
|
|
78
|
+
.writeTimeout(120, TimeUnit.SECONDS)
|
|
79
|
+
.build()
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Uploads a video segment to cloud storage.
|
|
83
|
+
*
|
|
84
|
+
* @param segmentFile Local file path of the .mp4 segment
|
|
85
|
+
* @param sessionId Session identifier
|
|
86
|
+
* @param startTime Segment start time in epoch milliseconds
|
|
87
|
+
* @param endTime Segment end time in epoch milliseconds
|
|
88
|
+
* @param frameCount Number of frames in the segment
|
|
89
|
+
* @return SegmentUploadResult indicating success or failure
|
|
90
|
+
*/
|
|
91
|
+
suspend fun uploadVideoSegment(
|
|
92
|
+
segmentFile: File,
|
|
93
|
+
sessionId: String,
|
|
94
|
+
startTime: Long,
|
|
95
|
+
endTime: Long,
|
|
96
|
+
frameCount: Int
|
|
97
|
+
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
98
|
+
Logger.debug("[SegmentUploader] uploadVideoSegment: ${segmentFile.name}, sessionId=$sessionId, frames=$frameCount")
|
|
99
|
+
Logger.debug("[SegmentUploader] apiKey=${if (apiKey != null) "<set>" else "<nil>"}, projectId=$projectId")
|
|
100
|
+
|
|
101
|
+
val key = apiKey
|
|
102
|
+
val pid = projectId
|
|
103
|
+
|
|
104
|
+
if (key == null || pid == null) {
|
|
105
|
+
Logger.error("[SegmentUploader] Missing apiKey or projectId!")
|
|
106
|
+
return@withContext SegmentUploadResult(false, "Missing configuration")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!segmentFile.exists()) {
|
|
110
|
+
Logger.error("[SegmentUploader] File not found: ${segmentFile.absolutePath}")
|
|
111
|
+
return@withContext SegmentUploadResult(false, "File not found")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pendingUploadCount.incrementAndGet()
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Step 1: Request presigned URL from backend
|
|
118
|
+
Logger.debug("[SegmentUploader] Requesting presigned URL for segment")
|
|
119
|
+
val presignResult = requestPresignedURL(
|
|
120
|
+
sessionId = sessionId,
|
|
121
|
+
kind = "video",
|
|
122
|
+
sizeBytes = segmentFile.length(),
|
|
123
|
+
startTime = startTime,
|
|
124
|
+
endTime = endTime,
|
|
125
|
+
frameCount = frameCount
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if (!presignResult.success || presignResult.presignedUrl == null) {
|
|
129
|
+
Logger.error("[SegmentUploader] Failed to get presigned URL: ${presignResult.error}")
|
|
130
|
+
return@withContext SegmentUploadResult(false, presignResult.error ?: "Failed to get presigned URL")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
val presignedUrl = presignResult.presignedUrl
|
|
134
|
+
val segmentId = presignResult.segmentId
|
|
135
|
+
val s3Key = presignResult.s3Key
|
|
136
|
+
|
|
137
|
+
Logger.debug("[SegmentUploader] Got presigned URL, segmentId=$segmentId")
|
|
138
|
+
|
|
139
|
+
// Step 2: Upload directly to S3
|
|
140
|
+
Logger.debug("[SegmentUploader] Uploading to S3...")
|
|
141
|
+
val uploadResult = uploadFileToS3(segmentFile, presignedUrl, "video/mp4")
|
|
142
|
+
|
|
143
|
+
if (!uploadResult.success) {
|
|
144
|
+
Logger.error("[SegmentUploader] S3 upload failed: ${uploadResult.error}")
|
|
145
|
+
return@withContext SegmentUploadResult(false, uploadResult.error ?: "S3 upload failed")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Logger.debug("[SegmentUploader] S3 upload SUCCESS, calling segment/complete")
|
|
149
|
+
|
|
150
|
+
// Step 3: Notify backend of completion
|
|
151
|
+
val completeResult = notifySegmentComplete(
|
|
152
|
+
segmentId = segmentId!!,
|
|
153
|
+
sessionId = sessionId,
|
|
154
|
+
startTime = startTime,
|
|
155
|
+
endTime = endTime,
|
|
156
|
+
frameCount = frameCount
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if (completeResult.success) {
|
|
160
|
+
Logger.info("[SegmentUploader] Upload complete for ${segmentFile.name}")
|
|
161
|
+
if (deleteAfterUpload) {
|
|
162
|
+
segmentFile.delete()
|
|
163
|
+
}
|
|
164
|
+
SegmentUploadResult(true)
|
|
165
|
+
} else {
|
|
166
|
+
Logger.warning("[SegmentUploader] Completion notification failed: ${completeResult.error}")
|
|
167
|
+
SegmentUploadResult(false, completeResult.error)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
Logger.error("[SegmentUploader] Upload failed with exception", e)
|
|
172
|
+
SegmentUploadResult(false, e.message ?: "Unknown error")
|
|
173
|
+
} finally {
|
|
174
|
+
pendingUploadCount.decrementAndGet()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Uploads a view hierarchy snapshot to cloud storage.
|
|
180
|
+
*
|
|
181
|
+
* @param hierarchyData JSON data of the hierarchy snapshot
|
|
182
|
+
* @param sessionId Session identifier
|
|
183
|
+
* @param timestamp Snapshot timestamp in epoch milliseconds
|
|
184
|
+
* @return SegmentUploadResult indicating success or failure
|
|
185
|
+
*/
|
|
186
|
+
suspend fun uploadHierarchy(
|
|
187
|
+
hierarchyData: ByteArray,
|
|
188
|
+
sessionId: String,
|
|
189
|
+
timestamp: Long
|
|
190
|
+
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
191
|
+
val key = apiKey
|
|
192
|
+
val pid = projectId
|
|
193
|
+
|
|
194
|
+
if (key == null || pid == null) {
|
|
195
|
+
return@withContext SegmentUploadResult(false, "Missing configuration")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pendingUploadCount.incrementAndGet()
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Compress data
|
|
202
|
+
val compressedData = gzipData(hierarchyData)
|
|
203
|
+
|
|
204
|
+
// Request presigned URL for hierarchy upload with gzip compression
|
|
205
|
+
val presignResult = requestPresignedURL(
|
|
206
|
+
sessionId = sessionId,
|
|
207
|
+
kind = "hierarchy",
|
|
208
|
+
sizeBytes = compressedData.size.toLong(),
|
|
209
|
+
startTime = timestamp,
|
|
210
|
+
endTime = timestamp,
|
|
211
|
+
frameCount = 0,
|
|
212
|
+
compression = "gzip"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (!presignResult.success || presignResult.presignedUrl == null) {
|
|
216
|
+
Logger.error("[SegmentUploader] Failed to get presigned URL for hierarchy: ${presignResult.error}")
|
|
217
|
+
return@withContext SegmentUploadResult(false, presignResult.error ?: "Failed to get presigned URL")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
val presignedUrl = presignResult.presignedUrl
|
|
221
|
+
val segmentId = presignResult.segmentId
|
|
222
|
+
|
|
223
|
+
// Upload compressed data to S3
|
|
224
|
+
val uploadResult = uploadDataToS3(compressedData, presignedUrl, "application/gzip")
|
|
225
|
+
|
|
226
|
+
if (!uploadResult.success) {
|
|
227
|
+
Logger.error("[SegmentUploader] S3 upload failed for hierarchy: ${uploadResult.error}")
|
|
228
|
+
return@withContext SegmentUploadResult(false, uploadResult.error)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Notify backend of completion
|
|
232
|
+
val completeResult = notifySegmentComplete(
|
|
233
|
+
segmentId = segmentId!!,
|
|
234
|
+
sessionId = sessionId,
|
|
235
|
+
startTime = timestamp,
|
|
236
|
+
endTime = timestamp,
|
|
237
|
+
frameCount = 0
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if (completeResult.success) {
|
|
241
|
+
Logger.debug("[SegmentUploader] Hierarchy uploaded for $sessionId at $timestamp")
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
SegmentUploadResult(completeResult.success, completeResult.error)
|
|
245
|
+
|
|
246
|
+
} catch (e: Exception) {
|
|
247
|
+
Logger.error("[SegmentUploader] Hierarchy upload failed", e)
|
|
248
|
+
SegmentUploadResult(false, e.message)
|
|
249
|
+
} finally {
|
|
250
|
+
pendingUploadCount.decrementAndGet()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Cancels all pending uploads.
|
|
256
|
+
*/
|
|
257
|
+
fun cancelAllUploads() {
|
|
258
|
+
scope.coroutineContext.cancelChildren()
|
|
259
|
+
pendingUploadCount.set(0)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Cleans up any leftover segment files from previous sessions.
|
|
264
|
+
*/
|
|
265
|
+
fun cleanupOrphanedSegments(segmentDir: File) {
|
|
266
|
+
try {
|
|
267
|
+
val cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000) // 24 hours ago
|
|
268
|
+
segmentDir.listFiles()?.forEach { file ->
|
|
269
|
+
if (file.isFile && file.lastModified() < cutoffTime) {
|
|
270
|
+
file.delete()
|
|
271
|
+
Logger.debug("[SegmentUploader] Cleaned up orphaned segment: ${file.name}")
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (e: Exception) {
|
|
275
|
+
Logger.debug("[SegmentUploader] Error cleaning orphaned segments: ${e.message}")
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ==================== Private Methods ====================
|
|
280
|
+
|
|
281
|
+
private data class PresignResult(
|
|
282
|
+
val success: Boolean,
|
|
283
|
+
val presignedUrl: String? = null,
|
|
284
|
+
val segmentId: String? = null,
|
|
285
|
+
val s3Key: String? = null,
|
|
286
|
+
val error: String? = null
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
private suspend fun requestPresignedURL(
|
|
290
|
+
sessionId: String,
|
|
291
|
+
kind: String,
|
|
292
|
+
sizeBytes: Long,
|
|
293
|
+
startTime: Long,
|
|
294
|
+
endTime: Long,
|
|
295
|
+
frameCount: Int,
|
|
296
|
+
compression: String? = null
|
|
297
|
+
): PresignResult = withContext(Dispatchers.IO) {
|
|
298
|
+
val url = "$baseURL/api/ingest/segment/presign"
|
|
299
|
+
|
|
300
|
+
val json = JSONObject().apply {
|
|
301
|
+
put("sessionId", sessionId)
|
|
302
|
+
put("kind", kind)
|
|
303
|
+
put("sizeBytes", sizeBytes)
|
|
304
|
+
put("startTime", startTime)
|
|
305
|
+
put("endTime", endTime)
|
|
306
|
+
put("frameCount", frameCount)
|
|
307
|
+
if (compression != null) {
|
|
308
|
+
put("compression", compression)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
val mediaType = "application/json; charset=utf-8".toMediaType()
|
|
313
|
+
val body = json.toString().toRequestBody(mediaType)
|
|
314
|
+
|
|
315
|
+
val requestBuilder = Request.Builder()
|
|
316
|
+
.url(url)
|
|
317
|
+
.post(body)
|
|
318
|
+
.header("Content-Type", "application/json")
|
|
319
|
+
|
|
320
|
+
val token = uploadToken
|
|
321
|
+
val key = apiKey
|
|
322
|
+
if (!token.isNullOrEmpty() && !key.isNullOrEmpty()) {
|
|
323
|
+
requestBuilder.header("x-upload-token", token)
|
|
324
|
+
requestBuilder.header("x-rejourney-key", key)
|
|
325
|
+
} else if (!key.isNullOrEmpty()) {
|
|
326
|
+
requestBuilder.header("x-api-key", key)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
val request = requestBuilder.build()
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
val response = client.newCall(request).execute()
|
|
333
|
+
val responseBody = response.body?.string()
|
|
334
|
+
|
|
335
|
+
if (response.isSuccessful && responseBody != null) {
|
|
336
|
+
val jsonResponse = JSONObject(responseBody)
|
|
337
|
+
PresignResult(
|
|
338
|
+
success = true,
|
|
339
|
+
presignedUrl = jsonResponse.optString("presignedUrl"),
|
|
340
|
+
segmentId = jsonResponse.optString("segmentId"),
|
|
341
|
+
s3Key = jsonResponse.optString("s3Key")
|
|
342
|
+
)
|
|
343
|
+
} else {
|
|
344
|
+
PresignResult(
|
|
345
|
+
success = false,
|
|
346
|
+
error = "HTTP ${response.code}: ${responseBody ?: "No response body"}"
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
} catch (e: Exception) {
|
|
350
|
+
PresignResult(success = false, error = e.message)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private suspend fun uploadFileToS3(
|
|
355
|
+
file: File,
|
|
356
|
+
presignedUrl: String,
|
|
357
|
+
contentType: String,
|
|
358
|
+
attempt: Int = 1
|
|
359
|
+
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
360
|
+
try {
|
|
361
|
+
// Read file content directly (no gzip)
|
|
362
|
+
val fileData = file.readBytes()
|
|
363
|
+
|
|
364
|
+
val mediaType = contentType.toMediaType()
|
|
365
|
+
val body = fileData.toRequestBody(mediaType)
|
|
366
|
+
|
|
367
|
+
val request = Request.Builder()
|
|
368
|
+
.url(presignedUrl)
|
|
369
|
+
.put(body)
|
|
370
|
+
.header("Content-Type", contentType)
|
|
371
|
+
// Removed Content-Encoding: gzip
|
|
372
|
+
.build()
|
|
373
|
+
|
|
374
|
+
val response = client.newCall(request).execute()
|
|
375
|
+
|
|
376
|
+
if (response.isSuccessful) {
|
|
377
|
+
SegmentUploadResult(true)
|
|
378
|
+
} else {
|
|
379
|
+
val error = "S3 upload failed: HTTP ${response.code}"
|
|
380
|
+
if (attempt < maxRetries) {
|
|
381
|
+
delay(calculateBackoff(attempt))
|
|
382
|
+
uploadFileToS3(file, presignedUrl, contentType, attempt + 1)
|
|
383
|
+
} else {
|
|
384
|
+
SegmentUploadResult(false, error)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (e: IOException) {
|
|
388
|
+
if (attempt < maxRetries) {
|
|
389
|
+
delay(calculateBackoff(attempt))
|
|
390
|
+
uploadFileToS3(file, presignedUrl, contentType, attempt + 1)
|
|
391
|
+
} else {
|
|
392
|
+
SegmentUploadResult(false, e.message)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private suspend fun uploadDataToS3(
|
|
398
|
+
data: ByteArray,
|
|
399
|
+
presignedUrl: String,
|
|
400
|
+
contentType: String,
|
|
401
|
+
attempt: Int = 1
|
|
402
|
+
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
403
|
+
try {
|
|
404
|
+
// Upload data directly (no gzip)
|
|
405
|
+
val mediaType = contentType.toMediaType()
|
|
406
|
+
val body = data.toRequestBody(mediaType)
|
|
407
|
+
|
|
408
|
+
val request = Request.Builder()
|
|
409
|
+
.url(presignedUrl)
|
|
410
|
+
.put(body)
|
|
411
|
+
.header("Content-Type", contentType)
|
|
412
|
+
// Removed Content-Encoding: gzip
|
|
413
|
+
.build()
|
|
414
|
+
|
|
415
|
+
val response = client.newCall(request).execute()
|
|
416
|
+
|
|
417
|
+
if (response.isSuccessful) {
|
|
418
|
+
SegmentUploadResult(true)
|
|
419
|
+
} else {
|
|
420
|
+
val error = "S3 upload failed: HTTP ${response.code}"
|
|
421
|
+
if (attempt < maxRetries) {
|
|
422
|
+
delay(calculateBackoff(attempt))
|
|
423
|
+
uploadDataToS3(data, presignedUrl, contentType, attempt + 1)
|
|
424
|
+
} else {
|
|
425
|
+
SegmentUploadResult(false, error)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} catch (e: IOException) {
|
|
429
|
+
if (attempt < maxRetries) {
|
|
430
|
+
delay(calculateBackoff(attempt))
|
|
431
|
+
uploadDataToS3(data, presignedUrl, contentType, attempt + 1)
|
|
432
|
+
} else {
|
|
433
|
+
SegmentUploadResult(false, e.message)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private suspend fun notifySegmentComplete(
|
|
439
|
+
segmentId: String,
|
|
440
|
+
sessionId: String,
|
|
441
|
+
startTime: Long,
|
|
442
|
+
endTime: Long,
|
|
443
|
+
frameCount: Int,
|
|
444
|
+
attempt: Int = 1
|
|
445
|
+
): SegmentUploadResult = withContext(Dispatchers.IO) {
|
|
446
|
+
val url = "$baseURL/api/ingest/segment/complete"
|
|
447
|
+
|
|
448
|
+
val json = JSONObject().apply {
|
|
449
|
+
put("segmentId", segmentId)
|
|
450
|
+
put("sessionId", sessionId)
|
|
451
|
+
put("projectId", projectId)
|
|
452
|
+
put("startTime", startTime)
|
|
453
|
+
put("endTime", endTime)
|
|
454
|
+
put("frameCount", frameCount)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
val mediaType = "application/json; charset=utf-8".toMediaType()
|
|
458
|
+
val body = json.toString().toRequestBody(mediaType)
|
|
459
|
+
|
|
460
|
+
val requestBuilder = Request.Builder()
|
|
461
|
+
.url(url)
|
|
462
|
+
.post(body)
|
|
463
|
+
.header("Content-Type", "application/json")
|
|
464
|
+
|
|
465
|
+
val token = uploadToken
|
|
466
|
+
val key = apiKey
|
|
467
|
+
if (!token.isNullOrEmpty() && !key.isNullOrEmpty()) {
|
|
468
|
+
requestBuilder.header("x-upload-token", token)
|
|
469
|
+
requestBuilder.header("x-rejourney-key", key)
|
|
470
|
+
} else if (!key.isNullOrEmpty()) {
|
|
471
|
+
requestBuilder.header("x-api-key", key)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
val request = requestBuilder.build()
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
val response = client.newCall(request).execute()
|
|
478
|
+
|
|
479
|
+
if (response.isSuccessful) {
|
|
480
|
+
SegmentUploadResult(true)
|
|
481
|
+
} else {
|
|
482
|
+
val error = "Completion notification failed: HTTP ${response.code}"
|
|
483
|
+
if (attempt < maxRetries) {
|
|
484
|
+
delay(calculateBackoff(attempt))
|
|
485
|
+
notifySegmentComplete(segmentId, sessionId, startTime, endTime, frameCount, attempt + 1)
|
|
486
|
+
} else {
|
|
487
|
+
SegmentUploadResult(false, error)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch (e: IOException) {
|
|
491
|
+
if (attempt < maxRetries) {
|
|
492
|
+
delay(calculateBackoff(attempt))
|
|
493
|
+
notifySegmentComplete(segmentId, sessionId, startTime, endTime, frameCount, attempt + 1)
|
|
494
|
+
} else {
|
|
495
|
+
SegmentUploadResult(false, e.message)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private fun gzipData(data: ByteArray): ByteArray {
|
|
501
|
+
val bos = ByteArrayOutputStream()
|
|
502
|
+
GZIPOutputStream(bos).use { gzip ->
|
|
503
|
+
gzip.write(data)
|
|
504
|
+
}
|
|
505
|
+
return bos.toByteArray()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private fun calculateBackoff(attempt: Int): Long {
|
|
509
|
+
// Exponential backoff: 1s, 2s, 4s, etc.
|
|
510
|
+
return (1000L * (1 shl (attempt - 1))).coerceAtMost(30000L)
|
|
511
|
+
}
|
|
512
|
+
}
|