react-native-inapp-inspector 2.3.17 → 2.3.19

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 (79) hide show
  1. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +47 -0
  2. package/dist/commonjs/capture.d.ts +2 -0
  3. package/dist/commonjs/capture.js +3 -0
  4. package/dist/commonjs/components/EmptyState.d.ts +1 -0
  5. package/dist/commonjs/components/EmptyState.js +2 -2
  6. package/dist/commonjs/components/Inspector/FabLauncher.js +494 -39
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +45 -26
  8. package/dist/commonjs/components/Inspector/MainScreen.js +4 -3
  9. package/dist/commonjs/components/Inspector/MediaGalleryTab.js +418 -13
  10. package/dist/commonjs/components/Inspector/MediaPreviewModal.js +376 -88
  11. package/dist/commonjs/components/Inspector/NetworkDetail.js +49 -63
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +499 -119
  13. package/dist/commonjs/components/Inspector/TabBar.js +166 -4
  14. package/dist/commonjs/components/JsonViewer.js +4 -3
  15. package/dist/commonjs/components/NetworkIcons.d.ts +4 -0
  16. package/dist/commonjs/components/NetworkIcons.js +26 -1
  17. package/dist/commonjs/components/SegmentedTabs.js +1 -0
  18. package/dist/commonjs/constants/version.d.ts +1 -1
  19. package/dist/commonjs/constants/version.js +1 -1
  20. package/dist/commonjs/i18n/locales/en.json +20 -2
  21. package/dist/commonjs/index.js +45 -30
  22. package/dist/commonjs/native/NativeInspector.d.ts +3 -0
  23. package/dist/commonjs/native/NativeInspector.js +14 -1
  24. package/dist/commonjs/native/NativeNetworkInspector.d.ts +1 -0
  25. package/dist/commonjs/styles/AppColors.js +78 -78
  26. package/dist/commonjs/types/enums.d.ts +2 -0
  27. package/dist/commonjs/types/enums.js +2 -0
  28. package/dist/commonjs/types/interfaces.d.ts +5 -0
  29. package/dist/esm/capture.d.ts +2 -0
  30. package/dist/esm/capture.js +4 -1
  31. package/dist/esm/components/EmptyState.d.ts +1 -0
  32. package/dist/esm/components/EmptyState.js +2 -2
  33. package/dist/esm/components/Inspector/FabLauncher.js +462 -40
  34. package/dist/esm/components/Inspector/InspectorHeader.js +46 -27
  35. package/dist/esm/components/Inspector/MainScreen.js +4 -3
  36. package/dist/esm/components/Inspector/MediaGalleryTab.js +419 -14
  37. package/dist/esm/components/Inspector/MediaPreviewModal.js +379 -91
  38. package/dist/esm/components/Inspector/NetworkDetail.js +49 -63
  39. package/dist/esm/components/Inspector/SettingsPanel.js +500 -120
  40. package/dist/esm/components/Inspector/TabBar.js +135 -6
  41. package/dist/esm/components/JsonViewer.js +4 -3
  42. package/dist/esm/components/NetworkIcons.d.ts +4 -0
  43. package/dist/esm/components/NetworkIcons.js +21 -0
  44. package/dist/esm/components/SegmentedTabs.js +1 -0
  45. package/dist/esm/constants/version.d.ts +1 -1
  46. package/dist/esm/constants/version.js +1 -1
  47. package/dist/esm/i18n/locales/en.json +20 -2
  48. package/dist/esm/index.js +44 -29
  49. package/dist/esm/native/NativeInspector.d.ts +3 -0
  50. package/dist/esm/native/NativeInspector.js +12 -0
  51. package/dist/esm/native/NativeNetworkInspector.d.ts +1 -0
  52. package/dist/esm/styles/AppColors.js +78 -78
  53. package/dist/esm/types/enums.d.ts +2 -0
  54. package/dist/esm/types/enums.js +2 -0
  55. package/dist/esm/types/interfaces.d.ts +5 -0
  56. package/ios/NetworkInspectorModule.mm +241 -136
  57. package/package.json +1 -1
  58. package/react-native-inapp-inspector.podspec +1 -1
  59. package/src/capture.ts +8 -0
  60. package/src/components/EmptyState.tsx +3 -1
  61. package/src/components/Inspector/FabLauncher.tsx +562 -52
  62. package/src/components/Inspector/InspectorHeader.tsx +60 -22
  63. package/src/components/Inspector/MainScreen.tsx +3 -2
  64. package/src/components/Inspector/MediaGalleryTab.tsx +502 -28
  65. package/src/components/Inspector/MediaPreviewModal.tsx +465 -117
  66. package/src/components/Inspector/NetworkDetail.tsx +80 -97
  67. package/src/components/Inspector/SettingsPanel.tsx +708 -152
  68. package/src/components/Inspector/TabBar.tsx +174 -6
  69. package/src/components/JsonViewer.tsx +4 -2
  70. package/src/components/NetworkIcons.tsx +56 -0
  71. package/src/components/SegmentedTabs.tsx +1 -0
  72. package/src/constants/version.ts +1 -1
  73. package/src/i18n/locales/en.json +20 -4
  74. package/src/index.tsx +46 -31
  75. package/src/native/NativeInspector.ts +17 -0
  76. package/src/native/NativeNetworkInspector.ts +1 -0
  77. package/src/styles/AppColors.ts +80 -80
  78. package/src/types/enums.ts +2 -0
  79. package/src/types/interfaces.ts +5 -0
@@ -5,6 +5,7 @@
5
5
  #import <unistd.h>
6
6
  #import <ReplayKit/ReplayKit.h>
7
7
  #import <AVFoundation/AVFoundation.h>
8
+ #import <AVKit/AVKit.h>
8
9
  #import <ImageIO/ImageIO.h>
9
10
  #import <MobileCoreServices/MobileCoreServices.h>
10
11
 
@@ -502,7 +503,11 @@ static void NativeExceptionHandler(NSException *exception) {
502
503
  BOOL _isSoftwareRecordingActive;
503
504
  NSInteger _softwareFrameIndex;
504
505
  NSTimeInterval _softwareRecordingStartTime;
506
+ long long _softwareRecordingTimestamp;
505
507
  NSString *_softwareRecordingFilePath;
508
+ NSString *_softwareRecordingThumbnailPath;
509
+ NSInteger _softwareVideoWidth;
510
+ NSInteger _softwareVideoHeight;
506
511
  }
507
512
 
508
513
  RCT_EXPORT_MODULE(NetworkInspectorModule);
@@ -1124,63 +1129,101 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
1124
1129
  return foundWindow;
1125
1130
  }
1126
1131
 
1127
- - (UIImage *)captureWindowImage:(UIWindow *)window scale:(CGFloat)scale {
1128
- if (!window) return nil;
1129
- CGRect bounds = window.bounds;
1130
- if (bounds.size.width <= 0 || bounds.size.height <= 0) {
1131
- bounds = [UIScreen mainScreen].bounds;
1132
+ - (UIImage *)captureScreenHierarchyWithScale:(CGFloat)scale {
1133
+ // Restrict capture strictly to active foreground app
1134
+ if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
1135
+ return nil;
1132
1136
  }
1137
+
1138
+ CGRect screenBounds = [UIScreen mainScreen].bounds;
1133
1139
  CGFloat screenScale = [UIScreen mainScreen].scale;
1134
1140
  CGFloat finalScale = screenScale * scale;
1135
1141
  if (finalScale <= 0.0) finalScale = screenScale;
1136
1142
 
1137
- UIGraphicsBeginImageContextWithOptions(bounds.size, NO, finalScale);
1143
+ UIGraphicsBeginImageContextWithOptions(screenBounds.size, NO, finalScale);
1138
1144
  CGContextRef context = UIGraphicsGetCurrentContext();
1139
-
1140
- BOOL drawn = NO;
1141
- @try {
1142
- drawn = [window drawViewHierarchyInRect:bounds afterScreenUpdates:NO];
1143
- } @catch (NSException *e) {
1144
- drawn = NO;
1145
+
1146
+ NSArray<UIWindow *> *windows = @[];
1147
+ if (@available(iOS 13.0, *)) {
1148
+ NSMutableArray *allWindows = [NSMutableArray array];
1149
+ for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
1150
+ if ([scene isKindOfClass:[UIWindowScene class]]) {
1151
+ [allWindows addObjectsFromArray:((UIWindowScene *)scene).windows];
1152
+ }
1153
+ }
1154
+ windows = allWindows;
1145
1155
  }
1146
-
1147
- if (!drawn && context) {
1156
+ if (windows.count == 0) {
1157
+ #pragma clang diagnostic push
1158
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
1159
+ windows = [UIApplication sharedApplication].windows;
1160
+ #pragma clang diagnostic pop
1161
+ }
1162
+
1163
+ NSArray<UIWindow *> *sortedWindows = [windows sortedArrayUsingComparator:^NSComparisonResult(UIWindow *w1, UIWindow *w2) {
1164
+ if (w1.windowLevel < w2.windowLevel) return NSOrderedAscending;
1165
+ if (w1.windowLevel > w2.windowLevel) return NSOrderedDescending;
1166
+ return NSOrderedSame;
1167
+ }];
1168
+
1169
+ BOOL drewAny = NO;
1170
+ for (UIWindow *w in sortedWindows) {
1171
+ if (w.hidden || w.alpha <= 0.01 || w.bounds.size.width <= 0) continue;
1148
1172
  @try {
1149
- [window.layer renderInContext:context];
1150
- } @catch (NSException *e) {}
1173
+ BOOL ok = [w drawViewHierarchyInRect:w.bounds afterScreenUpdates:NO];
1174
+ if (ok) {
1175
+ drewAny = YES;
1176
+ } else if (context) {
1177
+ [w.layer renderInContext:context];
1178
+ drewAny = YES;
1179
+ }
1180
+ } @catch (NSException *e) {
1181
+ if (context) {
1182
+ @try {
1183
+ [w.layer renderInContext:context];
1184
+ drewAny = YES;
1185
+ } @catch (NSException *ex) {}
1186
+ }
1187
+ }
1151
1188
  }
1152
- UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
1153
- UIGraphicsEndImageContext();
1154
1189
 
1155
- if (!image && window.rootViewController && window.rootViewController.view) {
1156
- UIGraphicsBeginImageContextWithOptions(bounds.size, NO, finalScale);
1157
- CGContextRef ctx = UIGraphicsGetCurrentContext();
1158
- if (ctx) {
1190
+ if (!drewAny) {
1191
+ UIWindow *fallback = [self findActiveKeyWindow];
1192
+ if (fallback && fallback.rootViewController && fallback.rootViewController.view && context) {
1159
1193
  @try {
1160
- [window.rootViewController.view.layer renderInContext:ctx];
1194
+ [fallback.rootViewController.view.layer renderInContext:context];
1161
1195
  } @catch (NSException *e) {}
1162
1196
  }
1163
- image = UIGraphicsGetImageFromCurrentImageContext();
1164
- UIGraphicsEndImageContext();
1165
1197
  }
1198
+
1199
+ UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
1200
+ UIGraphicsEndImageContext();
1166
1201
  return image;
1167
1202
  }
1168
1203
 
1169
- - (CVPixelBufferRef)createPixelBufferFromCGImage:(CGImageRef)image size:(CGSize)size {
1204
+ - (UIImage *)captureWindowImage:(UIWindow *)window scale:(CGFloat)scale {
1205
+ return [self captureScreenHierarchyWithScale:scale];
1206
+ }
1207
+
1208
+ - (CVPixelBufferRef)createPixelBufferFromUIImage:(UIImage *)image size:(CGSize)size {
1209
+ if (!image) return NULL;
1170
1210
  NSDictionary *options = @{
1171
1211
  (id)kCVPixelBufferCGImageCompatibilityKey: @(YES),
1172
1212
  (id)kCVPixelBufferCGBitmapContextCompatibilityKey: @(YES)
1173
1213
  };
1174
1214
  CVPixelBufferRef pxbuffer = NULL;
1175
- CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, (size_t)size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef)options, &pxbuffer);
1215
+ CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, (size_t)size.width, (size_t)size.height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef)options, &pxbuffer);
1176
1216
  if (status != kCVReturnSuccess || pxbuffer == NULL) return NULL;
1177
1217
 
1178
1218
  CVPixelBufferLockBaseAddress(pxbuffer, 0);
1179
1219
  void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
1220
+ size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pxbuffer);
1180
1221
  CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
1181
- CGContextRef context = CGBitmapContextCreate(pxdata, (size_t)size.width, (size_t)size.height, 8, 4 * (size_t)size.width, rgbColorSpace, (CGBitmapInfo)kCGImageAlphaNoneSkipFirst);
1222
+ CGContextRef context = CGBitmapContextCreate(pxdata, (size_t)size.width, (size_t)size.height, 8, bytesPerRow, rgbColorSpace, (CGBitmapInfo)kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
1182
1223
  if (context) {
1183
- CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), image);
1224
+ CGContextTranslateCTM(context, 0, size.height);
1225
+ CGContextScaleCTM(context, 1.0, -1.0);
1226
+ CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), image.CGImage);
1184
1227
  CGContextRelease(context);
1185
1228
  }
1186
1229
  CGColorSpaceRelease(rgbColorSpace);
@@ -1192,21 +1235,21 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
1192
1235
  resolve:(RCTPromiseResolveBlock)resolve
1193
1236
  reject:(RCTPromiseRejectBlock)reject {
1194
1237
  dispatch_async(dispatch_get_main_queue(), ^{
1195
- UIWindow *window = [self findActiveKeyWindow];
1196
- if (!window) {
1197
- reject(@"RECORDER_ERROR", @"Unable to find active UIWindow for screen recording", nil);
1198
- return;
1238
+ CGRect bounds = [UIScreen mainScreen].bounds;
1239
+ CGFloat screenScale = [UIScreen mainScreen].scale;
1240
+ double scaleParam = [options[@"scale"] doubleValue];
1241
+ if (scaleParam <= 0.0 || scaleParam > 1.0) {
1242
+ scaleParam = 0.5;
1199
1243
  }
1200
1244
 
1201
- CGRect bounds = window.bounds;
1202
- CGFloat screenScale = [UIScreen mainScreen].scale;
1203
- NSInteger width = ((NSInteger)(bounds.size.width * screenScale * 0.5) / 2) * 2;
1204
- NSInteger height = ((NSInteger)(bounds.size.height * screenScale * 0.5) / 2) * 2;
1245
+ NSInteger width = ((NSInteger)(bounds.size.width * screenScale * scaleParam) / 2) * 2;
1246
+ NSInteger height = ((NSInteger)(bounds.size.height * screenScale * scaleParam) / 2) * 2;
1205
1247
  if (width <= 0) width = 360;
1206
1248
  if (height <= 0) height = 640;
1207
1249
 
1208
1250
  CGSize videoSize = CGSizeMake(width, height);
1209
1251
  long long timestamp = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
1252
+ self->_softwareRecordingTimestamp = timestamp;
1210
1253
  NSString *filename = [NSString stringWithFormat:@"video_%lld.mp4", timestamp];
1211
1254
  NSString *filePath = [[self getCapturesDirectory] stringByAppendingPathComponent:filename];
1212
1255
  NSURL *outputUrl = [NSURL fileURLWithPath:filePath];
@@ -1228,7 +1271,7 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
1228
1271
  self->_softwareWriterInput.expectsMediaDataInRealTime = YES;
1229
1272
 
1230
1273
  NSDictionary *sourcePixelBufferAttributes = @{
1231
- (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32ARGB),
1274
+ (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
1232
1275
  (id)kCVPixelBufferWidthKey: @(videoSize.width),
1233
1276
  (id)kCVPixelBufferHeightKey: @(videoSize.height)
1234
1277
  };
@@ -1246,14 +1289,37 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
1246
1289
  self->_softwareFrameIndex = 0;
1247
1290
  self->_softwareRecordingStartTime = [[NSDate date] timeIntervalSince1970];
1248
1291
  self->_softwareRecordingFilePath = filePath;
1292
+ self->_softwareVideoWidth = width;
1293
+ self->_softwareVideoHeight = height;
1249
1294
 
1250
1295
  double fps = [options[@"fps"] doubleValue];
1251
1296
  if (fps <= 0.0 || fps > 30.0) fps = 15.0;
1252
1297
  double frameInterval = 1.0 / fps;
1253
1298
 
1299
+ // Immediately capture frame 0 & save thumbnail
1300
+ UIImage *firstImg = [self captureScreenHierarchyWithScale:(CGFloat)scaleParam];
1301
+ if (firstImg) {
1302
+ NSData *thumbData = UIImageJPEGRepresentation(firstImg, 0.8);
1303
+ if (thumbData) {
1304
+ NSString *thumbFilename = [NSString stringWithFormat:@"thumb_%lld.jpg", timestamp];
1305
+ NSString *thumbFilePath = [[self getCapturesDirectory] stringByAppendingPathComponent:thumbFilename];
1306
+ [thumbData writeToFile:thumbFilePath atomically:YES];
1307
+ self->_softwareRecordingThumbnailPath = thumbFilePath;
1308
+ }
1309
+
1310
+ if (self->_softwareWriterInput.isReadyForMoreMediaData) {
1311
+ CVPixelBufferRef buffer = [self createPixelBufferFromUIImage:firstImg size:videoSize];
1312
+ if (buffer) {
1313
+ [self->_softwarePixelBufferAdaptor appendPixelBuffer:buffer withPresentationTime:kCMTimeZero];
1314
+ CVPixelBufferRelease(buffer);
1315
+ self->_softwareFrameIndex = 1;
1316
+ }
1317
+ }
1318
+ }
1319
+
1254
1320
  dispatch_queue_t queue = dispatch_queue_create("com.inappinspector.recording", DISPATCH_QUEUE_SERIAL);
1255
1321
  self->_softwareRecordingTimerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
1256
- dispatch_source_set_timer(self->_softwareRecordingTimerSource, dispatch_time(DISPATCH_TIME_NOW, 0), (uint64_t)(frameInterval * NSEC_PER_SEC), (uint64_t)(frameInterval * 0.1 * NSEC_PER_SEC));
1322
+ dispatch_source_set_timer(self->_softwareRecordingTimerSource, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(frameInterval * NSEC_PER_SEC)), (uint64_t)(frameInterval * NSEC_PER_SEC), (uint64_t)(frameInterval * 0.1 * NSEC_PER_SEC));
1257
1323
 
1258
1324
  __weak NetworkInspectorModule *weakSelf = self;
1259
1325
  dispatch_source_set_event_handler(self->_softwareRecordingTimerSource, ^{
@@ -1262,10 +1328,11 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
1262
1328
 
1263
1329
  dispatch_sync(dispatch_get_main_queue(), ^{
1264
1330
  if (!strongSelf || !strongSelf->_isSoftwareRecordingActive) return;
1265
- UIWindow *w = [strongSelf findActiveKeyWindow];
1266
- UIImage *frameImg = [strongSelf captureWindowImage:w scale:0.5];
1267
- if (frameImg && frameImg.CGImage && strongSelf->_softwareWriterInput.isReadyForMoreMediaData) {
1268
- CVPixelBufferRef buffer = [strongSelf createPixelBufferFromCGImage:frameImg.CGImage size:videoSize];
1331
+ if (!strongSelf->_softwareAssetWriter || strongSelf->_softwareAssetWriter.status != AVAssetWriterStatusWriting) return;
1332
+
1333
+ UIImage *frameImg = [strongSelf captureScreenHierarchyWithScale:(CGFloat)scaleParam];
1334
+ if (frameImg && strongSelf->_softwareWriterInput.isReadyForMoreMediaData) {
1335
+ CVPixelBufferRef buffer = [strongSelf createPixelBufferFromUIImage:frameImg size:videoSize];
1269
1336
  if (buffer) {
1270
1337
  CMTime presentTime = CMTimeMake((int64_t)(strongSelf->_softwareFrameIndex * 1000 / fps), 1000);
1271
1338
  [strongSelf->_softwarePixelBufferAdaptor appendPixelBuffer:buffer withPresentationTime:presentTime];
@@ -1286,18 +1353,12 @@ RCT_EXPORT_METHOD(takeScreenshot:(NSDictionary *)options
1286
1353
  reject:(RCTPromiseRejectBlock)reject) {
1287
1354
  dispatch_async(dispatch_get_main_queue(), ^{
1288
1355
  @try {
1289
- UIWindow *keyWindow = [self findActiveKeyWindow];
1290
- if (!keyWindow) {
1291
- reject(@"SCREENSHOT_ERROR", @"Unable to find active UIWindow for screen capture", nil);
1292
- return;
1293
- }
1294
-
1295
1356
  double scaleParam = [options[@"scale"] doubleValue];
1296
1357
  if (scaleParam <= 0.0 || scaleParam > 1.0) {
1297
1358
  scaleParam = 1.0;
1298
1359
  }
1299
1360
 
1300
- UIImage *image = [self captureWindowImage:keyWindow scale:(CGFloat)scaleParam];
1361
+ UIImage *image = [self captureScreenHierarchyWithScale:(CGFloat)scaleParam];
1301
1362
  if (!image) {
1302
1363
  reject(@"SCREENSHOT_ERROR", @"Failed to render window hierarchy to image context", nil);
1303
1364
  return;
@@ -1358,96 +1419,40 @@ RCT_EXPORT_METHOD(takeScreenshot:(NSDictionary *)options
1358
1419
  RCT_EXPORT_METHOD(startVideoRecording:(NSDictionary *)options
1359
1420
  resolve:(RCTPromiseResolveBlock)resolve
1360
1421
  reject:(RCTPromiseRejectBlock)reject) {
1361
- if (self->_isSoftwareRecordingActive || [RPScreenRecorder sharedRecorder].isRecording) {
1422
+ if (self->_isSoftwareRecordingActive) {
1362
1423
  resolve(@(YES));
1363
1424
  return;
1364
1425
  }
1365
1426
 
1366
- RPScreenRecorder *recorder = [RPScreenRecorder sharedRecorder];
1367
- if (recorder.isAvailable) {
1368
- NSString *audioSource = [options[@"audioSource"] isKindOfClass:[NSString class]] ? options[@"audioSource"] : @"none";
1369
- BOOL enableMic = [audioSource isEqualToString:@"mic"] || [audioSource isEqualToString:@"mixed"];
1370
- recorder.microphoneEnabled = enableMic;
1371
-
1372
- [recorder startRecordingWithHandler:^(NSError * _Nullable error) {
1373
- if (error) {
1374
- // If ReplayKit fails to initialize (e.g. simulator or sandbox), fallback to software frame recorder
1375
- [self startSoftwareRecordingWithOptions:options resolve:resolve reject:reject];
1376
- } else {
1377
- resolve(@(YES));
1378
- }
1379
- }];
1380
- } else {
1381
- // Fallback to software frame recorder (works everywhere including iOS Simulators)
1382
- [self startSoftwareRecordingWithOptions:options resolve:resolve reject:reject];
1383
- }
1427
+ [self startSoftwareRecordingWithOptions:options resolve:resolve reject:reject];
1384
1428
  }
1385
1429
 
1386
1430
  RCT_EXPORT_METHOD(stopVideoRecording:(RCTPromiseResolveBlock)resolve
1387
1431
  reject:(RCTPromiseRejectBlock)reject) {
1388
- if (self->_isSoftwareRecordingActive) {
1389
- self->_isSoftwareRecordingActive = NO;
1390
- if (self->_softwareRecordingTimerSource) {
1391
- dispatch_source_cancel(self->_softwareRecordingTimerSource);
1392
- self->_softwareRecordingTimerSource = nil;
1393
- }
1394
-
1395
- [self->_softwareWriterInput markAsFinished];
1396
- NSString *filePath = self->_softwareRecordingFilePath;
1397
- NSTimeInterval duration = [[NSDate date] timeIntervalSince1970] - self->_softwareRecordingStartTime;
1398
- long long timestamp = (long long)(self->_softwareRecordingStartTime * 1000.0);
1399
-
1400
- [self->_softwareAssetWriter finishWritingWithCompletionHandler:^{
1401
- self->_softwareAssetWriter = nil;
1402
- self->_softwareWriterInput = nil;
1403
- self->_softwarePixelBufferAdaptor = nil;
1404
-
1405
- unsigned long long fileSize = 0;
1406
- NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
1407
- if (attrs) {
1408
- fileSize = [attrs fileSize];
1409
- }
1410
-
1411
- NSDictionary *result = @{
1412
- @"uri": [NSURL fileURLWithPath:filePath].absoluteString,
1413
- @"format": @"mp4",
1414
- @"durationMs": @(MAX(500.0, duration * 1000.0)),
1415
- @"hasAudio": @(NO),
1416
- @"width": @(720),
1417
- @"height": @(1280),
1418
- @"sizeBytes": @(fileSize),
1419
- @"timestamp": @(timestamp)
1420
- };
1421
- resolve(result);
1422
- }];
1423
- return;
1424
- }
1425
-
1426
- RPScreenRecorder *recorder = [RPScreenRecorder sharedRecorder];
1427
- if (!recorder.isRecording) {
1432
+ if (!self->_isSoftwareRecordingActive) {
1428
1433
  resolve([NSNull null]);
1429
1434
  return;
1430
1435
  }
1431
1436
 
1432
- long long timestamp = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
1433
- NSString *filename = [NSString stringWithFormat:@"video_%lld.mp4", timestamp];
1434
- NSString *filePath = [[self getCapturesDirectory] stringByAppendingPathComponent:filename];
1435
- NSURL *outputUrl = [NSURL fileURLWithPath:filePath];
1437
+ self->_isSoftwareRecordingActive = NO;
1438
+ if (self->_softwareRecordingTimerSource) {
1439
+ dispatch_source_cancel(self->_softwareRecordingTimerSource);
1440
+ self->_softwareRecordingTimerSource = nil;
1441
+ }
1436
1442
 
1437
- [recorder stopRecordingWithOutputURL:outputUrl completionHandler:^(NSError * _Nullable error) {
1438
- if (error) {
1439
- reject(@"RECORD_STOP_ERROR", error.localizedDescription, error);
1440
- return;
1441
- }
1443
+ [self->_softwareWriterInput markAsFinished];
1444
+ NSString *filePath = self->_softwareRecordingFilePath;
1445
+ NSString *thumbPath = self->_softwareRecordingThumbnailPath;
1446
+ NSTimeInterval duration = [[NSDate date] timeIntervalSince1970] - self->_softwareRecordingStartTime;
1447
+ long long timestamp = self->_softwareRecordingTimestamp > 0 ? self->_softwareRecordingTimestamp : (long long)(self->_softwareRecordingStartTime * 1000.0);
1448
+ NSInteger vidWidth = self->_softwareVideoWidth > 0 ? self->_softwareVideoWidth : 720;
1449
+ NSInteger vidHeight = self->_softwareVideoHeight > 0 ? self->_softwareVideoHeight : 1280;
1442
1450
 
1443
- AVURLAsset *asset = [AVURLAsset URLAssetWithURL:outputUrl options:nil];
1444
- CMTime duration = asset.duration;
1445
- double durationMs = CMTimeGetSeconds(duration) * 1000.0;
1446
- CGSize naturalSize = CGSizeMake(1080, 1920);
1447
- NSArray<AVAssetTrack *> *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
1448
- if (videoTracks.count > 0) {
1449
- naturalSize = videoTracks.firstObject.naturalSize;
1450
- }
1451
+ [self->_softwareAssetWriter finishWritingWithCompletionHandler:^{
1452
+ self->_softwareAssetWriter = nil;
1453
+ self->_softwareWriterInput = nil;
1454
+ self->_softwarePixelBufferAdaptor = nil;
1455
+ self->_softwareRecordingThumbnailPath = nil;
1451
1456
 
1452
1457
  unsigned long long fileSize = 0;
1453
1458
  NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
@@ -1455,24 +1460,71 @@ RCT_EXPORT_METHOD(stopVideoRecording:(RCTPromiseResolveBlock)resolve
1455
1460
  fileSize = [attrs fileSize];
1456
1461
  }
1457
1462
 
1458
- NSDictionary *result = @{
1459
- @"uri": outputUrl.absoluteString,
1463
+ NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{
1464
+ @"uri": [NSURL fileURLWithPath:filePath].absoluteString,
1460
1465
  @"format": @"mp4",
1461
- @"durationMs": @(durationMs),
1462
- @"hasAudio": @(recorder.isMicrophoneEnabled),
1463
- @"width": @((NSInteger)naturalSize.width),
1464
- @"height": @((NSInteger)naturalSize.height),
1466
+ @"durationMs": @(MAX(500.0, duration * 1000.0)),
1467
+ @"hasAudio": @(NO),
1468
+ @"width": @(vidWidth),
1469
+ @"height": @(vidHeight),
1465
1470
  @"sizeBytes": @(fileSize),
1466
1471
  @"timestamp": @(timestamp)
1467
- };
1472
+ }];
1473
+
1474
+ if (thumbPath && [[NSFileManager defaultManager] fileExistsAtPath:thumbPath]) {
1475
+ result[@"thumbnailUri"] = [NSURL fileURLWithPath:thumbPath].absoluteString;
1476
+ }
1477
+
1468
1478
  resolve(result);
1469
1479
  }];
1470
1480
  }
1471
1481
 
1472
1482
  RCT_EXPORT_METHOD(isRecording:(RCTPromiseResolveBlock)resolve
1473
1483
  reject:(RCTPromiseRejectBlock)reject) {
1474
- BOOL active = self->_isSoftwareRecordingActive || [RPScreenRecorder sharedRecorder].isRecording;
1475
- resolve(@(active));
1484
+ resolve(@(self->_isSoftwareRecordingActive));
1485
+ }
1486
+
1487
+ RCT_EXPORT_METHOD(playVideo:(NSString *)videoUri
1488
+ resolve:(RCTPromiseResolveBlock)resolve
1489
+ reject:(RCTPromiseRejectBlock)reject) {
1490
+ dispatch_async(dispatch_get_main_queue(), ^{
1491
+ @try {
1492
+ if (!videoUri || videoUri.length == 0) {
1493
+ reject(@"PLAY_ERROR", @"Invalid video URI", nil);
1494
+ return;
1495
+ }
1496
+ NSURL *url = [NSURL URLWithString:videoUri];
1497
+ if (!url || !url.scheme) {
1498
+ url = [NSURL fileURLWithPath:videoUri];
1499
+ }
1500
+
1501
+ AVPlayer *player = [AVPlayer playerWithURL:url];
1502
+ AVPlayerViewController *playerController = [[AVPlayerViewController alloc] init];
1503
+ playerController.player = player;
1504
+ playerController.showsPlaybackControls = YES;
1505
+ playerController.modalPresentationStyle = UIModalPresentationFullScreen;
1506
+
1507
+ UIWindow *keyWindow = [self findActiveKeyWindow];
1508
+ UIViewController *rootVC = keyWindow.rootViewController;
1509
+ if (!rootVC) {
1510
+ rootVC = [UIApplication sharedApplication].delegate.window.rootViewController;
1511
+ }
1512
+ while (rootVC.presentedViewController) {
1513
+ rootVC = rootVC.presentedViewController;
1514
+ }
1515
+
1516
+ if (rootVC) {
1517
+ [rootVC presentViewController:playerController animated:YES completion:^{
1518
+ [player play];
1519
+ resolve(@(YES));
1520
+ }];
1521
+ } else {
1522
+ reject(@"PLAY_ERROR", @"Unable to find view controller to present video player", nil);
1523
+ }
1524
+ } @catch (NSException *ex) {
1525
+ reject(@"PLAY_ERROR", ex.reason, nil);
1526
+ }
1527
+ });
1476
1528
  }
1477
1529
 
1478
1530
  RCT_EXPORT_METHOD(convertToGif:(NSString *)videoUri
@@ -1597,6 +1649,9 @@ RCT_EXPORT_METHOD(getCapturedMedia:(RCTPromiseResolveBlock)resolve
1597
1649
  NSMutableArray *mediaItems = [NSMutableArray array];
1598
1650
 
1599
1651
  for (NSString *file in (files ?: @[])) {
1652
+ // Skip standalone thumbnail files
1653
+ if ([file hasPrefix:@"thumb_"]) continue;
1654
+
1600
1655
  NSString *fullPath = [dir stringByAppendingPathComponent:file];
1601
1656
  NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
1602
1657
  if (!attrs) continue;
@@ -1613,7 +1668,7 @@ RCT_EXPORT_METHOD(getCapturedMedia:(RCTPromiseResolveBlock)resolve
1613
1668
  NSDate *modDate = [attrs fileModificationDate];
1614
1669
  long long timestamp = (long long)([modDate timeIntervalSince1970] * 1000.0);
1615
1670
 
1616
- [mediaItems addObject:@{
1671
+ NSMutableDictionary *item = [NSMutableDictionary dictionaryWithDictionary:@{
1617
1672
  @"id": file,
1618
1673
  @"type": type,
1619
1674
  @"format": ext,
@@ -1622,6 +1677,43 @@ RCT_EXPORT_METHOD(getCapturedMedia:(RCTPromiseResolveBlock)resolve
1622
1677
  @"sizeBytes": @(sizeBytes),
1623
1678
  @"timestamp": @(timestamp)
1624
1679
  }];
1680
+
1681
+ if ([type isEqualToString:@"video"]) {
1682
+ // Check if matching thumbnail exists
1683
+ NSString *nameWithoutExt = [file stringByDeletingPathExtension];
1684
+ NSString *timestampSuffix = [nameWithoutExt stringByReplacingOccurrencesOfString:@"video_" withString:@""];
1685
+ NSString *thumbFile = [NSString stringWithFormat:@"thumb_%@.jpg", timestampSuffix];
1686
+ NSString *thumbFullPath = [dir stringByAppendingPathComponent:thumbFile];
1687
+ if ([[NSFileManager defaultManager] fileExistsAtPath:thumbFullPath]) {
1688
+ item[@"thumbnailUri"] = [NSURL fileURLWithPath:thumbFullPath].absoluteString;
1689
+ } else {
1690
+ @try {
1691
+ NSURL *vidUrl = [NSURL fileURLWithPath:fullPath];
1692
+ AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:vidUrl options:nil];
1693
+ AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:asset];
1694
+ gen.appliesPreferredTrackTransform = YES;
1695
+ gen.maximumSize = CGSizeMake(480, 480);
1696
+ CMTime time = CMTimeMake(1, 10);
1697
+ NSError *err = nil;
1698
+ CGImageRef imgRef = [gen copyCGImageAtTime:time actualTime:NULL error:&err];
1699
+ if (!imgRef) {
1700
+ time = kCMTimeZero;
1701
+ imgRef = [gen copyCGImageAtTime:time actualTime:NULL error:&err];
1702
+ }
1703
+ if (imgRef) {
1704
+ UIImage *thumbImg = [UIImage imageWithCGImage:imgRef];
1705
+ CGImageRelease(imgRef);
1706
+ NSData *tData = UIImageJPEGRepresentation(thumbImg, 0.8);
1707
+ if (tData) {
1708
+ [tData writeToFile:thumbFullPath atomically:YES];
1709
+ item[@"thumbnailUri"] = [NSURL fileURLWithPath:thumbFullPath].absoluteString;
1710
+ }
1711
+ }
1712
+ } @catch (NSException *e) {}
1713
+ }
1714
+ }
1715
+
1716
+ [mediaItems addObject:item];
1625
1717
  }
1626
1718
 
1627
1719
  [mediaItems sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
@@ -1643,6 +1735,19 @@ RCT_EXPORT_METHOD(deleteCapturedMedia:(NSString *)uri
1643
1735
  NSString *path = url ? url.path : uri;
1644
1736
  if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
1645
1737
  [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
1738
+
1739
+ // Also remove any related thumbnail
1740
+ NSString *filename = [path lastPathComponent];
1741
+ if ([filename hasPrefix:@"video_"]) {
1742
+ NSString *nameWithoutExt = [filename stringByDeletingPathExtension];
1743
+ NSString *timestampSuffix = [nameWithoutExt stringByReplacingOccurrencesOfString:@"video_" withString:@""];
1744
+ NSString *thumbFile = [NSString stringWithFormat:@"thumb_%@.jpg", timestampSuffix];
1745
+ NSString *thumbPath = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:thumbFile];
1746
+ if ([[NSFileManager defaultManager] fileExistsAtPath:thumbPath]) {
1747
+ [[NSFileManager defaultManager] removeItemAtPath:thumbPath error:nil];
1748
+ }
1749
+ }
1750
+
1646
1751
  resolve(@(YES));
1647
1752
  } else {
1648
1753
  resolve(@(NO));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-inapp-inspector",
3
- "version": "2.3.17",
3
+ "version": "2.3.19",
4
4
  "description": "The zero-config, all-in-one in-app debugger for React Native & Expo. Inspect Network (fetch/axios), Console logs, Stack Traces, Redux State, Firebase Analytics, and JS Bundle size directly on device.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
13
13
  s.source = { :git => "https://github.com/vengatmacuser/react-native-inapp-inspector.git", :tag => "v#{s.version}" }
14
14
  s.source_files = "ios/**/*.{h,m,mm,swift}"
15
15
  s.requires_arc = true
16
- s.frameworks = "AVFoundation", "ReplayKit", "CoreMedia", "CoreVideo", "ImageIO", "MobileCoreServices", "QuartzCore"
16
+ s.frameworks = "AVFoundation", "AVKit", "ReplayKit", "CoreMedia", "CoreVideo", "ImageIO", "MobileCoreServices", "QuartzCore"
17
17
 
18
18
  # React Native New Architecture (TurboModules & Codegen) support
19
19
  if respond_to?(:install_modules_dependencies, true)
package/src/capture.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  startNativeVideoRecording,
4
4
  stopNativeVideoRecording,
5
5
  isNativeRecordingActive,
6
+ playNativeVideo,
6
7
  convertNativeVideoToGif,
7
8
  fetchCapturedMediaList,
8
9
  deleteCapturedMediaFile,
@@ -64,6 +65,13 @@ export const ScreenCapture = {
64
65
  return isNativeRecordingActive();
65
66
  },
66
67
 
68
+ /**
69
+ * Plays a recorded video in the full-screen native media player.
70
+ */
71
+ playVideo: async (videoUri: string): Promise<boolean> => {
72
+ return playNativeVideo(videoUri);
73
+ },
74
+
67
75
  /**
68
76
  * Converts a recorded MP4 video to an animated GIF.
69
77
  */
@@ -18,6 +18,7 @@ interface EmptyStateProps {
18
18
  customTitle?: string;
19
19
  customSub?: string;
20
20
  onClearSearch?: () => void;
21
+ showReload?: boolean;
21
22
  }
22
23
 
23
24
  const EmptyState = React.memo(function EmptyState({
@@ -26,6 +27,7 @@ const EmptyState = React.memo(function EmptyState({
26
27
  customTitle,
27
28
  customSub,
28
29
  onClearSearch,
30
+ showReload = true,
29
31
  }: EmptyStateProps) {
30
32
  const iconPulse = useRef(new Animated.Value(1)).current;
31
33
 
@@ -83,7 +85,7 @@ const EmptyState = React.memo(function EmptyState({
83
85
  <Text style={styles.reloadBtnText}>Clear Search & Filters</Text>
84
86
  </TouchableScale>
85
87
  )}
86
- {!isSearch && (
88
+ {!isSearch && showReload && (
87
89
  <TouchableScale style={styles.reloadBtn} onPress={handleReload}>
88
90
  <Text style={styles.reloadBtnText}>Reload App</Text>
89
91
  </TouchableScale>