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.
- package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +47 -0
- package/dist/commonjs/capture.d.ts +2 -0
- package/dist/commonjs/capture.js +3 -0
- package/dist/commonjs/components/EmptyState.d.ts +1 -0
- package/dist/commonjs/components/EmptyState.js +2 -2
- package/dist/commonjs/components/Inspector/FabLauncher.js +494 -39
- package/dist/commonjs/components/Inspector/InspectorHeader.js +45 -26
- package/dist/commonjs/components/Inspector/MainScreen.js +4 -3
- package/dist/commonjs/components/Inspector/MediaGalleryTab.js +418 -13
- package/dist/commonjs/components/Inspector/MediaPreviewModal.js +376 -88
- package/dist/commonjs/components/Inspector/NetworkDetail.js +49 -63
- package/dist/commonjs/components/Inspector/SettingsPanel.js +499 -119
- package/dist/commonjs/components/Inspector/TabBar.js +166 -4
- package/dist/commonjs/components/JsonViewer.js +4 -3
- package/dist/commonjs/components/NetworkIcons.d.ts +4 -0
- package/dist/commonjs/components/NetworkIcons.js +26 -1
- package/dist/commonjs/components/SegmentedTabs.js +1 -0
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/i18n/locales/en.json +20 -2
- package/dist/commonjs/index.js +45 -30
- package/dist/commonjs/native/NativeInspector.d.ts +3 -0
- package/dist/commonjs/native/NativeInspector.js +14 -1
- package/dist/commonjs/native/NativeNetworkInspector.d.ts +1 -0
- package/dist/commonjs/styles/AppColors.js +78 -78
- package/dist/commonjs/types/enums.d.ts +2 -0
- package/dist/commonjs/types/enums.js +2 -0
- package/dist/commonjs/types/interfaces.d.ts +5 -0
- package/dist/esm/capture.d.ts +2 -0
- package/dist/esm/capture.js +4 -1
- package/dist/esm/components/EmptyState.d.ts +1 -0
- package/dist/esm/components/EmptyState.js +2 -2
- package/dist/esm/components/Inspector/FabLauncher.js +462 -40
- package/dist/esm/components/Inspector/InspectorHeader.js +46 -27
- package/dist/esm/components/Inspector/MainScreen.js +4 -3
- package/dist/esm/components/Inspector/MediaGalleryTab.js +419 -14
- package/dist/esm/components/Inspector/MediaPreviewModal.js +379 -91
- package/dist/esm/components/Inspector/NetworkDetail.js +49 -63
- package/dist/esm/components/Inspector/SettingsPanel.js +500 -120
- package/dist/esm/components/Inspector/TabBar.js +135 -6
- package/dist/esm/components/JsonViewer.js +4 -3
- package/dist/esm/components/NetworkIcons.d.ts +4 -0
- package/dist/esm/components/NetworkIcons.js +21 -0
- package/dist/esm/components/SegmentedTabs.js +1 -0
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/i18n/locales/en.json +20 -2
- package/dist/esm/index.js +44 -29
- package/dist/esm/native/NativeInspector.d.ts +3 -0
- package/dist/esm/native/NativeInspector.js +12 -0
- package/dist/esm/native/NativeNetworkInspector.d.ts +1 -0
- package/dist/esm/styles/AppColors.js +78 -78
- package/dist/esm/types/enums.d.ts +2 -0
- package/dist/esm/types/enums.js +2 -0
- package/dist/esm/types/interfaces.d.ts +5 -0
- package/ios/NetworkInspectorModule.mm +241 -136
- package/package.json +1 -1
- package/react-native-inapp-inspector.podspec +1 -1
- package/src/capture.ts +8 -0
- package/src/components/EmptyState.tsx +3 -1
- package/src/components/Inspector/FabLauncher.tsx +562 -52
- package/src/components/Inspector/InspectorHeader.tsx +60 -22
- package/src/components/Inspector/MainScreen.tsx +3 -2
- package/src/components/Inspector/MediaGalleryTab.tsx +502 -28
- package/src/components/Inspector/MediaPreviewModal.tsx +465 -117
- package/src/components/Inspector/NetworkDetail.tsx +80 -97
- package/src/components/Inspector/SettingsPanel.tsx +708 -152
- package/src/components/Inspector/TabBar.tsx +174 -6
- package/src/components/JsonViewer.tsx +4 -2
- package/src/components/NetworkIcons.tsx +56 -0
- package/src/components/SegmentedTabs.tsx +1 -0
- package/src/constants/version.ts +1 -1
- package/src/i18n/locales/en.json +20 -4
- package/src/index.tsx +46 -31
- package/src/native/NativeInspector.ts +17 -0
- package/src/native/NativeNetworkInspector.ts +1 -0
- package/src/styles/AppColors.ts +80 -80
- package/src/types/enums.ts +2 -0
- 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 *)
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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(
|
|
1143
|
+
UIGraphicsBeginImageContextWithOptions(screenBounds.size, NO, finalScale);
|
|
1138
1144
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
@
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
1150
|
-
|
|
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 (!
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (ctx) {
|
|
1190
|
+
if (!drewAny) {
|
|
1191
|
+
UIWindow *fallback = [self findActiveKeyWindow];
|
|
1192
|
+
if (fallback && fallback.rootViewController && fallback.rootViewController.view && context) {
|
|
1159
1193
|
@try {
|
|
1160
|
-
[
|
|
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
|
-
- (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
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: @(
|
|
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,
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
|
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
|
|
1422
|
+
if (self->_isSoftwareRecordingActive) {
|
|
1362
1423
|
resolve(@(YES));
|
|
1363
1424
|
return;
|
|
1364
1425
|
}
|
|
1365
1426
|
|
|
1366
|
-
|
|
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
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1437
|
+
self->_isSoftwareRecordingActive = NO;
|
|
1438
|
+
if (self->_softwareRecordingTimerSource) {
|
|
1439
|
+
dispatch_source_cancel(self->_softwareRecordingTimerSource);
|
|
1440
|
+
self->_softwareRecordingTimerSource = nil;
|
|
1441
|
+
}
|
|
1436
1442
|
|
|
1437
|
-
[
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
1459
|
-
@"uri":
|
|
1463
|
+
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
1464
|
+
@"uri": [NSURL fileURLWithPath:filePath].absoluteString,
|
|
1460
1465
|
@"format": @"mp4",
|
|
1461
|
-
@"durationMs": @(
|
|
1462
|
-
@"hasAudio": @(
|
|
1463
|
-
@"width": @(
|
|
1464
|
-
@"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
|
-
|
|
1475
|
-
|
|
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
|
-
[
|
|
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.
|
|
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>
|