@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.
Files changed (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,778 @@
1
+ //
2
+ // RJSegmentUploader.m
3
+ // Rejourney
4
+ //
5
+ // Video segment uploader implementation.
6
+ //
7
+ // Licensed under the Apache License, Version 2.0 (the "License");
8
+ // you may not use this file except in compliance with the License.
9
+ // You may obtain a copy of the License at
10
+ //
11
+ // http://www.apache.org/licenses/LICENSE-2.0
12
+ //
13
+ // Unless required by applicable law or agreed to in writing, software
14
+ // distributed under the License is distributed on an "AS IS" BASIS,
15
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ // See the License for the specific language governing permissions and
17
+ // limitations under the License.
18
+ //
19
+ // Copyright (c) 2026 Rejourney
20
+ //
21
+
22
+ #import "RJSegmentUploader.h"
23
+ #import "../Core/RJLogger.h"
24
+ #import "../Network/RJDeviceAuthManager.h"
25
+
26
+ #import <UIKit/UIKit.h>
27
+ #import <zlib.h>
28
+
29
+ @interface RJSegmentUploader () <NSURLSessionDelegate>
30
+
31
+ @property(nonatomic, strong) NSURLSession *session;
32
+
33
+ @property(nonatomic, strong) NSOperationQueue *uploadQueue;
34
+
35
+ @property(atomic, assign) NSInteger pendingUploadCount;
36
+
37
+ - (void)notifySegmentCompleteWithSegmentId:(NSString *)segmentId
38
+ sessionId:(NSString *)sessionId
39
+ startTime:(NSTimeInterval)startTime
40
+ endTime:(NSTimeInterval)endTime
41
+ frameCount:(NSInteger)frameCount
42
+ attempt:(NSInteger)attempt
43
+ completion:(void (^)(BOOL, NSError *))completion;
44
+
45
+ @end
46
+
47
+ @implementation RJSegmentUploader
48
+
49
+ #pragma mark - Initialization
50
+
51
+ - (instancetype)initWithBaseURL:(NSString *)baseURL {
52
+ self = [super init];
53
+ if (self) {
54
+ _baseURL = [baseURL copy];
55
+ _maxRetries = 3;
56
+ _deleteAfterUpload = YES;
57
+ _pendingUploadCount = 0;
58
+
59
+ NSURLSessionConfiguration *config =
60
+ [NSURLSessionConfiguration defaultSessionConfiguration];
61
+ config.timeoutIntervalForRequest = 60;
62
+ config.timeoutIntervalForResource = 300;
63
+ config.waitsForConnectivity = YES;
64
+
65
+ _uploadQueue = [[NSOperationQueue alloc] init];
66
+ _uploadQueue.maxConcurrentOperationCount = 2;
67
+ _uploadQueue.qualityOfService = NSQualityOfServiceBackground;
68
+
69
+ _session = [NSURLSession sessionWithConfiguration:config
70
+ delegate:self
71
+ delegateQueue:_uploadQueue];
72
+
73
+ // Clean up any orphaned segments from previous app runs
74
+ [self cleanupOrphanedSegments];
75
+ }
76
+ return self;
77
+ }
78
+
79
+ - (void)dealloc {
80
+ [self.session invalidateAndCancel];
81
+ }
82
+
83
+ #pragma mark - Properties
84
+
85
+ - (NSInteger)pendingUploads {
86
+ return self.pendingUploadCount;
87
+ }
88
+
89
+ #pragma mark - Upload Methods
90
+
91
+ - (void)uploadVideoSegment:(NSURL *)segmentURL
92
+ sessionId:(NSString *)sessionId
93
+ startTime:(NSTimeInterval)startTime
94
+ endTime:(NSTimeInterval)endTime
95
+ frameCount:(NSInteger)frameCount
96
+ completion:(RJSegmentUploadCompletion)completion {
97
+
98
+ // Start background task
99
+ __block UIBackgroundTaskIdentifier bgTask = [[UIApplication sharedApplication]
100
+ beginBackgroundTaskWithName:@"RJVideoUpload"
101
+ expirationHandler:^{
102
+ RJLogWarning(@"Background task for video upload expired!");
103
+ [[UIApplication sharedApplication] endBackgroundTask:bgTask];
104
+ bgTask = UIBackgroundTaskInvalid;
105
+ }];
106
+
107
+ // Helper to end background task safely
108
+ void (^endBackgroundTask)(void) = ^{
109
+ if (bgTask != UIBackgroundTaskInvalid) {
110
+ [[UIApplication sharedApplication] endBackgroundTask:bgTask];
111
+ bgTask = UIBackgroundTaskInvalid;
112
+ }
113
+ };
114
+
115
+ RJLogInfo(@"[RJ-UPLOAD] uploadVideoSegment called: %@, sessionId=%@, frames=%ld",
116
+ segmentURL.lastPathComponent, sessionId, (long)frameCount);
117
+ RJLogInfo(@"[RJ-UPLOAD] apiKey=%@, projectId=%@, baseURL=%@",
118
+ self.apiKey ? @"<set>" : @"<nil>", self.projectId, self.baseURL);
119
+
120
+ if (!self.apiKey || !self.projectId) {
121
+ RJLogInfo(@"[RJ-UPLOAD] ERROR: Missing apiKey or projectId!");
122
+ RJLogError(@"Segment uploader: Missing apiKey or projectId");
123
+ if (completion) {
124
+ completion(NO, [self errorWithMessage:@"Missing configuration"]);
125
+ }
126
+ endBackgroundTask();
127
+ return;
128
+ }
129
+
130
+ if (![[NSFileManager defaultManager] fileExistsAtPath:segmentURL.path]) {
131
+ RJLogInfo(@"[RJ-UPLOAD] ERROR: File not found at %@", segmentURL.path);
132
+ RJLogError(@"Segment uploader: File not found at %@", segmentURL.path);
133
+ if (completion) {
134
+ completion(NO, [self errorWithMessage:@"File not found"]);
135
+ }
136
+ endBackgroundTask();
137
+ return;
138
+ }
139
+
140
+ // CRITICAL: Read file data SYNCHRONOUSLY before any async operations.
141
+ // During app termination, the file may be deleted between getting the
142
+ // presigned URL and the actual S3 upload. By reading upfront, we ensure the
143
+ // data is in memory.
144
+ NSData *fileData = [NSData dataWithContentsOfURL:segmentURL];
145
+ if (!fileData || fileData.length == 0) {
146
+ RJLogInfo(@"[RJ-UPLOAD] ERROR: Failed to read file data from %@",
147
+ segmentURL.path);
148
+ RJLogError(@"Segment uploader: Failed to read file data");
149
+ if (completion) {
150
+ completion(NO, [self errorWithMessage:@"Failed to read file data"]);
151
+ }
152
+ endBackgroundTask();
153
+ return;
154
+ }
155
+
156
+ RJLogInfo(@"[RJ-UPLOAD] Read %lu bytes from file into memory",
157
+ (unsigned long)fileData.length);
158
+
159
+ self.pendingUploadCount++;
160
+
161
+ RJLogInfo(@"[RJ-UPLOAD] Requesting presigned URL for segment");
162
+ RJLogInfo(@"Segment uploader: Uploading segment %@ (%ld frames)",
163
+ segmentURL.lastPathComponent, (long)frameCount);
164
+
165
+ [self
166
+ requestPresignedURLForSession:sessionId
167
+ kind:@"video"
168
+ sizeBytes:fileData.length
169
+ startTime:startTime
170
+ endTime:endTime
171
+ frameCount:frameCount
172
+ compression:nil
173
+ completion:^(NSDictionary *presignInfo, NSError *error) {
174
+ if (error || !presignInfo) {
175
+ self.pendingUploadCount--;
176
+ RJLogInfo(@"[RJ-UPLOAD] ERROR: Failed to get "
177
+ @"presigned URL: %@",
178
+ error);
179
+ RJLogError(@"Segment uploader: Failed to get "
180
+ @"presigned URL: %@",
181
+ error);
182
+ if (completion) {
183
+ completion(NO, error);
184
+ }
185
+ endBackgroundTask();
186
+ return;
187
+ }
188
+
189
+ RJLogInfo(@"[RJ-UPLOAD] Got presignInfo: %@",
190
+ presignInfo);
191
+
192
+ NSString *presignedUrl =
193
+ presignInfo[@"presignedUrl"];
194
+ NSString *segmentId = presignInfo[@"segmentId"];
195
+ NSString *s3Key = presignInfo[@"s3Key"];
196
+
197
+ if (!presignedUrl) {
198
+ self.pendingUploadCount--;
199
+ RJLogInfo(@"[RJ-UPLOAD] ERROR: No presigned URL in "
200
+ @"response");
201
+ RJLogError(@"Segment uploader: No presigned URL "
202
+ @"in response");
203
+ if (completion) {
204
+ completion(
205
+ NO,
206
+ [self errorWithMessage:@"No presigned URL"]);
207
+ }
208
+ endBackgroundTask();
209
+ return;
210
+ }
211
+
212
+ RJLogInfo(@"[RJ-UPLOAD] Uploading to S3: %@, segmentId: "
213
+ @"%@",
214
+ presignedUrl, segmentId);
215
+
216
+ // Use uploadDataToS3 with pre-read data instead of
217
+ // uploadFileToS3 to avoid file-not-found errors
218
+ // during app termination
219
+ [self
220
+ uploadDataToS3:fileData
221
+ presignedURL:presignedUrl
222
+ contentType:@"video/mp4"
223
+ attempt:1
224
+ completion:^(BOOL success, NSError *uploadError) {
225
+ if (!success) {
226
+ self.pendingUploadCount--;
227
+ RJLogInfo(@"[RJ-UPLOAD] ERROR: S3 upload failed: %@", uploadError);
228
+ RJLogError(@"Segment uploader: S3 "
229
+ @"upload failed: %@",
230
+ uploadError);
231
+ if (completion) {
232
+ completion(NO, uploadError);
233
+ }
234
+ endBackgroundTask();
235
+ return;
236
+ }
237
+
238
+ RJLogInfo(@"[RJ-UPLOAD] S3 upload SUCCESS, "
239
+ @"calling segment/complete with "
240
+ @"segmentId: %@",
241
+ segmentId);
242
+
243
+ [self
244
+ notifySegmentCompleteWithSegmentId:
245
+ segmentId
246
+ sessionId:
247
+ sessionId
248
+ startTime:
249
+ startTime
250
+ endTime:
251
+ endTime
252
+ frameCount:
253
+ frameCount
254
+ completion:^(BOOL notifySuccess, NSError *notifyError) {
255
+ self.pendingUploadCount--;
256
+
257
+ if (notifySuccess) {
258
+ if (self.deleteAfterUpload) {
259
+ [[NSFileManager defaultManager] removeItemAtURL:segmentURL error:nil];
260
+ }
261
+ } else {
262
+ RJLogWarning(@"Segment uploader: Completion notification failed: %@", notifyError);
263
+ }
264
+
265
+ if (completion) {
266
+ completion(
267
+ notifySuccess,
268
+ notifyError);
269
+ }
270
+ endBackgroundTask();
271
+ }];
272
+ }];
273
+ }];
274
+ }
275
+
276
+ - (void)uploadHierarchy:(NSData *)hierarchyData
277
+ sessionId:(NSString *)sessionId
278
+ timestamp:(NSTimeInterval)timestamp
279
+ completion:(RJSegmentUploadCompletion)completion {
280
+
281
+ // Start background task
282
+ __block UIBackgroundTaskIdentifier bgTask = [[UIApplication sharedApplication]
283
+ beginBackgroundTaskWithName:@"RJHierarchyUpload"
284
+ expirationHandler:^{
285
+ RJLogWarning(
286
+ @"Background task for hierarchy upload expired!");
287
+ [[UIApplication sharedApplication] endBackgroundTask:bgTask];
288
+ bgTask = UIBackgroundTaskInvalid;
289
+ }];
290
+
291
+ // Helper to end background task safely
292
+ void (^endBackgroundTask)(void) = ^{
293
+ if (bgTask != UIBackgroundTaskInvalid) {
294
+ [[UIApplication sharedApplication] endBackgroundTask:bgTask];
295
+ bgTask = UIBackgroundTaskInvalid;
296
+ }
297
+ };
298
+
299
+ if (!self.apiKey || !self.projectId) {
300
+ if (completion) {
301
+ completion(NO, [self errorWithMessage:@"Missing configuration"]);
302
+ }
303
+ endBackgroundTask();
304
+ return;
305
+ }
306
+
307
+ // Compress data
308
+ NSData *compressedData = [self gzipData:hierarchyData];
309
+ if (!compressedData) {
310
+ if (completion) {
311
+ completion(NO,
312
+ [self errorWithMessage:@"Failed to compress hierarchy data"]);
313
+ }
314
+ endBackgroundTask();
315
+ return;
316
+ }
317
+
318
+ self.pendingUploadCount++;
319
+
320
+ [self
321
+ requestPresignedURLForSession:sessionId
322
+ kind:@"hierarchy"
323
+ sizeBytes:compressedData.length
324
+ startTime:timestamp
325
+ endTime:timestamp
326
+ frameCount:0
327
+ compression:@"gzip"
328
+ completion:^(NSDictionary *presignInfo, NSError *error) {
329
+ if (error || !presignInfo[@"presignedUrl"]) {
330
+ self.pendingUploadCount--;
331
+ if (completion) {
332
+ completion(NO, error);
333
+ }
334
+ endBackgroundTask();
335
+ return;
336
+ }
337
+
338
+ NSString *segmentId = presignInfo[@"segmentId"];
339
+
340
+ [self
341
+ uploadDataToS3:compressedData
342
+ presignedURL:presignInfo[@"presignedUrl"]
343
+ contentType:@"application/gzip"
344
+ attempt:1
345
+ completion:^(BOOL success, NSError *uploadError) {
346
+ if (!success) {
347
+ self.pendingUploadCount--;
348
+ if (completion) {
349
+ completion(NO, uploadError);
350
+ }
351
+ endBackgroundTask();
352
+ return;
353
+ }
354
+
355
+ [self
356
+ notifySegmentCompleteWithSegmentId:
357
+ segmentId
358
+ sessionId:
359
+ sessionId
360
+ startTime:
361
+ timestamp
362
+ endTime:
363
+ timestamp
364
+ frameCount:0
365
+ completion:^(BOOL notifySuccess, NSError *notifyError) {
366
+ self.pendingUploadCount--;
367
+
368
+ if (notifySuccess) {
369
+ RJLogDebug(@"Segment uploader: Hierarchy uploaded for timestamp %.0f", timestamp);
370
+ }
371
+
372
+ if (completion) {
373
+ completion(
374
+ notifySuccess,
375
+ notifyError);
376
+ }
377
+ endBackgroundTask();
378
+ }];
379
+ }];
380
+ }];
381
+ }
382
+
383
+ - (NSData *)gzipData:(NSData *)inputData {
384
+ if (inputData.length == 0)
385
+ return inputData;
386
+
387
+ z_stream zStream;
388
+ bzero(&zStream, sizeof(z_stream));
389
+
390
+ zStream.zalloc = Z_NULL;
391
+ zStream.zfree = Z_NULL;
392
+ zStream.opaque = Z_NULL;
393
+ zStream.next_in = (Bytef *)inputData.bytes;
394
+ zStream.avail_in = (uInt)inputData.length;
395
+ zStream.total_out = 0;
396
+
397
+ // deflateInit2(stream, level, method, windowBits, memLevel, strategy)
398
+ // windowBits + 16 enables gzip header/trailer
399
+ if (deflateInit2(&zStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8,
400
+ Z_DEFAULT_STRATEGY) != Z_OK) {
401
+ return nil;
402
+ }
403
+
404
+ // 16KB chunk size
405
+ NSMutableData *compressedData = [NSMutableData dataWithLength:16384];
406
+
407
+ do {
408
+ if (zStream.total_out >= compressedData.length) {
409
+ [compressedData increaseLengthBy:16384];
410
+ }
411
+
412
+ zStream.next_out = (Bytef *)compressedData.mutableBytes + zStream.total_out;
413
+ zStream.avail_out = (uInt)(compressedData.length - zStream.total_out);
414
+
415
+ int status = deflate(&zStream, Z_FINISH);
416
+
417
+ if (status == Z_STREAM_END) {
418
+ break;
419
+ } else if (status != Z_OK) {
420
+ deflateEnd(&zStream);
421
+ return nil;
422
+ }
423
+
424
+ } while (zStream.avail_out == 0);
425
+
426
+ deflateEnd(&zStream);
427
+ [compressedData setLength:zStream.total_out];
428
+
429
+ return compressedData;
430
+ }
431
+
432
+ - (void)cancelAllUploads {
433
+ [self.session getAllTasksWithCompletionHandler:^(
434
+ NSArray<__kindof NSURLSessionTask *> *tasks) {
435
+ for (NSURLSessionTask *task in tasks) {
436
+ [task cancel];
437
+ }
438
+ }];
439
+ self.pendingUploadCount = 0;
440
+ }
441
+
442
+ - (void)cleanupOrphanedSegments {
443
+ NSURL *tempDir = [[NSURL fileURLWithPath:NSTemporaryDirectory()]
444
+ URLByAppendingPathComponent:@"rj_segments"
445
+ isDirectory:YES];
446
+
447
+ NSArray *contents = [[NSFileManager defaultManager]
448
+ contentsOfDirectoryAtURL:tempDir
449
+ includingPropertiesForKeys:@[ NSURLCreationDateKey ]
450
+ options:0
451
+ error:nil];
452
+
453
+ if (!contents || contents.count == 0)
454
+ return;
455
+
456
+ NSDate *cutoff = [NSDate dateWithTimeIntervalSinceNow:-3600];
457
+
458
+ for (NSURL *fileURL in contents) {
459
+ NSDictionary *attrs =
460
+ [[NSFileManager defaultManager] attributesOfItemAtPath:fileURL.path
461
+ error:nil];
462
+ NSDate *creationDate = attrs[NSFileCreationDate];
463
+
464
+ if (creationDate && [creationDate compare:cutoff] == NSOrderedAscending) {
465
+ [[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil];
466
+ RJLogDebug(@"Segment uploader: Cleaned up orphaned segment %@",
467
+ fileURL.lastPathComponent);
468
+ }
469
+ }
470
+ }
471
+
472
+ #pragma mark - Private Methods
473
+
474
+ - (void)requestPresignedURLForSession:(NSString *)sessionId
475
+ kind:(NSString *)kind
476
+ sizeBytes:(NSUInteger)sizeBytes
477
+ startTime:(NSTimeInterval)startTime
478
+ endTime:(NSTimeInterval)endTime
479
+ frameCount:(NSInteger)frameCount
480
+ compression:(NSString *)compression
481
+ completion:
482
+ (void (^)(NSDictionary *, NSError *))completion {
483
+
484
+ NSString *urlString = [NSString
485
+ stringWithFormat:@"%@/api/ingest/segment/presign", self.baseURL];
486
+ NSURL *url = [NSURL URLWithString:urlString];
487
+
488
+ RJLogInfo(@"[RJ-UPLOAD] Requesting presigned URL: %@", urlString);
489
+
490
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
491
+ request.HTTPMethod = @"POST";
492
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
493
+
494
+ RJDeviceAuthManager *deviceAuth = [RJDeviceAuthManager sharedManager];
495
+ NSString *currentUploadToken = [deviceAuth currentUploadToken];
496
+
497
+ if (currentUploadToken.length > 0 && self.apiKey.length > 0) {
498
+ [request setValue:currentUploadToken forHTTPHeaderField:@"x-upload-token"];
499
+ [request setValue:self.apiKey forHTTPHeaderField:@"x-rejourney-key"];
500
+ RJLogInfo(@"[RJ-UPLOAD] Using device auth: uploadToken=<set>, publicKey=%@",
501
+ [self.apiKey substringToIndex:MIN(12, self.apiKey.length)]);
502
+ } else if (self.uploadToken.length > 0 && self.apiKey.length > 0) {
503
+
504
+ [request setValue:self.uploadToken forHTTPHeaderField:@"x-upload-token"];
505
+ [request setValue:self.apiKey forHTTPHeaderField:@"x-rejourney-key"];
506
+ RJLogInfo(@"[RJ-UPLOAD] Using stored upload token: publicKey=%@",
507
+ [self.apiKey substringToIndex:MIN(12, self.apiKey.length)]);
508
+ } else {
509
+
510
+ [request setValue:self.apiKey forHTTPHeaderField:@"x-api-key"];
511
+ RJLogInfo(@"[RJ-UPLOAD] WARNING: No upload token, using API key (may fail): "
512
+ @"apiKey=%@",
513
+ [self.apiKey substringToIndex:MIN(12, self.apiKey.length)]);
514
+ }
515
+
516
+ NSMutableDictionary *body = [NSMutableDictionary dictionaryWithDictionary:@{
517
+ @"sessionId" : sessionId,
518
+ @"kind" : kind,
519
+ @"sizeBytes" : @(sizeBytes),
520
+ @"startTime" : @(startTime),
521
+ @"endTime" : @(endTime),
522
+ @"frameCount" : @(frameCount),
523
+ }];
524
+
525
+ if (compression) {
526
+ body[@"compression"] = compression;
527
+ }
528
+
529
+ RJLogInfo(@"[RJ-UPLOAD] Request body: %@", body);
530
+
531
+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
532
+ options:0
533
+ error:nil];
534
+
535
+ NSURLSessionDataTask *task = [self.session
536
+ dataTaskWithRequest:request
537
+ completionHandler:^(NSData *data, NSURLResponse *response,
538
+ NSError *error) {
539
+ if (error) {
540
+ RJLogInfo(@"[RJ-UPLOAD] Presign request error: %@", error);
541
+ completion(nil, error);
542
+ return;
543
+ }
544
+
545
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
546
+ RJLogInfo(@"[RJ-UPLOAD] Presign response status: %ld",
547
+ (long)httpResponse.statusCode);
548
+
549
+ if (httpResponse.statusCode >= 400) {
550
+ NSString *responseBody =
551
+ [[NSString alloc] initWithData:data
552
+ encoding:NSUTF8StringEncoding];
553
+ RJLogInfo(@"[RJ-UPLOAD] Presign error response (HTTP %ld): %@",
554
+ (long)httpResponse.statusCode, responseBody);
555
+
556
+ // Log headers to see if we're missing CORS or Auth headers
557
+ RJLogInfo(@"[RJ-UPLOAD] Response Headers: %@",
558
+ httpResponse.allHeaderFields);
559
+
560
+ completion(
561
+ nil,
562
+ [self errorWithMessage:[NSString
563
+ stringWithFormat:@"HTTP %ld: %@",
564
+ (long)httpResponse
565
+ .statusCode,
566
+ responseBody]]);
567
+ return;
568
+ }
569
+
570
+ NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data
571
+ options:0
572
+ error:nil];
573
+ RJLogInfo(@"[RJ-UPLOAD] Presign success: s3Key=%@", json[@"s3Key"]);
574
+ completion(json, nil);
575
+ }];
576
+
577
+ [task resume];
578
+ }
579
+
580
+ - (void)uploadFileToS3:(NSURL *)fileURL
581
+ presignedURL:(NSString *)presignedURL
582
+ contentType:(NSString *)contentType
583
+ attempt:(NSInteger)attempt
584
+ completion:(void (^)(BOOL, NSError *))completion {
585
+
586
+ NSData *fileData = [NSData dataWithContentsOfURL:fileURL];
587
+ if (!fileData) {
588
+ completion(NO, [self errorWithMessage:@"Failed to read file"]);
589
+ return;
590
+ }
591
+
592
+ [self uploadDataToS3:fileData
593
+ presignedURL:presignedURL
594
+ contentType:contentType
595
+ attempt:attempt
596
+ completion:completion];
597
+ }
598
+
599
+ - (void)uploadDataToS3:(NSData *)data
600
+ presignedURL:(NSString *)presignedURL
601
+ contentType:(NSString *)contentType
602
+ attempt:(NSInteger)attempt
603
+ completion:(void (^)(BOOL, NSError *))completion {
604
+
605
+ NSURL *url = [NSURL URLWithString:presignedURL];
606
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
607
+ request.HTTPMethod = @"PUT";
608
+ request.HTTPBody = data;
609
+ [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
610
+ [request setValue:[@(data.length) stringValue]
611
+ forHTTPHeaderField:@"Content-Length"];
612
+
613
+ NSURLSessionDataTask *task = [self.session
614
+ dataTaskWithRequest:request
615
+ completionHandler:^(NSData *responseData, NSURLResponse *response,
616
+ NSError *error) {
617
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
618
+
619
+ if (error || httpResponse.statusCode >= 400) {
620
+
621
+ if (attempt < self.maxRetries) {
622
+ NSTimeInterval delay = pow(2, attempt);
623
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
624
+ (int64_t)(delay * NSEC_PER_SEC)),
625
+ dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0),
626
+ ^{
627
+ [self uploadDataToS3:data
628
+ presignedURL:presignedURL
629
+ contentType:contentType
630
+ attempt:attempt + 1
631
+ completion:completion];
632
+ });
633
+ } else {
634
+ completion(
635
+ NO,
636
+ error
637
+ ?: [self
638
+ errorWithMessage:
639
+ [NSString
640
+ stringWithFormat:@"S3 upload failed: %ld",
641
+ (long)httpResponse
642
+ .statusCode]]);
643
+ }
644
+ return;
645
+ }
646
+
647
+ completion(YES, nil);
648
+ }];
649
+
650
+ [task resume];
651
+ }
652
+
653
+ - (void)notifySegmentCompleteWithSegmentId:(NSString *)segmentId
654
+ sessionId:(NSString *)sessionId
655
+ startTime:(NSTimeInterval)startTime
656
+ endTime:(NSTimeInterval)endTime
657
+ frameCount:(NSInteger)frameCount
658
+ completion:
659
+ (void (^)(BOOL, NSError *))completion {
660
+ [self notifySegmentCompleteWithSegmentId:segmentId
661
+ sessionId:sessionId
662
+ startTime:startTime
663
+ endTime:endTime
664
+ frameCount:frameCount
665
+ attempt:1
666
+ completion:completion];
667
+ }
668
+
669
+ - (void)notifySegmentCompleteWithSegmentId:(NSString *)segmentId
670
+ sessionId:(NSString *)sessionId
671
+ startTime:(NSTimeInterval)startTime
672
+ endTime:(NSTimeInterval)endTime
673
+ frameCount:(NSInteger)frameCount
674
+ attempt:(NSInteger)attempt
675
+ completion:(void (^)(BOOL, NSError *))completion {
676
+ if (segmentId.length == 0 || sessionId.length == 0) {
677
+ NSError *error = [self errorWithMessage:@"Missing segmentId or sessionId"];
678
+ if (completion) {
679
+ completion(NO, error);
680
+ }
681
+ return;
682
+ }
683
+
684
+ NSString *urlString = [NSString
685
+ stringWithFormat:@"%@/api/ingest/segment/complete", self.baseURL];
686
+ NSURL *url = [NSURL URLWithString:urlString];
687
+
688
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
689
+ request.HTTPMethod = @"POST";
690
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
691
+
692
+ RJDeviceAuthManager *deviceAuth = [RJDeviceAuthManager sharedManager];
693
+ NSString *currentUploadToken = [deviceAuth currentUploadToken];
694
+
695
+ if (currentUploadToken.length > 0 && self.apiKey.length > 0) {
696
+ [request setValue:currentUploadToken forHTTPHeaderField:@"x-upload-token"];
697
+ [request setValue:self.apiKey forHTTPHeaderField:@"x-rejourney-key"];
698
+ } else {
699
+ [request setValue:self.apiKey forHTTPHeaderField:@"x-api-key"];
700
+ }
701
+
702
+ NSDictionary *body = @{
703
+ @"segmentId" : segmentId ?: @"",
704
+ @"sessionId" : sessionId,
705
+ @"frameCount" : @(frameCount),
706
+ };
707
+
708
+ RJLogInfo(@"[RJ-UPLOAD] segment/complete request: %@", body);
709
+
710
+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
711
+ options:0
712
+ error:nil];
713
+
714
+ NSURLSessionDataTask *task = [self.session
715
+ dataTaskWithRequest:request
716
+ completionHandler:^(NSData *data, NSURLResponse *response,
717
+ NSError *error) {
718
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
719
+ BOOL success = !error && httpResponse.statusCode < 400;
720
+
721
+ if (!success) {
722
+ NSString *responseBody =
723
+ [[NSString alloc] initWithData:data
724
+ encoding:NSUTF8StringEncoding];
725
+ RJLogInfo(@"[RJ-UPLOAD] Segment Completion Failed (HTTP %ld): %@",
726
+ (long)httpResponse.statusCode, responseBody);
727
+
728
+ if (attempt < self.maxRetries) {
729
+ NSTimeInterval delay = MIN(pow(2.0, attempt), 8.0);
730
+ dispatch_after(
731
+ dispatch_time(DISPATCH_TIME_NOW,
732
+ (int64_t)(delay * NSEC_PER_SEC)),
733
+ dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
734
+ [self notifySegmentCompleteWithSegmentId:segmentId
735
+ sessionId:sessionId
736
+ startTime:startTime
737
+ endTime:endTime
738
+ frameCount:frameCount
739
+ attempt:attempt + 1
740
+ completion:completion];
741
+ });
742
+ return;
743
+ }
744
+ } else {
745
+ RJLogInfo(@"[RJ-UPLOAD] Segment completion succeeded: %@",
746
+ segmentId);
747
+ }
748
+
749
+ NSError *finalError = error;
750
+ if (!success && !finalError) {
751
+ finalError = [self errorWithMessage:
752
+ [NSString stringWithFormat:
753
+ @"Segment completion failed (%ld)",
754
+ (long)httpResponse.statusCode]];
755
+ }
756
+
757
+ if (completion) {
758
+ completion(success, finalError);
759
+ }
760
+ }];
761
+
762
+ [task resume];
763
+ }
764
+
765
+ - (NSUInteger)fileSizeAtURL:(NSURL *)url {
766
+ NSDictionary *attrs =
767
+ [[NSFileManager defaultManager] attributesOfItemAtPath:url.path
768
+ error:nil];
769
+ return [attrs[NSFileSize] unsignedIntegerValue];
770
+ }
771
+
772
+ - (NSError *)errorWithMessage:(NSString *)message {
773
+ return [NSError errorWithDomain:@"RJSegmentUploader"
774
+ code:-1
775
+ userInfo:@{NSLocalizedDescriptionKey : message}];
776
+ }
777
+
778
+ @end