react-native-inapp-inspector 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ar.md +163 -0
- package/README.de.md +163 -0
- package/README.es.md +164 -0
- package/README.fr.md +163 -0
- package/README.hi.md +163 -0
- package/README.ja.md +180 -0
- package/README.ko.md +163 -0
- package/README.md +340 -243
- package/README.pt-BR.md +163 -0
- package/README.ru.md +163 -0
- package/README.zh-CN.md +387 -0
- package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +605 -23
- package/assets/banner_dark.svg +186 -0
- package/assets/banner_light.svg +186 -0
- package/assets/icon_circle.svg +148 -0
- package/assets/icon_square.svg +369 -0
- package/assets/inapp_inspector_icon.gif +0 -0
- package/assets/inapp_inspector_icon_original.gif +0 -0
- package/assets/social_preview.png +0 -0
- package/assets/walkthrough.gif +0 -0
- package/dist/commonjs/capture.d.ts +2 -0
- package/dist/commonjs/capture.js +1 -1
- package/dist/commonjs/components/AnalyticsDetail.js +23 -23
- package/dist/commonjs/components/AnalyticsEventCard.js +1 -1
- package/dist/commonjs/components/AnalyticsGraph.js +44 -44
- package/dist/commonjs/components/AnimatedBrandOwl.d.ts +7 -0
- package/dist/commonjs/components/AnimatedBrandOwl.js +175 -0
- package/dist/commonjs/components/AppHeaderLogo.d.ts +1 -0
- package/dist/commonjs/components/AppHeaderLogo.js +5 -3
- package/dist/commonjs/components/BrandCircleIcon.js +28 -28
- package/dist/commonjs/components/BrandSquareIcon.d.ts +5 -0
- package/dist/commonjs/components/BrandSquareIcon.js +139 -0
- package/dist/commonjs/components/ConsoleLogCard.d.ts +2 -1
- package/dist/commonjs/components/ConsoleLogCard.js +23 -39
- package/dist/commonjs/components/Inspector/AboutModal.js +44 -54
- package/dist/commonjs/components/Inspector/AnalyticsTab.js +6 -6
- package/dist/commonjs/components/Inspector/ConfirmationModal.d.ts +13 -0
- package/dist/commonjs/components/Inspector/ConfirmationModal.js +30 -0
- package/dist/commonjs/components/Inspector/ConsoleTab.js +12 -12
- package/dist/commonjs/components/Inspector/CrashTab.js +44 -9
- package/dist/commonjs/components/Inspector/DebuggingTab.js +17 -17
- package/dist/commonjs/components/Inspector/DeviceInfoTab.js +1 -1
- package/dist/commonjs/components/Inspector/FabLauncher.js +30 -33
- package/dist/commonjs/components/Inspector/FeatureUnderDevNotice.js +2 -2
- package/dist/commonjs/components/Inspector/FloatingCaptureWidget.d.ts +3 -0
- package/dist/commonjs/components/Inspector/FloatingCaptureWidget.js +54 -0
- package/dist/commonjs/components/Inspector/GlobalCrashScreenModal.d.ts +11 -0
- package/dist/commonjs/components/Inspector/GlobalCrashScreenModal.js +234 -0
- package/dist/commonjs/components/Inspector/InspectorHeader.js +137 -127
- package/dist/commonjs/components/Inspector/LanguageSelectorModal.d.ts +15 -0
- package/dist/commonjs/components/Inspector/LanguageSelectorModal.js +172 -0
- package/dist/commonjs/components/Inspector/MainScreen.js +55 -35
- package/dist/commonjs/components/Inspector/MediaGalleryTab.js +40 -35
- package/dist/commonjs/components/Inspector/MediaPreviewModal.d.ts +36 -0
- package/dist/commonjs/components/Inspector/MediaPreviewModal.js +341 -135
- package/dist/commonjs/components/Inspector/NetworkTab.js +50 -50
- package/dist/commonjs/components/Inspector/NpmUpdateToast.js +1 -1
- package/dist/commonjs/components/Inspector/PushCard.d.ts +3 -2
- package/dist/commonjs/components/Inspector/PushCard.js +5 -4
- package/dist/commonjs/components/Inspector/PushDetail.js +41 -32
- package/dist/commonjs/components/Inspector/PushTab.js +12 -12
- package/dist/commonjs/components/Inspector/ReduxDetail.js +32 -32
- package/dist/commonjs/components/Inspector/ReduxTab.js +10 -8
- package/dist/commonjs/components/Inspector/SecondaryTelemetryStrip.d.ts +2 -0
- package/dist/commonjs/components/Inspector/SecondaryTelemetryStrip.js +43 -0
- package/dist/commonjs/components/Inspector/SettingsPanel.js +287 -139
- package/dist/commonjs/components/Inspector/SocketCard.d.ts +5 -0
- package/dist/commonjs/components/Inspector/SocketCard.js +116 -0
- package/dist/commonjs/components/Inspector/SocketDetail.d.ts +4 -0
- package/dist/commonjs/components/Inspector/SocketDetail.js +327 -0
- package/dist/commonjs/components/Inspector/SocketFilterModal.d.ts +13 -0
- package/dist/commonjs/components/Inspector/SocketFilterModal.js +69 -0
- package/dist/commonjs/components/Inspector/SocketTab.d.ts +3 -0
- package/dist/commonjs/components/Inspector/SocketTab.js +112 -0
- package/dist/commonjs/components/Inspector/StorageTab.js +39 -37
- package/dist/commonjs/components/Inspector/SupportModal.d.ts +7 -0
- package/dist/commonjs/components/Inspector/SupportModal.js +125 -0
- package/dist/commonjs/components/Inspector/SupportPage.d.ts +5 -0
- package/dist/commonjs/components/Inspector/SupportPage.js +227 -0
- package/dist/commonjs/components/Inspector/TabBar.js +24 -23
- package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +24 -24
- package/dist/commonjs/components/JsonViewer.js +50 -48
- package/dist/commonjs/components/LogCard.d.ts +3 -2
- package/dist/commonjs/components/LogCard.js +27 -25
- package/dist/commonjs/components/LogSyntaxHighlighter.js +5 -5
- package/dist/commonjs/components/ModuleErrorBoundary.js +5 -5
- package/dist/commonjs/components/NetworkIcons.d.ts +42 -0
- package/dist/commonjs/components/NetworkIcons.js +139 -6
- package/dist/commonjs/components/SegmentedTabs.js +1 -1
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/crash.d.ts +1 -1
- package/dist/commonjs/crash.js +1 -1
- package/dist/commonjs/customHooks/crashHandler.d.ts +2 -1
- package/dist/commonjs/customHooks/crashHandler.js +7 -7
- package/dist/commonjs/customHooks/socketLogger.d.ts +26 -0
- package/dist/commonjs/customHooks/socketLogger.js +2 -0
- package/dist/commonjs/editor.d.ts +13 -0
- package/dist/commonjs/editor.js +1 -0
- package/dist/commonjs/helpers/index.d.ts +1 -0
- package/dist/commonjs/helpers/index.js +3 -3
- package/dist/commonjs/helpers/memoryManager.d.ts +1 -0
- package/dist/commonjs/helpers/memoryManager.js +1 -1
- package/dist/commonjs/helpers/searchQueryParser.d.ts +1 -1
- package/dist/commonjs/helpers/searchQueryParser.js +5 -1
- package/dist/commonjs/helpers/settingsStore.d.ts +1 -0
- package/dist/commonjs/helpers/settingsStore.js +1 -1
- package/dist/commonjs/helpers/shareFormatter.d.ts +3 -1
- package/dist/commonjs/helpers/shareFormatter.js +8 -7
- package/dist/commonjs/helpers/telemetry.js +1 -1
- package/dist/commonjs/i18n/index.d.ts +99 -1
- package/dist/commonjs/i18n/index.js +1 -1
- package/dist/commonjs/i18n/locales/ar.json +1 -1
- package/dist/commonjs/i18n/locales/bn.json +1 -0
- package/dist/commonjs/i18n/locales/de.json +1 -1
- package/dist/commonjs/i18n/locales/en.json +1 -1
- package/dist/commonjs/i18n/locales/es.json +1 -1
- package/dist/commonjs/i18n/locales/fr.json +1 -1
- package/dist/commonjs/i18n/locales/gu.json +1 -0
- package/dist/commonjs/i18n/locales/hi.json +1 -1
- package/dist/commonjs/i18n/locales/id.json +1 -0
- package/dist/commonjs/i18n/locales/it.json +1 -0
- package/dist/commonjs/i18n/locales/ja.json +1 -1
- package/dist/commonjs/i18n/locales/kn.json +1 -0
- package/dist/commonjs/i18n/locales/ko.json +1 -1
- package/dist/commonjs/i18n/locales/ml.json +1 -0
- package/dist/commonjs/i18n/locales/mr.json +1 -0
- package/dist/commonjs/i18n/locales/nl.json +1 -0
- package/dist/commonjs/i18n/locales/pa.json +1 -0
- package/dist/commonjs/i18n/locales/pl.json +1 -0
- package/dist/commonjs/i18n/locales/pt.json +1 -1
- package/dist/commonjs/i18n/locales/ru.json +1 -1
- package/dist/commonjs/i18n/locales/ta.json +1 -0
- package/dist/commonjs/i18n/locales/te.json +1 -0
- package/dist/commonjs/i18n/locales/tr.json +1 -0
- package/dist/commonjs/i18n/locales/vi.json +1 -0
- package/dist/commonjs/i18n/locales/zh.json +1 -1
- package/dist/commonjs/index.d.ts +16 -13
- package/dist/commonjs/index.js +5 -4
- package/dist/commonjs/native/NativeInspector.d.ts +4 -0
- package/dist/commonjs/native/NativeInspector.js +1 -1
- package/dist/commonjs/native/NativeNetworkInspector.d.ts +4 -0
- package/dist/commonjs/socket.d.ts +4 -0
- package/dist/commonjs/socket.js +1 -0
- package/dist/commonjs/styles/AppColors.d.ts +42 -2
- package/dist/commonjs/styles/AppColors.js +1 -1
- package/dist/commonjs/styles/common.js +1 -1
- package/dist/commonjs/styles/index.d.ts +30 -0
- package/dist/commonjs/styles/index.js +1 -1
- package/dist/commonjs/types/editor.d.ts +96 -0
- package/dist/commonjs/types/editor.js +1 -0
- package/dist/commonjs/types/enums.d.ts +40 -0
- package/dist/commonjs/types/enums.js +1 -1
- package/dist/commonjs/types/interfaces.d.ts +143 -1
- package/dist/esm/capture.d.ts +2 -0
- package/dist/esm/capture.js +1 -1
- package/dist/esm/components/AnalyticsDetail.js +31 -31
- package/dist/esm/components/AnalyticsEventCard.js +1 -1
- package/dist/esm/components/AnalyticsGraph.js +74 -74
- package/dist/esm/components/AnimatedBrandOwl.d.ts +7 -0
- package/dist/esm/components/AnimatedBrandOwl.js +175 -0
- package/dist/esm/components/AppHeaderLogo.d.ts +1 -0
- package/dist/esm/components/AppHeaderLogo.js +7 -5
- package/dist/esm/components/BrandCircleIcon.js +70 -70
- package/dist/esm/components/BrandSquareIcon.d.ts +5 -0
- package/dist/esm/components/BrandSquareIcon.js +139 -0
- package/dist/esm/components/ConsoleLogCard.d.ts +2 -1
- package/dist/esm/components/ConsoleLogCard.js +61 -77
- package/dist/esm/components/Inspector/AboutModal.js +85 -95
- package/dist/esm/components/Inspector/AnalyticsTab.js +24 -24
- package/dist/esm/components/Inspector/ConfirmationModal.d.ts +13 -0
- package/dist/esm/components/Inspector/ConfirmationModal.js +30 -0
- package/dist/esm/components/Inspector/ConsoleTab.js +48 -48
- package/dist/esm/components/Inspector/CrashTab.js +203 -168
- package/dist/esm/components/Inspector/DebuggingTab.js +29 -29
- package/dist/esm/components/Inspector/DeviceInfoTab.js +75 -75
- package/dist/esm/components/Inspector/FabLauncher.js +57 -60
- package/dist/esm/components/Inspector/FeatureUnderDevNotice.js +7 -7
- package/dist/esm/components/Inspector/FloatingCaptureWidget.d.ts +3 -0
- package/dist/esm/components/Inspector/FloatingCaptureWidget.js +54 -0
- package/dist/esm/components/Inspector/GlobalCrashScreenModal.d.ts +11 -0
- package/dist/esm/components/Inspector/GlobalCrashScreenModal.js +234 -0
- package/dist/esm/components/Inspector/InspectorHeader.js +162 -152
- package/dist/esm/components/Inspector/LanguageSelectorModal.d.ts +15 -0
- package/dist/esm/components/Inspector/LanguageSelectorModal.js +172 -0
- package/dist/esm/components/Inspector/MainScreen.js +130 -110
- package/dist/esm/components/Inspector/MediaGalleryTab.js +83 -78
- package/dist/esm/components/Inspector/MediaPreviewModal.d.ts +36 -0
- package/dist/esm/components/Inspector/MediaPreviewModal.js +345 -139
- package/dist/esm/components/Inspector/NetworkTab.js +130 -130
- package/dist/esm/components/Inspector/NpmUpdateToast.js +1 -1
- package/dist/esm/components/Inspector/PushCard.d.ts +3 -2
- package/dist/esm/components/Inspector/PushCard.js +23 -22
- package/dist/esm/components/Inspector/PushDetail.js +45 -36
- package/dist/esm/components/Inspector/PushTab.js +23 -23
- package/dist/esm/components/Inspector/ReduxDetail.js +42 -42
- package/dist/esm/components/Inspector/ReduxTab.js +20 -18
- package/dist/esm/components/Inspector/SecondaryTelemetryStrip.d.ts +2 -0
- package/dist/esm/components/Inspector/SecondaryTelemetryStrip.js +43 -0
- package/dist/esm/components/Inspector/SettingsPanel.js +526 -378
- package/dist/esm/components/Inspector/SocketCard.d.ts +5 -0
- package/dist/esm/components/Inspector/SocketCard.js +116 -0
- package/dist/esm/components/Inspector/SocketDetail.d.ts +4 -0
- package/dist/esm/components/Inspector/SocketDetail.js +327 -0
- package/dist/esm/components/Inspector/SocketFilterModal.d.ts +13 -0
- package/dist/esm/components/Inspector/SocketFilterModal.js +69 -0
- package/dist/esm/components/Inspector/SocketTab.d.ts +3 -0
- package/dist/esm/components/Inspector/SocketTab.js +112 -0
- package/dist/esm/components/Inspector/StorageTab.js +139 -137
- package/dist/esm/components/Inspector/SupportModal.d.ts +7 -0
- package/dist/esm/components/Inspector/SupportModal.js +125 -0
- package/dist/esm/components/Inspector/SupportPage.d.ts +5 -0
- package/dist/esm/components/Inspector/SupportPage.js +227 -0
- package/dist/esm/components/Inspector/TabBar.js +36 -35
- package/dist/esm/components/Inspector/UpdateAvailableModal.js +22 -22
- package/dist/esm/components/JsonViewer.js +66 -64
- package/dist/esm/components/LogCard.d.ts +3 -2
- package/dist/esm/components/LogCard.js +57 -55
- package/dist/esm/components/LogSyntaxHighlighter.js +5 -5
- package/dist/esm/components/ModuleErrorBoundary.js +23 -23
- package/dist/esm/components/NetworkIcons.d.ts +42 -0
- package/dist/esm/components/NetworkIcons.js +139 -6
- package/dist/esm/components/SegmentedTabs.js +7 -7
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/crash.d.ts +1 -1
- package/dist/esm/crash.js +1 -1
- package/dist/esm/customHooks/crashHandler.d.ts +2 -1
- package/dist/esm/customHooks/crashHandler.js +7 -7
- package/dist/esm/customHooks/socketLogger.d.ts +26 -0
- package/dist/esm/customHooks/socketLogger.js +2 -0
- package/dist/esm/editor.d.ts +13 -0
- package/dist/esm/editor.js +1 -0
- package/dist/esm/helpers/index.d.ts +1 -0
- package/dist/esm/helpers/index.js +4 -4
- package/dist/esm/helpers/memoryManager.d.ts +1 -0
- package/dist/esm/helpers/memoryManager.js +1 -1
- package/dist/esm/helpers/searchQueryParser.d.ts +1 -1
- package/dist/esm/helpers/searchQueryParser.js +5 -1
- package/dist/esm/helpers/settingsStore.d.ts +1 -0
- package/dist/esm/helpers/settingsStore.js +1 -1
- package/dist/esm/helpers/shareFormatter.d.ts +3 -1
- package/dist/esm/helpers/shareFormatter.js +8 -7
- package/dist/esm/helpers/telemetry.js +1 -1
- package/dist/esm/i18n/index.d.ts +99 -1
- package/dist/esm/i18n/index.js +1 -1
- package/dist/esm/i18n/locales/ar.json +1 -1
- package/dist/esm/i18n/locales/bn.json +1 -0
- package/dist/esm/i18n/locales/de.json +1 -1
- package/dist/esm/i18n/locales/en.json +1 -1
- package/dist/esm/i18n/locales/es.json +1 -1
- package/dist/esm/i18n/locales/fr.json +1 -1
- package/dist/esm/i18n/locales/gu.json +1 -0
- package/dist/esm/i18n/locales/hi.json +1 -1
- package/dist/esm/i18n/locales/id.json +1 -0
- package/dist/esm/i18n/locales/it.json +1 -0
- package/dist/esm/i18n/locales/ja.json +1 -1
- package/dist/esm/i18n/locales/kn.json +1 -0
- package/dist/esm/i18n/locales/ko.json +1 -1
- package/dist/esm/i18n/locales/ml.json +1 -0
- package/dist/esm/i18n/locales/mr.json +1 -0
- package/dist/esm/i18n/locales/nl.json +1 -0
- package/dist/esm/i18n/locales/pa.json +1 -0
- package/dist/esm/i18n/locales/pl.json +1 -0
- package/dist/esm/i18n/locales/pt.json +1 -1
- package/dist/esm/i18n/locales/ru.json +1 -1
- package/dist/esm/i18n/locales/ta.json +1 -0
- package/dist/esm/i18n/locales/te.json +1 -0
- package/dist/esm/i18n/locales/tr.json +1 -0
- package/dist/esm/i18n/locales/vi.json +1 -0
- package/dist/esm/i18n/locales/zh.json +1 -1
- package/dist/esm/index.d.ts +16 -13
- package/dist/esm/index.js +8 -7
- package/dist/esm/native/NativeInspector.d.ts +4 -0
- package/dist/esm/native/NativeInspector.js +1 -1
- package/dist/esm/native/NativeNetworkInspector.d.ts +4 -0
- package/dist/esm/socket.d.ts +4 -0
- package/dist/esm/socket.js +1 -0
- package/dist/esm/styles/AppColors.d.ts +42 -2
- package/dist/esm/styles/AppColors.js +1 -1
- package/dist/esm/styles/common.js +1 -1
- package/dist/esm/styles/index.d.ts +30 -0
- package/dist/esm/styles/index.js +1 -1
- package/dist/esm/types/editor.d.ts +96 -0
- package/dist/esm/types/editor.js +1 -0
- package/dist/esm/types/enums.d.ts +40 -0
- package/dist/esm/types/enums.js +1 -1
- package/dist/esm/types/interfaces.d.ts +143 -1
- package/editor.d.ts +1 -0
- package/editor.js +1 -0
- package/ios/NetworkInspectorModule.mm +531 -12
- package/package.json +213 -2
- package/react-native-inapp-inspector.podspec +1 -1
- package/src/native/NativeInspector.ts +65 -4
- package/src/native/NativeNetworkInspector.ts +4 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#import <ReplayKit/ReplayKit.h>
|
|
7
7
|
#import <AVFoundation/AVFoundation.h>
|
|
8
8
|
#import <AVKit/AVKit.h>
|
|
9
|
+
#import <CoreImage/CoreImage.h>
|
|
9
10
|
#import <ImageIO/ImageIO.h>
|
|
10
11
|
#import <MobileCoreServices/MobileCoreServices.h>
|
|
11
12
|
|
|
@@ -539,6 +540,21 @@ RCT_EXPORT_MODULE(NetworkInspectorModule);
|
|
|
539
540
|
return YES;
|
|
540
541
|
}
|
|
541
542
|
|
|
543
|
+
- (NSDictionary *)constantsToExport {
|
|
544
|
+
NSBundle *mainBundle = [NSBundle mainBundle];
|
|
545
|
+
NSString *appName = [mainBundle objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: [mainBundle objectForInfoDictionaryKey:@"CFBundleName"] ?: @"App";
|
|
546
|
+
NSString *appVersion = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"1.0";
|
|
547
|
+
NSString *appBuild = [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] ?: @"1";
|
|
548
|
+
NSString *appBundleId = [mainBundle bundleIdentifier] ?: @"";
|
|
549
|
+
return @{
|
|
550
|
+
@"appName": appName,
|
|
551
|
+
@"appVersion": appVersion,
|
|
552
|
+
@"appBuild": appBuild,
|
|
553
|
+
@"appBundleId": appBundleId,
|
|
554
|
+
@"appPackageName": appBundleId
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
542
558
|
- (void)safeSendEvent:(NSString *)eventName body:(id)body {
|
|
543
559
|
if (!hasListeners) return;
|
|
544
560
|
if (self.bridge == nil) return;
|
|
@@ -1235,6 +1251,9 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
|
|
|
1235
1251
|
rgbColorSpace,
|
|
1236
1252
|
(CGBitmapInfo)kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaPremultipliedFirst);
|
|
1237
1253
|
if (context) {
|
|
1254
|
+
// Invert Y axis for CGBitmapContext so row 0 aligns with the top of the video frame
|
|
1255
|
+
CGContextTranslateCTM(context, 0, size.height);
|
|
1256
|
+
CGContextScaleCTM(context, 1.0, -1.0);
|
|
1238
1257
|
UIGraphicsPushContext(context);
|
|
1239
1258
|
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
|
|
1240
1259
|
UIGraphicsPopContext();
|
|
@@ -1275,10 +1294,25 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
|
|
|
1275
1294
|
return;
|
|
1276
1295
|
}
|
|
1277
1296
|
|
|
1297
|
+
// Read bitrate from options (default: adaptive based on resolution)
|
|
1298
|
+
double bitrateParam = [options[@"bitrate"] doubleValue];
|
|
1299
|
+
if (bitrateParam <= 0.0) {
|
|
1300
|
+
// Auto-calculate: ~6 Mbps for 1080p, scales proportionally
|
|
1301
|
+
bitrateParam = (double)(width * height) * 4.0;
|
|
1302
|
+
if (bitrateParam < 1000000.0) bitrateParam = 1000000.0; // Floor: 1 Mbps
|
|
1303
|
+
if (bitrateParam > 20000000.0) bitrateParam = 20000000.0; // Ceiling: 20 Mbps
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1278
1306
|
NSDictionary *videoSettings = @{
|
|
1279
1307
|
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
1280
1308
|
AVVideoWidthKey: @(videoSize.width),
|
|
1281
1309
|
AVVideoHeightKey: @(videoSize.height),
|
|
1310
|
+
AVVideoCompressionPropertiesKey: @{
|
|
1311
|
+
AVVideoAverageBitRateKey: @((NSInteger)bitrateParam),
|
|
1312
|
+
AVVideoMaxKeyFrameIntervalKey: @(30),
|
|
1313
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
1314
|
+
AVVideoExpectedSourceFrameRateKey: @([options[@"fps"] doubleValue] > 0 ? [options[@"fps"] doubleValue] : 30),
|
|
1315
|
+
},
|
|
1282
1316
|
};
|
|
1283
1317
|
|
|
1284
1318
|
self->_softwareWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
|
|
@@ -1307,9 +1341,12 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
|
|
|
1307
1341
|
self->_softwareVideoHeight = height;
|
|
1308
1342
|
|
|
1309
1343
|
double fps = [options[@"fps"] doubleValue];
|
|
1310
|
-
if (fps <= 0.0 || fps >
|
|
1344
|
+
if (fps <= 0.0 || fps > 60.0) fps = 30.0;
|
|
1311
1345
|
double frameInterval = 1.0 / fps;
|
|
1312
1346
|
|
|
1347
|
+
double maxDuration = [options[@"maxDurationSeconds"] doubleValue];
|
|
1348
|
+
if (maxDuration <= 0.0 || maxDuration > 300.0) maxDuration = 120.0;
|
|
1349
|
+
|
|
1313
1350
|
// Immediately capture frame 0 & save thumbnail
|
|
1314
1351
|
UIImage *firstImg = [self captureScreenHierarchyWithScale:(CGFloat)scaleParam];
|
|
1315
1352
|
if (firstImg) {
|
|
@@ -1336,24 +1373,54 @@ RCT_EXPORT_METHOD(getNativeCachedPage:(NSString *)pageKey
|
|
|
1336
1373
|
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));
|
|
1337
1374
|
|
|
1338
1375
|
__weak NetworkInspectorModule *weakSelf = self;
|
|
1376
|
+
__block BOOL isFrameBusy = NO;
|
|
1377
|
+
NSTimeInterval recordingStartTime = self->_softwareRecordingStartTime;
|
|
1339
1378
|
dispatch_source_set_event_handler(self->_softwareRecordingTimerSource, ^{
|
|
1340
1379
|
NetworkInspectorModule *strongSelf = weakSelf;
|
|
1341
1380
|
if (!strongSelf || !strongSelf->_isSoftwareRecordingActive) return;
|
|
1342
1381
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1382
|
+
// Auto-stop guard: enforce max recording duration
|
|
1383
|
+
NSTimeInterval elapsed = [[NSDate date] timeIntervalSince1970] - recordingStartTime;
|
|
1384
|
+
if (elapsed >= maxDuration) {
|
|
1385
|
+
strongSelf->_isSoftwareRecordingActive = NO;
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (isFrameBusy) {
|
|
1390
|
+
// Drop frame tick if previous frame capture or encoding is still executing to prevent UI freezing
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
isFrameBusy = YES;
|
|
1394
|
+
|
|
1395
|
+
// Fast snapshot on main queue without blocking user interactions
|
|
1396
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1397
|
+
if (!strongSelf || !strongSelf->_isSoftwareRecordingActive) {
|
|
1398
|
+
isFrameBusy = NO;
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (!strongSelf->_softwareAssetWriter || strongSelf->_softwareAssetWriter.status != AVAssetWriterStatusWriting) {
|
|
1402
|
+
isFrameBusy = NO;
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1346
1405
|
|
|
1347
1406
|
UIImage *frameImg = [strongSelf captureScreenHierarchyWithScale:(CGFloat)scaleParam];
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1407
|
+
|
|
1408
|
+
// Offload all heavy CVPixelBuffer creation, context drawing, and AVAssetWriter append to background queue
|
|
1409
|
+
dispatch_async(queue, ^{
|
|
1410
|
+
@try {
|
|
1411
|
+
if (strongSelf && strongSelf->_isSoftwareRecordingActive && frameImg && strongSelf->_softwareWriterInput.isReadyForMoreMediaData) {
|
|
1412
|
+
CVPixelBufferRef buffer = [strongSelf createPixelBufferFromUIImage:frameImg size:videoSize];
|
|
1413
|
+
if (buffer) {
|
|
1414
|
+
CMTime presentTime = CMTimeMake((int64_t)(strongSelf->_softwareFrameIndex * 1000 / fps), 1000);
|
|
1415
|
+
[strongSelf->_softwarePixelBufferAdaptor appendPixelBuffer:buffer withPresentationTime:presentTime];
|
|
1416
|
+
CVPixelBufferRelease(buffer);
|
|
1417
|
+
strongSelf->_softwareFrameIndex++;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} @finally {
|
|
1421
|
+
isFrameBusy = NO;
|
|
1355
1422
|
}
|
|
1356
|
-
}
|
|
1423
|
+
});
|
|
1357
1424
|
});
|
|
1358
1425
|
});
|
|
1359
1426
|
|
|
@@ -1487,6 +1554,30 @@ RCT_EXPORT_METHOD(stopVideoRecording:(RCTPromiseResolveBlock)resolve
|
|
|
1487
1554
|
|
|
1488
1555
|
if (thumbPath && [[NSFileManager defaultManager] fileExistsAtPath:thumbPath]) {
|
|
1489
1556
|
result[@"thumbnailUri"] = [NSURL fileURLWithPath:thumbPath].absoluteString;
|
|
1557
|
+
} else {
|
|
1558
|
+
@try {
|
|
1559
|
+
NSURL *vidUrl = [NSURL fileURLWithPath:filePath];
|
|
1560
|
+
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:vidUrl options:nil];
|
|
1561
|
+
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:asset];
|
|
1562
|
+
gen.appliesPreferredTrackTransform = YES;
|
|
1563
|
+
gen.requestedTimeToleranceBefore = kCMTimePositiveInfinity;
|
|
1564
|
+
gen.requestedTimeToleranceAfter = kCMTimePositiveInfinity;
|
|
1565
|
+
gen.maximumSize = CGSizeMake(720, 720);
|
|
1566
|
+
CMTime time = kCMTimeZero;
|
|
1567
|
+
NSError *err = nil;
|
|
1568
|
+
CGImageRef imgRef = [gen copyCGImageAtTime:time actualTime:NULL error:&err];
|
|
1569
|
+
if (imgRef) {
|
|
1570
|
+
UIImage *thumbImg = [UIImage imageWithCGImage:imgRef];
|
|
1571
|
+
CGImageRelease(imgRef);
|
|
1572
|
+
NSData *tData = UIImageJPEGRepresentation(thumbImg, 0.85);
|
|
1573
|
+
if (tData) {
|
|
1574
|
+
NSString *generatedThumbFile = [NSString stringWithFormat:@"thumb_%lld.jpg", timestamp];
|
|
1575
|
+
NSString *generatedThumbPath = [[self getCapturesDirectory] stringByAppendingPathComponent:generatedThumbFile];
|
|
1576
|
+
[tData writeToFile:generatedThumbPath atomically:YES];
|
|
1577
|
+
result[@"thumbnailUri"] = [NSURL fileURLWithPath:generatedThumbPath].absoluteString;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
} @catch (NSException *e) {}
|
|
1490
1581
|
}
|
|
1491
1582
|
|
|
1492
1583
|
resolve(result);
|
|
@@ -1821,6 +1912,434 @@ RCT_EXPORT_METHOD(clearAllCapturedMedia:(RCTPromiseResolveBlock)resolve
|
|
|
1821
1912
|
});
|
|
1822
1913
|
}
|
|
1823
1914
|
|
|
1915
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1916
|
+
// 100% Native Media Editor: Photo Editing (CoreImage + ImageIO)
|
|
1917
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1918
|
+
|
|
1919
|
+
RCT_EXPORT_METHOD(editPhoto:(NSDictionary *)options
|
|
1920
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1921
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
1922
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
1923
|
+
@try {
|
|
1924
|
+
NSString *rawUri = options[@"uri"];
|
|
1925
|
+
if (!rawUri || rawUri.length == 0) {
|
|
1926
|
+
reject(@"INVALID_URI", @"Image URI is missing or empty", nil);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
NSString *cleanPath = rawUri;
|
|
1931
|
+
if ([cleanPath hasPrefix:@"file://"]) {
|
|
1932
|
+
cleanPath = [cleanPath substringFromIndex:7];
|
|
1933
|
+
}
|
|
1934
|
+
cleanPath = [cleanPath stringByRemovingPercentEncoding];
|
|
1935
|
+
|
|
1936
|
+
UIImage *sourceImage = [UIImage imageWithContentsOfFile:cleanPath];
|
|
1937
|
+
if (!sourceImage) {
|
|
1938
|
+
reject(@"IMAGE_LOAD_FAILED", @"Failed to load source image", nil);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// 1. Convert to CIImage
|
|
1943
|
+
CIImage *ciImage = [CIImage imageWithCGImage:sourceImage.CGImage];
|
|
1944
|
+
if (!ciImage) {
|
|
1945
|
+
ciImage = [CIImage imageWithContentsOfURL:[NSURL fileURLWithPath:cleanPath]];
|
|
1946
|
+
}
|
|
1947
|
+
if (!ciImage) {
|
|
1948
|
+
reject(@"CIIMAGE_FAILED", @"Failed to initialize CoreImage pipeline", nil);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// 2. Apply Rotation & Flip
|
|
1953
|
+
NSInteger rotation = [options[@"rotation"] integerValue];
|
|
1954
|
+
BOOL flipH = [options[@"flipHorizontal"] boolValue];
|
|
1955
|
+
BOOL flipV = [options[@"flipVertical"] boolValue];
|
|
1956
|
+
|
|
1957
|
+
CGAffineTransform transform = CGAffineTransformIdentity;
|
|
1958
|
+
if (rotation == 90) {
|
|
1959
|
+
transform = CGAffineTransformRotate(transform, -M_PI_2);
|
|
1960
|
+
} else if (rotation == 180) {
|
|
1961
|
+
transform = CGAffineTransformRotate(transform, M_PI);
|
|
1962
|
+
} else if (rotation == 270) {
|
|
1963
|
+
transform = CGAffineTransformRotate(transform, M_PI_2);
|
|
1964
|
+
}
|
|
1965
|
+
if (flipH) {
|
|
1966
|
+
transform = CGAffineTransformScale(transform, -1, 1);
|
|
1967
|
+
}
|
|
1968
|
+
if (flipV) {
|
|
1969
|
+
transform = CGAffineTransformScale(transform, 1, -1);
|
|
1970
|
+
}
|
|
1971
|
+
if (!CGAffineTransformIsIdentity(transform)) {
|
|
1972
|
+
ciImage = [ciImage imageByApplyingTransform:transform];
|
|
1973
|
+
CGRect newExtent = ciImage.extent;
|
|
1974
|
+
CGAffineTransform fixOrigin = CGAffineTransformMakeTranslation(-newExtent.origin.x, -newExtent.origin.y);
|
|
1975
|
+
ciImage = [ciImage imageByApplyingTransform:fixOrigin];
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// 3. Apply Crop
|
|
1979
|
+
NSDictionary *cropDict = options[@"crop"];
|
|
1980
|
+
if (cropDict && [cropDict isKindOfClass:[NSDictionary class]]) {
|
|
1981
|
+
CGFloat x = [cropDict[@"x"] doubleValue];
|
|
1982
|
+
CGFloat y = [cropDict[@"y"] doubleValue];
|
|
1983
|
+
CGFloat width = [cropDict[@"width"] doubleValue];
|
|
1984
|
+
CGFloat height = [cropDict[@"height"] doubleValue];
|
|
1985
|
+
BOOL isNorm = [cropDict[@"isNormalized"] boolValue];
|
|
1986
|
+
|
|
1987
|
+
CGRect extent = ciImage.extent;
|
|
1988
|
+
if (isNorm) {
|
|
1989
|
+
x *= extent.size.width;
|
|
1990
|
+
y *= extent.size.height;
|
|
1991
|
+
width *= extent.size.width;
|
|
1992
|
+
height *= extent.size.height;
|
|
1993
|
+
}
|
|
1994
|
+
// CoreImage coordinate system (bottom-left origin)
|
|
1995
|
+
CGFloat ciY = extent.size.height - (y + height);
|
|
1996
|
+
CGRect cropRect = CGRectMake(x, ciY, width, height);
|
|
1997
|
+
CGRect intersection = CGRectIntersection(extent, cropRect);
|
|
1998
|
+
if (!CGRectIsNull(intersection) && intersection.size.width > 0 && intersection.size.height > 0) {
|
|
1999
|
+
ciImage = [ciImage imageByCroppingToRect:intersection];
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// 4. Color Grading Adjustments (CIColorControls, CITemperatureAndTint, CIVignette)
|
|
2004
|
+
NSDictionary *adjustments = options[@"adjustments"];
|
|
2005
|
+
if (adjustments && [adjustments isKindOfClass:[NSDictionary class]]) {
|
|
2006
|
+
NSNumber *brightness = adjustments[@"brightness"];
|
|
2007
|
+
NSNumber *contrast = adjustments[@"contrast"];
|
|
2008
|
+
NSNumber *saturation = adjustments[@"saturation"];
|
|
2009
|
+
|
|
2010
|
+
if (brightness || contrast || saturation) {
|
|
2011
|
+
CIFilter *colorControls = [CIFilter filterWithName:@"CIColorControls"];
|
|
2012
|
+
[colorControls setValue:ciImage forKey:kCIInputImageKey];
|
|
2013
|
+
if (brightness) [colorControls setValue:brightness forKey:kCIInputBrightnessKey];
|
|
2014
|
+
if (contrast) [colorControls setValue:contrast forKey:kCIInputContrastKey];
|
|
2015
|
+
if (saturation) [colorControls setValue:saturation forKey:kCIInputSaturationKey];
|
|
2016
|
+
ciImage = colorControls.outputImage ?: ciImage;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
NSNumber *temperature = adjustments[@"temperature"];
|
|
2020
|
+
if (temperature && [temperature doubleValue] != 0) {
|
|
2021
|
+
CIFilter *tempFilter = [CIFilter filterWithName:@"CITemperatureAndTint"];
|
|
2022
|
+
[tempFilter setValue:ciImage forKey:kCIInputImageKey];
|
|
2023
|
+
CIVector *neutral = [CIVector vectorWithX:6500 Y:0];
|
|
2024
|
+
CGFloat shift = [temperature doubleValue] * 2000;
|
|
2025
|
+
CIVector *target = [CIVector vectorWithX:6500 + shift Y:0];
|
|
2026
|
+
[tempFilter setValue:neutral forKey:@"inputNeutral"];
|
|
2027
|
+
[tempFilter setValue:target forKey:@"inputTargetNeutral"];
|
|
2028
|
+
ciImage = tempFilter.outputImage ?: ciImage;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
NSNumber *vignette = adjustments[@"vignette"];
|
|
2032
|
+
if (vignette && [vignette doubleValue] > 0) {
|
|
2033
|
+
CIFilter *vigFilter = [CIFilter filterWithName:@"CIVignette"];
|
|
2034
|
+
[vigFilter setValue:ciImage forKey:kCIInputImageKey];
|
|
2035
|
+
[vigFilter setValue:@([vignette doubleValue] * 2.0) forKey:kCIInputIntensityKey];
|
|
2036
|
+
[vigFilter setValue:@(1.0) forKey:kCIInputRadiusKey];
|
|
2037
|
+
ciImage = vigFilter.outputImage ?: ciImage;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
NSNumber *sharpen = adjustments[@"sharpen"];
|
|
2041
|
+
if (sharpen && [sharpen doubleValue] > 0) {
|
|
2042
|
+
CIFilter *sharpFilter = [CIFilter filterWithName:@"CISharpenLuminance"];
|
|
2043
|
+
[sharpFilter setValue:ciImage forKey:kCIInputImageKey];
|
|
2044
|
+
[sharpFilter setValue:@([sharpen doubleValue] * 2.0) forKey:kCIInputSharpnessKey];
|
|
2045
|
+
ciImage = sharpFilter.outputImage ?: ciImage;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// 5. Preset Filters
|
|
2050
|
+
NSString *preset = options[@"filterPreset"];
|
|
2051
|
+
if (preset && preset.length > 0 && ![preset isEqualToString:@"none"]) {
|
|
2052
|
+
NSString *filterName = nil;
|
|
2053
|
+
if ([preset isEqualToString:@"mono"]) filterName = @"CIPhotoEffectMono";
|
|
2054
|
+
else if ([preset isEqualToString:@"noir"]) filterName = @"CIPhotoEffectNoir";
|
|
2055
|
+
else if ([preset isEqualToString:@"sepia"]) filterName = @"CISepiaTone";
|
|
2056
|
+
else if ([preset isEqualToString:@"vibrant"]) filterName = @"CIPhotoEffectChrome";
|
|
2057
|
+
else if ([preset isEqualToString:@"fade"]) filterName = @"CIPhotoEffectFade";
|
|
2058
|
+
else if ([preset isEqualToString:@"vintage"]) filterName = @"CIPhotoEffectInstant";
|
|
2059
|
+
|
|
2060
|
+
if (filterName) {
|
|
2061
|
+
CIFilter *pFilter = [CIFilter filterWithName:filterName];
|
|
2062
|
+
[pFilter setValue:ciImage forKey:kCIInputImageKey];
|
|
2063
|
+
ciImage = pFilter.outputImage ?: ciImage;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// 6. GPU Render to File
|
|
2068
|
+
CIContext *context = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer: @(NO)}];
|
|
2069
|
+
CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
|
|
2070
|
+
if (!cgImage) {
|
|
2071
|
+
reject(@"RENDER_FAILED", @"Failed to render CGImage", nil);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
UIImage *resultImage = [UIImage imageWithCGImage:cgImage];
|
|
2076
|
+
CGImageRelease(cgImage);
|
|
2077
|
+
|
|
2078
|
+
// 7. Apply Redactions & Annotations Overlay if provided
|
|
2079
|
+
NSArray *redactions = options[@"redactions"];
|
|
2080
|
+
NSArray *annotations = options[@"annotations"];
|
|
2081
|
+
if ((redactions && [redactions isKindOfClass:[NSArray class]] && redactions.count > 0) ||
|
|
2082
|
+
(annotations && [annotations isKindOfClass:[NSArray class]] && annotations.count > 0)) {
|
|
2083
|
+
UIGraphicsBeginImageContextWithOptions(resultImage.size, NO, 1.0);
|
|
2084
|
+
[resultImage drawInRect:CGRectMake(0, 0, resultImage.size.width, resultImage.size.height)];
|
|
2085
|
+
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
2086
|
+
|
|
2087
|
+
if (redactions && [redactions isKindOfClass:[NSArray class]]) {
|
|
2088
|
+
for (NSDictionary *box in redactions) {
|
|
2089
|
+
CGFloat x = [box[@"x"] doubleValue];
|
|
2090
|
+
CGFloat y = [box[@"y"] doubleValue];
|
|
2091
|
+
CGFloat w = [box[@"width"] doubleValue];
|
|
2092
|
+
CGFloat h = [box[@"height"] doubleValue];
|
|
2093
|
+
BOOL isNorm = [box[@"isNormalized"] boolValue];
|
|
2094
|
+
if (isNorm) {
|
|
2095
|
+
x *= resultImage.size.width;
|
|
2096
|
+
y *= resultImage.size.height;
|
|
2097
|
+
w *= resultImage.size.width;
|
|
2098
|
+
h *= resultImage.size.height;
|
|
2099
|
+
}
|
|
2100
|
+
CGContextSetFillColorWithColor(ctx, [UIColor blackColor].CGColor);
|
|
2101
|
+
CGContextFillRect(ctx, CGRectMake(x, y, w, h));
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (annotations && [annotations isKindOfClass:[NSArray class]]) {
|
|
2106
|
+
for (NSDictionary *ann in annotations) {
|
|
2107
|
+
CGFloat x = [ann[@"x"] doubleValue];
|
|
2108
|
+
CGFloat y = [ann[@"y"] doubleValue];
|
|
2109
|
+
CGFloat w = [ann[@"width"] doubleValue];
|
|
2110
|
+
CGFloat h = [ann[@"height"] doubleValue];
|
|
2111
|
+
BOOL isNorm = [ann[@"isNormalized"] boolValue];
|
|
2112
|
+
if (isNorm) {
|
|
2113
|
+
x *= resultImage.size.width;
|
|
2114
|
+
y *= resultImage.size.height;
|
|
2115
|
+
w *= resultImage.size.width;
|
|
2116
|
+
h *= resultImage.size.height;
|
|
2117
|
+
}
|
|
2118
|
+
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
|
|
2119
|
+
CGContextSetLineWidth(ctx, 4.0);
|
|
2120
|
+
CGContextStrokeRect(ctx, CGRectMake(x, y, w, h));
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
resultImage = UIGraphicsGetImageFromCurrentImageContext();
|
|
2125
|
+
UIGraphicsEndImageContext();
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
NSString *format = [options[@"format"] lowercaseString] ?: @"jpeg";
|
|
2129
|
+
CGFloat quality = options[@"quality"] ? [options[@"quality"] doubleValue] : 0.9;
|
|
2130
|
+
NSData *imageData = nil;
|
|
2131
|
+
NSString *ext = @"jpg";
|
|
2132
|
+
NSString *mime = @"image/jpeg";
|
|
2133
|
+
|
|
2134
|
+
if ([format isEqualToString:@"png"]) {
|
|
2135
|
+
imageData = UIImagePNGRepresentation(resultImage);
|
|
2136
|
+
ext = @"png";
|
|
2137
|
+
mime = @"image/png";
|
|
2138
|
+
} else {
|
|
2139
|
+
imageData = UIImageJPEGRepresentation(resultImage, quality);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
NSDateFormatter *df = [[NSDateFormatter alloc] init];
|
|
2143
|
+
[df setDateFormat:@"yyyyMMdd_HHmmss_SSS"];
|
|
2144
|
+
NSString *fileName = [NSString stringWithFormat:@"edit_%@.%@", [df stringFromDate:[NSDate date]], ext];
|
|
2145
|
+
NSString *outPath = [[self getCapturesDirectory] stringByAppendingPathComponent:fileName];
|
|
2146
|
+
[imageData writeToFile:outPath atomically:YES];
|
|
2147
|
+
|
|
2148
|
+
resolve(@{
|
|
2149
|
+
@"uri": [NSURL fileURLWithPath:outPath].absoluteString,
|
|
2150
|
+
@"width": @(resultImage.size.width),
|
|
2151
|
+
@"height": @(resultImage.size.height),
|
|
2152
|
+
@"size": @(imageData.length),
|
|
2153
|
+
@"mimeType": mime,
|
|
2154
|
+
@"format": format,
|
|
2155
|
+
});
|
|
2156
|
+
} @catch (NSException *ex) {
|
|
2157
|
+
reject(@"EDIT_FAILED", ex.reason ?: @"Photo edit failed", nil);
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2163
|
+
// 100% Native Media Editor: Video Trimming (AVFoundation)
|
|
2164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2165
|
+
|
|
2166
|
+
RCT_EXPORT_METHOD(trimVideo:(NSDictionary *)options
|
|
2167
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
2168
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
2169
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
2170
|
+
@try {
|
|
2171
|
+
NSString *rawUri = options[@"uri"];
|
|
2172
|
+
if (!rawUri || rawUri.length == 0) {
|
|
2173
|
+
reject(@"INVALID_URI", @"Video URI is missing", nil);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
NSURL *sourceUrl = [NSURL URLWithString:rawUri];
|
|
2178
|
+
if (!sourceUrl.scheme) {
|
|
2179
|
+
sourceUrl = [NSURL fileURLWithPath:rawUri];
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:sourceUrl options:nil];
|
|
2183
|
+
double startMs = [options[@"startTimeMs"] doubleValue];
|
|
2184
|
+
double endMs = [options[@"endTimeMs"] doubleValue];
|
|
2185
|
+
BOOL mute = [options[@"mute"] boolValue] || [options[@"muteAudio"] boolValue];
|
|
2186
|
+
|
|
2187
|
+
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
|
|
2188
|
+
if (!videoTrack) {
|
|
2189
|
+
reject(@"NO_VIDEO_TRACK", @"Source video has no valid video track", nil);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
double trackDurationSec = CMTimeGetSeconds(videoTrack.timeRange.duration);
|
|
2194
|
+
if (isnan(trackDurationSec) || trackDurationSec <= 0) {
|
|
2195
|
+
trackDurationSec = CMTimeGetSeconds(asset.duration);
|
|
2196
|
+
}
|
|
2197
|
+
double trackDurationMs = trackDurationSec * 1000.0;
|
|
2198
|
+
if (endMs > trackDurationMs || endMs <= 0) {
|
|
2199
|
+
endMs = trackDurationMs;
|
|
2200
|
+
}
|
|
2201
|
+
if (startMs >= endMs) {
|
|
2202
|
+
startMs = 0;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
AVMutableComposition *composition = [AVMutableComposition composition];
|
|
2206
|
+
AVMutableCompositionTrack *compVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
|
|
2207
|
+
|
|
2208
|
+
CMTime startTime = CMTimeMakeWithSeconds(startMs / 1000.0, 600);
|
|
2209
|
+
CMTime durationTime = CMTimeMakeWithSeconds((endMs - startMs) / 1000.0, 600);
|
|
2210
|
+
CMTimeRange timeRange = CMTimeRangeMake(startTime, durationTime);
|
|
2211
|
+
|
|
2212
|
+
NSError *error = nil;
|
|
2213
|
+
[compVideoTrack insertTimeRange:timeRange ofTrack:videoTrack atTime:kCMTimeZero error:&error];
|
|
2214
|
+
compVideoTrack.preferredTransform = videoTrack.preferredTransform;
|
|
2215
|
+
|
|
2216
|
+
if (!mute) {
|
|
2217
|
+
AVAssetTrack *audioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
|
|
2218
|
+
if (audioTrack) {
|
|
2219
|
+
AVMutableCompositionTrack *compAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
|
|
2220
|
+
CMTimeRange audioTimeRange = timeRange;
|
|
2221
|
+
double audioDurationSec = CMTimeGetSeconds(audioTrack.timeRange.duration);
|
|
2222
|
+
if (!isnan(audioDurationSec) && audioDurationSec > 0) {
|
|
2223
|
+
double audioDurationMs = audioDurationSec * 1000.0;
|
|
2224
|
+
if (endMs > audioDurationMs) {
|
|
2225
|
+
audioTimeRange = CMTimeRangeMake(startTime, CMTimeMakeWithSeconds((audioDurationMs - startMs) / 1000.0, 600));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
[compAudioTrack insertTimeRange:audioTimeRange ofTrack:audioTrack atTime:kCMTimeZero error:nil];
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
NSString *preset = AVAssetExportPresetHighestQuality;
|
|
2233
|
+
NSString *quality = options[@"quality"];
|
|
2234
|
+
if ([quality isEqualToString:@"medium"]) preset = AVAssetExportPreset1280x720;
|
|
2235
|
+
else if ([quality isEqualToString:@"low"]) preset = AVAssetExportPreset640x480;
|
|
2236
|
+
|
|
2237
|
+
AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:composition presetName:preset];
|
|
2238
|
+
NSDateFormatter *df = [[NSDateFormatter alloc] init];
|
|
2239
|
+
[df setDateFormat:@"yyyyMMdd_HHmmss_SSS"];
|
|
2240
|
+
NSString *fileName = [NSString stringWithFormat:@"trim_%@.mp4", [df stringFromDate:[NSDate date]]];
|
|
2241
|
+
NSString *outPath = [[self getCapturesDirectory] stringByAppendingPathComponent:fileName];
|
|
2242
|
+
NSURL *outUrl = [NSURL fileURLWithPath:outPath];
|
|
2243
|
+
|
|
2244
|
+
exportSession.outputURL = outUrl;
|
|
2245
|
+
exportSession.outputFileType = AVFileTypeMPEG4;
|
|
2246
|
+
exportSession.shouldOptimizeForNetworkUse = YES;
|
|
2247
|
+
|
|
2248
|
+
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
|
2249
|
+
if (exportSession.status == AVAssetExportSessionStatusCompleted) {
|
|
2250
|
+
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:outPath error:nil];
|
|
2251
|
+
CGSize natSize = CGSizeApplyAffineTransform(videoTrack.naturalSize, videoTrack.preferredTransform);
|
|
2252
|
+
resolve(@{
|
|
2253
|
+
@"uri": outUrl.absoluteString,
|
|
2254
|
+
@"durationMs": @(endMs - startMs),
|
|
2255
|
+
@"size": @([attrs fileSize]),
|
|
2256
|
+
@"width": @(fabs(natSize.width)),
|
|
2257
|
+
@"height": @(fabs(natSize.height)),
|
|
2258
|
+
@"format": @"mp4",
|
|
2259
|
+
});
|
|
2260
|
+
} else {
|
|
2261
|
+
reject(@"EXPORT_FAILED", exportSession.error.localizedDescription ?: @"Video export failed", exportSession.error);
|
|
2262
|
+
}
|
|
2263
|
+
}];
|
|
2264
|
+
} @catch (NSException *ex) {
|
|
2265
|
+
reject(@"TRIM_FAILED", ex.reason ?: @"Video trim failed", nil);
|
|
2266
|
+
}
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2271
|
+
// 100% Native Media Editor: Filmstrip Thumbnail Generator (AVAssetImageGenerator)
|
|
2272
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2273
|
+
|
|
2274
|
+
RCT_EXPORT_METHOD(generateFilmstrip:(NSDictionary *)options
|
|
2275
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
2276
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
2277
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
2278
|
+
@try {
|
|
2279
|
+
NSString *rawUri = options[@"uri"];
|
|
2280
|
+
if (!rawUri || rawUri.length == 0) {
|
|
2281
|
+
reject(@"INVALID_URI", @"Video URI is missing", nil);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
NSURL *sourceUrl = [NSURL URLWithString:rawUri];
|
|
2286
|
+
if (!sourceUrl.scheme) {
|
|
2287
|
+
sourceUrl = [NSURL fileURLWithPath:rawUri];
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:sourceUrl options:nil];
|
|
2291
|
+
NSInteger count = options[@"count"] ? [options[@"count"] integerValue] : 10;
|
|
2292
|
+
if (count <= 0) count = 10;
|
|
2293
|
+
if (count > 50) count = 50;
|
|
2294
|
+
|
|
2295
|
+
CGFloat targetWidth = options[@"maxWidth"] ? [options[@"maxWidth"] doubleValue] : (options[@"targetWidth"] ? [options[@"targetWidth"] doubleValue] : 120);
|
|
2296
|
+
CGFloat targetHeight = options[@"maxHeight"] ? [options[@"maxHeight"] doubleValue] : (options[@"targetHeight"] ? [options[@"targetHeight"] doubleValue] : 120);
|
|
2297
|
+
CGFloat quality = options[@"quality"] ? [options[@"quality"] doubleValue] : 0.7;
|
|
2298
|
+
|
|
2299
|
+
double durationSeconds = CMTimeGetSeconds(asset.duration);
|
|
2300
|
+
if (durationSeconds <= 0) {
|
|
2301
|
+
reject(@"INVALID_DURATION", @"Unable to determine video duration", nil);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
|
|
2306
|
+
generator.appliesPreferredTrackTransform = YES;
|
|
2307
|
+
generator.maximumSize = CGSizeMake(targetWidth, targetHeight);
|
|
2308
|
+
generator.requestedTimeToleranceBefore = kCMTimeZero;
|
|
2309
|
+
generator.requestedTimeToleranceAfter = kCMTimeZero;
|
|
2310
|
+
|
|
2311
|
+
NSMutableArray *thumbnails = [NSMutableArray arrayWithCapacity:count];
|
|
2312
|
+
double step = durationSeconds / (double)count;
|
|
2313
|
+
|
|
2314
|
+
for (NSInteger i = 0; i < count; i++) {
|
|
2315
|
+
double targetSec = i * step;
|
|
2316
|
+
CMTime time = CMTimeMakeWithSeconds(targetSec, 600);
|
|
2317
|
+
NSError *genErr = nil;
|
|
2318
|
+
CGImageRef cgImage = [generator copyCGImageAtTime:time actualTime:NULL error:&genErr];
|
|
2319
|
+
if (cgImage) {
|
|
2320
|
+
UIImage *img = [UIImage imageWithCGImage:cgImage];
|
|
2321
|
+
CGImageRelease(cgImage);
|
|
2322
|
+
NSData *data = UIImageJPEGRepresentation(img, quality);
|
|
2323
|
+
NSString *b64 = [data base64EncodedStringWithOptions:0];
|
|
2324
|
+
NSString *dataUrl = [NSString stringWithFormat:@"data:image/jpeg;base64,%@", b64];
|
|
2325
|
+
[thumbnails addObject:@{
|
|
2326
|
+
@"timeMs": @(targetSec * 1000.0),
|
|
2327
|
+
@"uri": dataUrl,
|
|
2328
|
+
@"index": @(i),
|
|
2329
|
+
}];
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
resolve(@{
|
|
2334
|
+
@"thumbnails": thumbnails,
|
|
2335
|
+
@"durationMs": @(durationSeconds * 1000.0),
|
|
2336
|
+
});
|
|
2337
|
+
} @catch (NSException *ex) {
|
|
2338
|
+
reject(@"FILMSTRIP_FAILED", ex.reason ?: @"Filmstrip generation failed", nil);
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
|
|
1824
2343
|
|
|
1825
2344
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
1826
2345
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|