@rejourneyco/react-native 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +35 -29
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
- package/ios/Capture/RJCaptureEngine.m +3 -34
- package/ios/Capture/RJVideoEncoder.m +0 -26
- package/ios/Core/Rejourney.mm +32 -95
- package/ios/Utils/RJPerfTiming.m +0 -5
- package/ios/Utils/RJWindowUtils.m +0 -1
- package/lib/commonjs/components/Mask.js +1 -6
- package/lib/commonjs/index.js +4 -87
- package/lib/commonjs/sdk/autoTracking.js +39 -310
- package/lib/commonjs/sdk/constants.js +2 -13
- package/lib/commonjs/sdk/errorTracking.js +1 -29
- package/lib/commonjs/sdk/metricsTracking.js +3 -24
- package/lib/commonjs/sdk/navigation.js +3 -42
- package/lib/commonjs/sdk/networkInterceptor.js +7 -49
- package/lib/commonjs/sdk/utils.js +0 -5
- package/lib/module/components/Mask.js +1 -6
- package/lib/module/index.js +3 -91
- package/lib/module/sdk/autoTracking.js +39 -311
- package/lib/module/sdk/constants.js +2 -13
- package/lib/module/sdk/errorTracking.js +1 -29
- package/lib/module/sdk/index.js +0 -2
- package/lib/module/sdk/metricsTracking.js +3 -24
- package/lib/module/sdk/navigation.js +3 -42
- package/lib/module/sdk/networkInterceptor.js +7 -49
- package/lib/module/sdk/utils.js +0 -5
- package/lib/typescript/NativeRejourney.d.ts +1 -0
- package/lib/typescript/sdk/autoTracking.d.ts +4 -4
- package/lib/typescript/types/index.d.ts +0 -1
- package/package.json +2 -8
- package/src/NativeRejourney.ts +2 -0
- package/src/components/Mask.tsx +0 -3
- package/src/index.ts +3 -73
- package/src/sdk/autoTracking.ts +51 -282
- package/src/sdk/constants.ts +13 -13
- package/src/sdk/errorTracking.ts +1 -17
- package/src/sdk/index.ts +0 -2
- package/src/sdk/metricsTracking.ts +5 -33
- package/src/sdk/navigation.ts +8 -29
- package/src/sdk/networkInterceptor.ts +9 -33
- package/src/sdk/utils.ts +0 -5
- package/src/types/index.ts +0 -29
|
@@ -57,7 +57,6 @@ import java.io.File
|
|
|
57
57
|
import java.util.*
|
|
58
58
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
59
59
|
|
|
60
|
-
// Enum for session end reasons
|
|
61
60
|
enum class EndReason {
|
|
62
61
|
SESSION_TIMEOUT,
|
|
63
62
|
MANUAL_STOP,
|
|
@@ -107,7 +106,6 @@ class RejourneyModuleImpl(
|
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
// Core components
|
|
111
109
|
private var captureEngine: CaptureEngine? = null
|
|
112
110
|
private var uploadManager: UploadManager? = null
|
|
113
111
|
private var touchInterceptor: TouchInterceptor? = null
|
|
@@ -116,7 +114,6 @@ class RejourneyModuleImpl(
|
|
|
116
114
|
private var keyboardTracker: KeyboardTracker? = null
|
|
117
115
|
private var textInputTracker: TextInputTracker? = null
|
|
118
116
|
|
|
119
|
-
// Session state
|
|
120
117
|
private var currentSessionId: String? = null
|
|
121
118
|
private var userId: String? = null
|
|
122
119
|
@Volatile private var isRecording: Boolean = false
|
|
@@ -134,59 +131,43 @@ class RejourneyModuleImpl(
|
|
|
134
131
|
private var maxRecordingMinutes: Int = 10
|
|
135
132
|
@Volatile private var sessionEndSent: Boolean = false
|
|
136
133
|
|
|
137
|
-
// Keyboard state
|
|
138
134
|
private var keyPressCount: Int = 0
|
|
139
135
|
private var isKeyboardVisible: Boolean = false
|
|
140
136
|
private var lastKeyboardHeight: Int = 0
|
|
141
137
|
|
|
142
|
-
// Session state saved on background - used to restore on foreground if within timeout
|
|
143
138
|
private var savedApiUrl: String = ""
|
|
144
139
|
private var savedPublicKey: String = ""
|
|
145
140
|
private var savedDeviceHash: String = ""
|
|
146
141
|
|
|
147
|
-
// Events buffer
|
|
148
142
|
private val sessionEvents = CopyOnWriteArrayList<Map<String, Any?>>()
|
|
149
143
|
|
|
150
|
-
// Throttle immediate upload kicks (ms)
|
|
151
144
|
@Volatile private var lastImmediateUploadKickMs: Long = 0
|
|
152
145
|
|
|
153
|
-
// Write-first event buffer for crash-safe persistence
|
|
154
146
|
private var eventBuffer: EventBuffer? = null
|
|
155
147
|
|
|
156
|
-
// Coroutine scope for async operations
|
|
157
148
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
158
149
|
|
|
159
|
-
// Dedicated scope for background flush - survives independently of main scope
|
|
160
|
-
// This prevents cancellation when app goes to background
|
|
161
150
|
private val backgroundScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
162
151
|
|
|
163
|
-
// Timer jobs
|
|
164
152
|
private var batchUploadJob: Job? = null
|
|
165
153
|
private var durationLimitJob: Job? = null
|
|
166
154
|
|
|
167
|
-
// Main thread handler for posting delayed tasks
|
|
168
155
|
private val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
169
156
|
|
|
170
|
-
// Debounced background detection (prevents transient pauses from ending sessions)
|
|
171
157
|
private var scheduledBackgroundRunnable: Runnable? = null
|
|
172
158
|
private var backgroundScheduled: Boolean = false
|
|
173
159
|
|
|
174
|
-
// Safety flag
|
|
175
160
|
@Volatile private var isShuttingDown = false
|
|
176
161
|
|
|
177
|
-
// Auth resilience - retry mechanism
|
|
178
162
|
private var authRetryCount = 0
|
|
179
163
|
private var authPermanentlyFailed = false
|
|
180
164
|
private var authRetryJob: Job? = null
|
|
181
165
|
|
|
182
166
|
init {
|
|
183
167
|
// DO NOT initialize anything here that could throw exceptions
|
|
184
|
-
// React Native needs the module constructor to complete cleanly
|
|
185
|
-
// All initialization will happen lazily on first method call
|
|
186
168
|
Logger.debug("RejourneyModuleImpl constructor completed")
|
|
187
169
|
}
|
|
188
170
|
|
|
189
|
-
// Lazy initialization flag
|
|
190
171
|
@Volatile
|
|
191
172
|
private var isInitialized = false
|
|
192
173
|
private val initLock = Any()
|
|
@@ -237,21 +218,19 @@ class RejourneyModuleImpl(
|
|
|
237
218
|
Logger.error("Failed to schedule recovery upload (non-critical)", e)
|
|
238
219
|
}
|
|
239
220
|
|
|
240
|
-
//
|
|
221
|
+
// Che ck if app was killed in previous session (Android 11+)
|
|
241
222
|
try {
|
|
242
223
|
checkPreviousAppKill()
|
|
243
224
|
} catch (e: Exception) {
|
|
244
225
|
Logger.error("Failed to check previous app kill (non-critical)", e)
|
|
245
226
|
}
|
|
246
227
|
|
|
247
|
-
// Check for unclosed sessions from previous launch
|
|
248
228
|
try {
|
|
249
229
|
checkForUnclosedSessions()
|
|
250
230
|
} catch (e: Exception) {
|
|
251
231
|
Logger.error("Failed to check for unclosed sessions (non-critical)", e)
|
|
252
232
|
}
|
|
253
233
|
|
|
254
|
-
// Log OEM information for debugging
|
|
255
234
|
val oem = OEMDetector.getOEM()
|
|
256
235
|
Logger.debug("Device OEM: $oem")
|
|
257
236
|
Logger.debug("OEM Recommendations: ${OEMDetector.getRecommendations()}")
|
|
@@ -291,7 +270,6 @@ class RejourneyModuleImpl(
|
|
|
291
270
|
Logger.error("Failed to set up task removed listener (non-critical)", e)
|
|
292
271
|
}
|
|
293
272
|
|
|
294
|
-
// Use lifecycle log - only shown in debug builds
|
|
295
273
|
Logger.logInitSuccess(Constants.SDK_VERSION)
|
|
296
274
|
|
|
297
275
|
isInitialized = true
|
|
@@ -313,8 +291,6 @@ class RejourneyModuleImpl(
|
|
|
313
291
|
|
|
314
292
|
Logger.debug("[Rejourney] addEventWithPersistence: type=$eventType, sessionId=$sessionId, inMemoryCount=${sessionEvents.size + 1}")
|
|
315
293
|
|
|
316
|
-
// CRITICAL: Write to disk immediately for crash safety
|
|
317
|
-
// This ensures events are never lost even if app is force-killed
|
|
318
294
|
val bufferSuccess = eventBuffer?.appendEvent(event) ?: false
|
|
319
295
|
if (!bufferSuccess) {
|
|
320
296
|
Logger.warning("[Rejourney] addEventWithPersistence: Failed to append event to buffer: type=$eventType")
|
|
@@ -322,7 +298,6 @@ class RejourneyModuleImpl(
|
|
|
322
298
|
Logger.debug("[Rejourney] addEventWithPersistence: Event appended to buffer: type=$eventType")
|
|
323
299
|
}
|
|
324
300
|
|
|
325
|
-
// Also add to in-memory buffer for batched upload
|
|
326
301
|
sessionEvents.add(event)
|
|
327
302
|
Logger.debug("[Rejourney] addEventWithPersistence: Event added to in-memory list: type=$eventType, totalInMemory=${sessionEvents.size}")
|
|
328
303
|
}
|
|
@@ -332,14 +307,12 @@ class RejourneyModuleImpl(
|
|
|
332
307
|
* This is more reliable than Activity lifecycle callbacks.
|
|
333
308
|
*/
|
|
334
309
|
private fun registerProcessLifecycleObserver() {
|
|
335
|
-
// Must run on main thread
|
|
336
310
|
Handler(Looper.getMainLooper()).post {
|
|
337
311
|
try {
|
|
338
312
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
339
313
|
Logger.debug("ProcessLifecycleOwner observer registered")
|
|
340
314
|
} catch (e: Exception) {
|
|
341
315
|
Logger.error("Failed to register ProcessLifecycleOwner observer (non-critical)", e)
|
|
342
|
-
// This is non-critical - we can still use Activity lifecycle callbacks as fallback
|
|
343
316
|
}
|
|
344
317
|
}
|
|
345
318
|
}
|
|
@@ -472,7 +445,40 @@ class RejourneyModuleImpl(
|
|
|
472
445
|
}
|
|
473
446
|
}
|
|
474
447
|
|
|
475
|
-
|
|
448
|
+
fun getDeviceInfo(promise: Promise) {
|
|
449
|
+
try {
|
|
450
|
+
val map = Arguments.createMap()
|
|
451
|
+
map.putString("model", android.os.Build.MODEL)
|
|
452
|
+
map.putString("brand", android.os.Build.MANUFACTURER)
|
|
453
|
+
map.putString("systemName", "Android")
|
|
454
|
+
map.putString("systemVersion", android.os.Build.VERSION.RELEASE)
|
|
455
|
+
map.putString("bundleId", reactContext.packageName)
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
val pInfo = reactContext.packageManager.getPackageInfo(reactContext.packageName, 0)
|
|
459
|
+
map.putString("appVersion", pInfo.versionName)
|
|
460
|
+
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
|
461
|
+
map.putString("buildNumber", pInfo.longVersionCode.toString())
|
|
462
|
+
} else {
|
|
463
|
+
@Suppress("DEPRECATION")
|
|
464
|
+
map.putString("buildNumber", pInfo.versionCode.toString())
|
|
465
|
+
}
|
|
466
|
+
} catch (e: Exception) {
|
|
467
|
+
map.putString("appVersion", "unknown")
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
map.putBoolean("isTablet", isTablet())
|
|
471
|
+
promise.resolve(map)
|
|
472
|
+
} catch (e: Exception) {
|
|
473
|
+
promise.reject("DEVICE_INFO_ERROR", e)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private fun isTablet(): Boolean {
|
|
478
|
+
val configuration = reactContext.resources.configuration
|
|
479
|
+
return (configuration.screenLayout and android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK) >=
|
|
480
|
+
android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
|
|
481
|
+
}
|
|
476
482
|
|
|
477
483
|
fun startSession(userId: String, apiUrl: String, publicKey: String, promise: Promise) {
|
|
478
484
|
ensureInitialized() // Lazy init on first call
|
|
@@ -132,6 +132,13 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
132
132
|
instance.getSDKMetrics(promise)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
@ReactMethod
|
|
136
|
+
@DoNotStrip
|
|
137
|
+
override fun getDeviceInfo(promise: Promise) {
|
|
138
|
+
val instance = getImplOrReject(promise) ?: return
|
|
139
|
+
instance.getDeviceInfo(promise)
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
@ReactMethod
|
|
136
143
|
@DoNotStrip
|
|
137
144
|
override fun debugCrash() {
|
|
@@ -128,6 +128,15 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
@ReactMethod
|
|
132
|
+
fun getDeviceInfo(promise: Promise) {
|
|
133
|
+
try {
|
|
134
|
+
impl.getDeviceInfo(promise)
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
promise.resolve(Arguments.createMap())
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
@ReactMethod
|
|
132
141
|
fun debugCrash() {
|
|
133
142
|
try {
|
|
@@ -1155,9 +1155,6 @@ typedef struct {
|
|
|
1155
1155
|
NSTimeInterval now = CACurrentMediaTime();
|
|
1156
1156
|
|
|
1157
1157
|
if (self.pendingCapture) {
|
|
1158
|
-
// If we have a pending capture, force it to resolve now.
|
|
1159
|
-
// If the new request is High importance, we effectively "upgrade" the
|
|
1160
|
-
// current cycle by ensuring it runs immediately.
|
|
1161
1158
|
self.pendingCapture.deadline = now;
|
|
1162
1159
|
[self attemptPendingCapture:self.pendingCapture fullScan:NO];
|
|
1163
1160
|
}
|
|
@@ -1173,8 +1170,6 @@ typedef struct {
|
|
|
1173
1170
|
grace = MIN(grace, 0.3);
|
|
1174
1171
|
}
|
|
1175
1172
|
|
|
1176
|
-
// High importance requests should have a shorter grace, forcing faster
|
|
1177
|
-
// resolution
|
|
1178
1173
|
if (isCritical) {
|
|
1179
1174
|
grace = MIN(grace, 0.1);
|
|
1180
1175
|
}
|
|
@@ -1731,7 +1726,6 @@ typedef struct {
|
|
|
1731
1726
|
}
|
|
1732
1727
|
|
|
1733
1728
|
if (self.internalPerformanceLevel == RJPerformanceLevelMinimal) {
|
|
1734
|
-
// Minimal mode trades quality for speed
|
|
1735
1729
|
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
|
|
1736
1730
|
CGContextSetShouldAntialias(context, false);
|
|
1737
1731
|
CGContextSetAllowsAntialiasing(context, false);
|
|
@@ -1741,17 +1735,13 @@ typedef struct {
|
|
|
1741
1735
|
CGContextSetAllowsAntialiasing(context, true);
|
|
1742
1736
|
}
|
|
1743
1737
|
|
|
1744
|
-
// Set up context transform (flip for UIKit coordinates)
|
|
1745
1738
|
CGContextScaleCTM(context, contextScale, -contextScale);
|
|
1746
1739
|
CGContextTranslateCTM(context, 0, -sizePoints.height);
|
|
1747
1740
|
|
|
1748
|
-
// Optimization #9: Fast Memset Clear (White = 0xFF)
|
|
1749
|
-
// Much faster than CGContextFillRect
|
|
1750
1741
|
memset(baseAddress, 0xFF, bytesPerRow * height);
|
|
1751
1742
|
|
|
1752
1743
|
UIGraphicsPushContext(context);
|
|
1753
1744
|
|
|
1754
|
-
// ===== RENDERING: ALWAYS USE drawViewHierarchyInRect =====
|
|
1755
1745
|
RJ_TIME_START_NAMED(render);
|
|
1756
1746
|
BOOL didDraw = NO;
|
|
1757
1747
|
@try {
|
|
@@ -1772,10 +1762,6 @@ typedef struct {
|
|
|
1772
1762
|
return NULL;
|
|
1773
1763
|
}
|
|
1774
1764
|
|
|
1775
|
-
// Recalculate effective scale so consumers (PrivacyMask) know the real
|
|
1776
|
-
// mapping Used by caller to pass to applyToPixelBuffer Note: we don't need to
|
|
1777
|
-
// return it, caller has it.
|
|
1778
|
-
|
|
1779
1765
|
return pixelBuffer;
|
|
1780
1766
|
}
|
|
1781
1767
|
|
|
@@ -1815,9 +1801,6 @@ typedef struct {
|
|
|
1815
1801
|
}
|
|
1816
1802
|
|
|
1817
1803
|
- (NSTimeInterval)currentTimestamp {
|
|
1818
|
-
// Always use wall clock time for session timestamps
|
|
1819
|
-
// CACurrentMediaTime optimization removed - it causes drift after
|
|
1820
|
-
// background periods The ~1ms overhead is acceptable for 1fps capture
|
|
1821
1804
|
return [[NSDate date] timeIntervalSince1970] * 1000.0;
|
|
1822
1805
|
}
|
|
1823
1806
|
|
|
@@ -1829,8 +1812,6 @@ typedef struct {
|
|
|
1829
1812
|
endTime:(NSTimeInterval)endTime
|
|
1830
1813
|
frameCount:(NSInteger)frameCount {
|
|
1831
1814
|
|
|
1832
|
-
// Ensure we are on our own encoding queue to protect hierarchySnapshots
|
|
1833
|
-
// and maintain thread safety (callback comes from VideoEncoder queue)
|
|
1834
1815
|
dispatch_async(self.encodingQueue, ^{
|
|
1835
1816
|
RJLogDebug(@"CaptureEngine: videoEncoderDidFinishSegment: %@ (%ld frames, "
|
|
1836
1817
|
@"%.1fs), sessionId=%@",
|
|
@@ -1872,9 +1853,6 @@ typedef struct {
|
|
|
1872
1853
|
|
|
1873
1854
|
[self uploadCurrentHierarchySnapshots];
|
|
1874
1855
|
|
|
1875
|
-
// NUCLEAR FIX: Do NOT call startSegmentWithSize here!
|
|
1876
|
-
// The encoder's appendFrame method will auto-start a segment with the
|
|
1877
|
-
// correct PIXEL dimensions when the next frame is captured.
|
|
1878
1856
|
if (self.internalIsRecording && !self.isShuttingDown) {
|
|
1879
1857
|
RJLogDebug(
|
|
1880
1858
|
@"CaptureEngine: Segment finished, auto-start new on next frame");
|
|
@@ -1912,10 +1890,8 @@ typedef struct {
|
|
|
1912
1890
|
|
|
1913
1891
|
RJLogInfo(@"CaptureEngine: Pausing video capture (sync=%d)", synchronous);
|
|
1914
1892
|
|
|
1915
|
-
// Reset capture-in-progress flag immediately to prevent stuck state
|
|
1916
1893
|
self.captureInProgress = NO;
|
|
1917
1894
|
|
|
1918
|
-
// Invalidate timer synchronously if in sync mode
|
|
1919
1895
|
if (synchronous) {
|
|
1920
1896
|
[self teardownDisplayLink];
|
|
1921
1897
|
} else {
|
|
@@ -1925,7 +1901,7 @@ typedef struct {
|
|
|
1925
1901
|
}
|
|
1926
1902
|
|
|
1927
1903
|
if (self.internalVideoEncoder) {
|
|
1928
|
-
self.internalIsRecording = NO;
|
|
1904
|
+
self.internalIsRecording = NO;
|
|
1929
1905
|
|
|
1930
1906
|
if (synchronous) {
|
|
1931
1907
|
void (^finishSync)(void) = ^{
|
|
@@ -1964,19 +1940,15 @@ typedef struct {
|
|
|
1964
1940
|
return;
|
|
1965
1941
|
}
|
|
1966
1942
|
|
|
1967
|
-
// Set recording back to YES to allow captureVideoFrame to proceed
|
|
1968
1943
|
self.internalIsRecording = YES;
|
|
1969
1944
|
|
|
1970
1945
|
RJLogInfo(@"CaptureEngine: Resuming video capture");
|
|
1971
1946
|
|
|
1972
|
-
// Reset capture state to ensure clean resumption
|
|
1973
|
-
// These flags may have been left in an inconsistent state when going to
|
|
1974
|
-
// background
|
|
1975
1947
|
self.captureInProgress = NO;
|
|
1976
|
-
self.lastIntentTime = 0;
|
|
1948
|
+
self.lastIntentTime = 0;
|
|
1977
1949
|
|
|
1978
1950
|
self.internalPerformanceLevel =
|
|
1979
|
-
RJPerformanceLevelNormal;
|
|
1951
|
+
RJPerformanceLevelNormal;
|
|
1980
1952
|
|
|
1981
1953
|
self.pendingCapture = nil;
|
|
1982
1954
|
self.pendingCaptureGeneration = 0;
|
|
@@ -1995,7 +1967,6 @@ typedef struct {
|
|
|
1995
1967
|
if (window && self.internalVideoEncoder) {
|
|
1996
1968
|
RJLogInfo(@"CaptureEngine: Resuming capture...");
|
|
1997
1969
|
|
|
1998
|
-
// Use the optimized Display Link
|
|
1999
1970
|
[self setupDisplayLink];
|
|
2000
1971
|
|
|
2001
1972
|
} else {
|
|
@@ -2012,14 +1983,12 @@ typedef struct {
|
|
|
2012
1983
|
if (!self.internalIsRecording)
|
|
2013
1984
|
return;
|
|
2014
1985
|
|
|
2015
|
-
// Force update if screen changed
|
|
2016
1986
|
if (![screenName isEqualToString:self.currentScreenName]) {
|
|
2017
1987
|
NSTimeInterval now = CACurrentMediaTime();
|
|
2018
1988
|
self.currentScreenName = screenName;
|
|
2019
1989
|
RJLogDebug(@"Navigation to screen: %@ (forcing layout refresh)",
|
|
2020
1990
|
screenName);
|
|
2021
1991
|
|
|
2022
|
-
// Force layout change detection on next frame
|
|
2023
1992
|
[self.captureHeuristics invalidateSignature];
|
|
2024
1993
|
[self.captureHeuristics recordNavigationEventAtTime:now];
|
|
2025
1994
|
self.lastSerializedSignature = nil;
|
|
@@ -520,16 +520,7 @@
|
|
|
520
520
|
}
|
|
521
521
|
|
|
522
522
|
- (void)cleanup {
|
|
523
|
-
// Only cancel the current in-progress segment, don't delete the entire temp
|
|
524
|
-
// directory. Completed segments may still be uploading and must not be
|
|
525
|
-
// deleted here. The RJSegmentUploader handles file cleanup after successful
|
|
526
|
-
// upload.
|
|
527
523
|
[self cancelSegment];
|
|
528
|
-
|
|
529
|
-
// NOTE: Do NOT delete rj_segments directory here!
|
|
530
|
-
// Other segments may be in the middle of uploading.
|
|
531
|
-
// Old orphaned segments are cleaned up by
|
|
532
|
-
// RJSegmentUploader.cleanupOrphanedSegments()
|
|
533
524
|
}
|
|
534
525
|
|
|
535
526
|
#pragma mark - Private Methods
|
|
@@ -561,14 +552,9 @@
|
|
|
561
552
|
size_t width = (size_t)self.currentFrameSize.width;
|
|
562
553
|
size_t height = (size_t)self.currentFrameSize.height;
|
|
563
554
|
|
|
564
|
-
// CRITICAL FIX: Validate incoming image dimensions match expected size
|
|
565
|
-
// During keyboard/rotation transitions, image size may temporarily differ
|
|
566
|
-
// from currentFrameSize, causing CGBitmapContextCreate bytesPerRow mismatch
|
|
567
555
|
size_t imageWidth = CGImageGetWidth(cgImage);
|
|
568
556
|
size_t imageHeight = CGImageGetHeight(cgImage);
|
|
569
557
|
|
|
570
|
-
// Allow small variance (1-2 pixels) due to rounding, but reject major
|
|
571
|
-
// mismatches
|
|
572
558
|
if (labs((long)imageWidth - (long)width) > 2 ||
|
|
573
559
|
labs((long)imageHeight - (long)height) > 2) {
|
|
574
560
|
RJLogDebug(@"Video encoder: Skipping frame - size mismatch (got %zux%zu, "
|
|
@@ -639,9 +625,6 @@
|
|
|
639
625
|
colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
640
626
|
}
|
|
641
627
|
|
|
642
|
-
// CRITICAL: Validate bytesPerRow is sufficient for the target width
|
|
643
|
-
// Error "CGBitmapContextCreate: invalid data bytes/row" occurs when
|
|
644
|
-
// bytesPerRow < width * 4 (4 bytes per pixel for BGRA)
|
|
645
628
|
size_t requiredBytesPerRow = width * 4;
|
|
646
629
|
if (bytesPerRow < requiredBytesPerRow) {
|
|
647
630
|
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
|
|
@@ -663,7 +646,6 @@
|
|
|
663
646
|
return NULL;
|
|
664
647
|
}
|
|
665
648
|
|
|
666
|
-
// Use fastest interpolation for pixel buffer drawing
|
|
667
649
|
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
|
|
668
650
|
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
|
|
669
651
|
CGContextRelease(context);
|
|
@@ -681,14 +663,10 @@
|
|
|
681
663
|
}
|
|
682
664
|
|
|
683
665
|
- (void)prewarmPixelBufferPool {
|
|
684
|
-
// Pre-warm the VideoToolbox H.264 encoder by creating a minimal AVAssetWriter
|
|
685
|
-
// and encoding a single dummy frame. This eliminates the ~1.5s spike on first
|
|
686
|
-
// real frame encode by front-loading the hardware encoder initialization.
|
|
687
666
|
dispatch_async(self.encodingQueue, ^{
|
|
688
667
|
@autoreleasepool {
|
|
689
668
|
NSTimeInterval startTime = CACurrentMediaTime();
|
|
690
669
|
|
|
691
|
-
// Use a small size for fast prewarm (H.264 requires even dimensions)
|
|
692
670
|
CGSize warmupSize = CGSizeMake(100, 100);
|
|
693
671
|
|
|
694
672
|
// Create temp file for dummy segment
|
|
@@ -808,15 +786,12 @@ static dispatch_once_t sPrewarmOnceToken;
|
|
|
808
786
|
return;
|
|
809
787
|
sEncoderPrewarmed = YES;
|
|
810
788
|
|
|
811
|
-
// Run prewarm on a low-priority background queue
|
|
812
789
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
813
790
|
@autoreleasepool {
|
|
814
791
|
NSTimeInterval startTime = CACurrentMediaTime();
|
|
815
792
|
|
|
816
|
-
// Use a small size for fast prewarm (H.264 requires even dimensions)
|
|
817
793
|
CGSize warmupSize = CGSizeMake(100, 100);
|
|
818
794
|
|
|
819
|
-
// Create temp file for dummy segment
|
|
820
795
|
NSURL *tempDir = [NSURL fileURLWithPath:NSTemporaryDirectory()];
|
|
821
796
|
NSURL *warmupURL =
|
|
822
797
|
[tempDir URLByAppendingPathComponent:@"rj_encoder_prewarm.mp4"];
|
|
@@ -873,7 +848,6 @@ static dispatch_once_t sPrewarmOnceToken;
|
|
|
873
848
|
|
|
874
849
|
[warmupWriter startSessionAtSourceTime:kCMTimeZero];
|
|
875
850
|
|
|
876
|
-
// Create and encode a single dummy frame to trigger H.264 encoder init
|
|
877
851
|
CVPixelBufferRef dummyBuffer = NULL;
|
|
878
852
|
NSDictionary *pixelBufferOpts = @{
|
|
879
853
|
(id)kCVPixelBufferCGImageCompatibilityKey : @YES,
|