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.
Files changed (295) hide show
  1. package/README.ar.md +163 -0
  2. package/README.de.md +163 -0
  3. package/README.es.md +164 -0
  4. package/README.fr.md +163 -0
  5. package/README.hi.md +163 -0
  6. package/README.ja.md +180 -0
  7. package/README.ko.md +163 -0
  8. package/README.md +340 -243
  9. package/README.pt-BR.md +163 -0
  10. package/README.ru.md +163 -0
  11. package/README.zh-CN.md +387 -0
  12. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +605 -23
  13. package/assets/banner_dark.svg +186 -0
  14. package/assets/banner_light.svg +186 -0
  15. package/assets/icon_circle.svg +148 -0
  16. package/assets/icon_square.svg +369 -0
  17. package/assets/inapp_inspector_icon.gif +0 -0
  18. package/assets/inapp_inspector_icon_original.gif +0 -0
  19. package/assets/social_preview.png +0 -0
  20. package/assets/walkthrough.gif +0 -0
  21. package/dist/commonjs/capture.d.ts +2 -0
  22. package/dist/commonjs/capture.js +1 -1
  23. package/dist/commonjs/components/AnalyticsDetail.js +23 -23
  24. package/dist/commonjs/components/AnalyticsEventCard.js +1 -1
  25. package/dist/commonjs/components/AnalyticsGraph.js +44 -44
  26. package/dist/commonjs/components/AnimatedBrandOwl.d.ts +7 -0
  27. package/dist/commonjs/components/AnimatedBrandOwl.js +175 -0
  28. package/dist/commonjs/components/AppHeaderLogo.d.ts +1 -0
  29. package/dist/commonjs/components/AppHeaderLogo.js +5 -3
  30. package/dist/commonjs/components/BrandCircleIcon.js +28 -28
  31. package/dist/commonjs/components/BrandSquareIcon.d.ts +5 -0
  32. package/dist/commonjs/components/BrandSquareIcon.js +139 -0
  33. package/dist/commonjs/components/ConsoleLogCard.d.ts +2 -1
  34. package/dist/commonjs/components/ConsoleLogCard.js +23 -39
  35. package/dist/commonjs/components/Inspector/AboutModal.js +44 -54
  36. package/dist/commonjs/components/Inspector/AnalyticsTab.js +6 -6
  37. package/dist/commonjs/components/Inspector/ConfirmationModal.d.ts +13 -0
  38. package/dist/commonjs/components/Inspector/ConfirmationModal.js +30 -0
  39. package/dist/commonjs/components/Inspector/ConsoleTab.js +12 -12
  40. package/dist/commonjs/components/Inspector/CrashTab.js +44 -9
  41. package/dist/commonjs/components/Inspector/DebuggingTab.js +17 -17
  42. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +1 -1
  43. package/dist/commonjs/components/Inspector/FabLauncher.js +30 -33
  44. package/dist/commonjs/components/Inspector/FeatureUnderDevNotice.js +2 -2
  45. package/dist/commonjs/components/Inspector/FloatingCaptureWidget.d.ts +3 -0
  46. package/dist/commonjs/components/Inspector/FloatingCaptureWidget.js +54 -0
  47. package/dist/commonjs/components/Inspector/GlobalCrashScreenModal.d.ts +11 -0
  48. package/dist/commonjs/components/Inspector/GlobalCrashScreenModal.js +234 -0
  49. package/dist/commonjs/components/Inspector/InspectorHeader.js +137 -127
  50. package/dist/commonjs/components/Inspector/LanguageSelectorModal.d.ts +15 -0
  51. package/dist/commonjs/components/Inspector/LanguageSelectorModal.js +172 -0
  52. package/dist/commonjs/components/Inspector/MainScreen.js +55 -35
  53. package/dist/commonjs/components/Inspector/MediaGalleryTab.js +40 -35
  54. package/dist/commonjs/components/Inspector/MediaPreviewModal.d.ts +36 -0
  55. package/dist/commonjs/components/Inspector/MediaPreviewModal.js +341 -135
  56. package/dist/commonjs/components/Inspector/NetworkTab.js +50 -50
  57. package/dist/commonjs/components/Inspector/NpmUpdateToast.js +1 -1
  58. package/dist/commonjs/components/Inspector/PushCard.d.ts +3 -2
  59. package/dist/commonjs/components/Inspector/PushCard.js +5 -4
  60. package/dist/commonjs/components/Inspector/PushDetail.js +41 -32
  61. package/dist/commonjs/components/Inspector/PushTab.js +12 -12
  62. package/dist/commonjs/components/Inspector/ReduxDetail.js +32 -32
  63. package/dist/commonjs/components/Inspector/ReduxTab.js +10 -8
  64. package/dist/commonjs/components/Inspector/SecondaryTelemetryStrip.d.ts +2 -0
  65. package/dist/commonjs/components/Inspector/SecondaryTelemetryStrip.js +43 -0
  66. package/dist/commonjs/components/Inspector/SettingsPanel.js +287 -139
  67. package/dist/commonjs/components/Inspector/SocketCard.d.ts +5 -0
  68. package/dist/commonjs/components/Inspector/SocketCard.js +116 -0
  69. package/dist/commonjs/components/Inspector/SocketDetail.d.ts +4 -0
  70. package/dist/commonjs/components/Inspector/SocketDetail.js +327 -0
  71. package/dist/commonjs/components/Inspector/SocketFilterModal.d.ts +13 -0
  72. package/dist/commonjs/components/Inspector/SocketFilterModal.js +69 -0
  73. package/dist/commonjs/components/Inspector/SocketTab.d.ts +3 -0
  74. package/dist/commonjs/components/Inspector/SocketTab.js +112 -0
  75. package/dist/commonjs/components/Inspector/StorageTab.js +39 -37
  76. package/dist/commonjs/components/Inspector/SupportModal.d.ts +7 -0
  77. package/dist/commonjs/components/Inspector/SupportModal.js +125 -0
  78. package/dist/commonjs/components/Inspector/SupportPage.d.ts +5 -0
  79. package/dist/commonjs/components/Inspector/SupportPage.js +227 -0
  80. package/dist/commonjs/components/Inspector/TabBar.js +24 -23
  81. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +24 -24
  82. package/dist/commonjs/components/JsonViewer.js +50 -48
  83. package/dist/commonjs/components/LogCard.d.ts +3 -2
  84. package/dist/commonjs/components/LogCard.js +27 -25
  85. package/dist/commonjs/components/LogSyntaxHighlighter.js +5 -5
  86. package/dist/commonjs/components/ModuleErrorBoundary.js +5 -5
  87. package/dist/commonjs/components/NetworkIcons.d.ts +42 -0
  88. package/dist/commonjs/components/NetworkIcons.js +139 -6
  89. package/dist/commonjs/components/SegmentedTabs.js +1 -1
  90. package/dist/commonjs/constants/version.d.ts +1 -1
  91. package/dist/commonjs/constants/version.js +1 -1
  92. package/dist/commonjs/crash.d.ts +1 -1
  93. package/dist/commonjs/crash.js +1 -1
  94. package/dist/commonjs/customHooks/crashHandler.d.ts +2 -1
  95. package/dist/commonjs/customHooks/crashHandler.js +7 -7
  96. package/dist/commonjs/customHooks/socketLogger.d.ts +26 -0
  97. package/dist/commonjs/customHooks/socketLogger.js +2 -0
  98. package/dist/commonjs/editor.d.ts +13 -0
  99. package/dist/commonjs/editor.js +1 -0
  100. package/dist/commonjs/helpers/index.d.ts +1 -0
  101. package/dist/commonjs/helpers/index.js +3 -3
  102. package/dist/commonjs/helpers/memoryManager.d.ts +1 -0
  103. package/dist/commonjs/helpers/memoryManager.js +1 -1
  104. package/dist/commonjs/helpers/searchQueryParser.d.ts +1 -1
  105. package/dist/commonjs/helpers/searchQueryParser.js +5 -1
  106. package/dist/commonjs/helpers/settingsStore.d.ts +1 -0
  107. package/dist/commonjs/helpers/settingsStore.js +1 -1
  108. package/dist/commonjs/helpers/shareFormatter.d.ts +3 -1
  109. package/dist/commonjs/helpers/shareFormatter.js +8 -7
  110. package/dist/commonjs/helpers/telemetry.js +1 -1
  111. package/dist/commonjs/i18n/index.d.ts +99 -1
  112. package/dist/commonjs/i18n/index.js +1 -1
  113. package/dist/commonjs/i18n/locales/ar.json +1 -1
  114. package/dist/commonjs/i18n/locales/bn.json +1 -0
  115. package/dist/commonjs/i18n/locales/de.json +1 -1
  116. package/dist/commonjs/i18n/locales/en.json +1 -1
  117. package/dist/commonjs/i18n/locales/es.json +1 -1
  118. package/dist/commonjs/i18n/locales/fr.json +1 -1
  119. package/dist/commonjs/i18n/locales/gu.json +1 -0
  120. package/dist/commonjs/i18n/locales/hi.json +1 -1
  121. package/dist/commonjs/i18n/locales/id.json +1 -0
  122. package/dist/commonjs/i18n/locales/it.json +1 -0
  123. package/dist/commonjs/i18n/locales/ja.json +1 -1
  124. package/dist/commonjs/i18n/locales/kn.json +1 -0
  125. package/dist/commonjs/i18n/locales/ko.json +1 -1
  126. package/dist/commonjs/i18n/locales/ml.json +1 -0
  127. package/dist/commonjs/i18n/locales/mr.json +1 -0
  128. package/dist/commonjs/i18n/locales/nl.json +1 -0
  129. package/dist/commonjs/i18n/locales/pa.json +1 -0
  130. package/dist/commonjs/i18n/locales/pl.json +1 -0
  131. package/dist/commonjs/i18n/locales/pt.json +1 -1
  132. package/dist/commonjs/i18n/locales/ru.json +1 -1
  133. package/dist/commonjs/i18n/locales/ta.json +1 -0
  134. package/dist/commonjs/i18n/locales/te.json +1 -0
  135. package/dist/commonjs/i18n/locales/tr.json +1 -0
  136. package/dist/commonjs/i18n/locales/vi.json +1 -0
  137. package/dist/commonjs/i18n/locales/zh.json +1 -1
  138. package/dist/commonjs/index.d.ts +16 -13
  139. package/dist/commonjs/index.js +5 -4
  140. package/dist/commonjs/native/NativeInspector.d.ts +4 -0
  141. package/dist/commonjs/native/NativeInspector.js +1 -1
  142. package/dist/commonjs/native/NativeNetworkInspector.d.ts +4 -0
  143. package/dist/commonjs/socket.d.ts +4 -0
  144. package/dist/commonjs/socket.js +1 -0
  145. package/dist/commonjs/styles/AppColors.d.ts +42 -2
  146. package/dist/commonjs/styles/AppColors.js +1 -1
  147. package/dist/commonjs/styles/common.js +1 -1
  148. package/dist/commonjs/styles/index.d.ts +30 -0
  149. package/dist/commonjs/styles/index.js +1 -1
  150. package/dist/commonjs/types/editor.d.ts +96 -0
  151. package/dist/commonjs/types/editor.js +1 -0
  152. package/dist/commonjs/types/enums.d.ts +40 -0
  153. package/dist/commonjs/types/enums.js +1 -1
  154. package/dist/commonjs/types/interfaces.d.ts +143 -1
  155. package/dist/esm/capture.d.ts +2 -0
  156. package/dist/esm/capture.js +1 -1
  157. package/dist/esm/components/AnalyticsDetail.js +31 -31
  158. package/dist/esm/components/AnalyticsEventCard.js +1 -1
  159. package/dist/esm/components/AnalyticsGraph.js +74 -74
  160. package/dist/esm/components/AnimatedBrandOwl.d.ts +7 -0
  161. package/dist/esm/components/AnimatedBrandOwl.js +175 -0
  162. package/dist/esm/components/AppHeaderLogo.d.ts +1 -0
  163. package/dist/esm/components/AppHeaderLogo.js +7 -5
  164. package/dist/esm/components/BrandCircleIcon.js +70 -70
  165. package/dist/esm/components/BrandSquareIcon.d.ts +5 -0
  166. package/dist/esm/components/BrandSquareIcon.js +139 -0
  167. package/dist/esm/components/ConsoleLogCard.d.ts +2 -1
  168. package/dist/esm/components/ConsoleLogCard.js +61 -77
  169. package/dist/esm/components/Inspector/AboutModal.js +85 -95
  170. package/dist/esm/components/Inspector/AnalyticsTab.js +24 -24
  171. package/dist/esm/components/Inspector/ConfirmationModal.d.ts +13 -0
  172. package/dist/esm/components/Inspector/ConfirmationModal.js +30 -0
  173. package/dist/esm/components/Inspector/ConsoleTab.js +48 -48
  174. package/dist/esm/components/Inspector/CrashTab.js +203 -168
  175. package/dist/esm/components/Inspector/DebuggingTab.js +29 -29
  176. package/dist/esm/components/Inspector/DeviceInfoTab.js +75 -75
  177. package/dist/esm/components/Inspector/FabLauncher.js +57 -60
  178. package/dist/esm/components/Inspector/FeatureUnderDevNotice.js +7 -7
  179. package/dist/esm/components/Inspector/FloatingCaptureWidget.d.ts +3 -0
  180. package/dist/esm/components/Inspector/FloatingCaptureWidget.js +54 -0
  181. package/dist/esm/components/Inspector/GlobalCrashScreenModal.d.ts +11 -0
  182. package/dist/esm/components/Inspector/GlobalCrashScreenModal.js +234 -0
  183. package/dist/esm/components/Inspector/InspectorHeader.js +162 -152
  184. package/dist/esm/components/Inspector/LanguageSelectorModal.d.ts +15 -0
  185. package/dist/esm/components/Inspector/LanguageSelectorModal.js +172 -0
  186. package/dist/esm/components/Inspector/MainScreen.js +130 -110
  187. package/dist/esm/components/Inspector/MediaGalleryTab.js +83 -78
  188. package/dist/esm/components/Inspector/MediaPreviewModal.d.ts +36 -0
  189. package/dist/esm/components/Inspector/MediaPreviewModal.js +345 -139
  190. package/dist/esm/components/Inspector/NetworkTab.js +130 -130
  191. package/dist/esm/components/Inspector/NpmUpdateToast.js +1 -1
  192. package/dist/esm/components/Inspector/PushCard.d.ts +3 -2
  193. package/dist/esm/components/Inspector/PushCard.js +23 -22
  194. package/dist/esm/components/Inspector/PushDetail.js +45 -36
  195. package/dist/esm/components/Inspector/PushTab.js +23 -23
  196. package/dist/esm/components/Inspector/ReduxDetail.js +42 -42
  197. package/dist/esm/components/Inspector/ReduxTab.js +20 -18
  198. package/dist/esm/components/Inspector/SecondaryTelemetryStrip.d.ts +2 -0
  199. package/dist/esm/components/Inspector/SecondaryTelemetryStrip.js +43 -0
  200. package/dist/esm/components/Inspector/SettingsPanel.js +526 -378
  201. package/dist/esm/components/Inspector/SocketCard.d.ts +5 -0
  202. package/dist/esm/components/Inspector/SocketCard.js +116 -0
  203. package/dist/esm/components/Inspector/SocketDetail.d.ts +4 -0
  204. package/dist/esm/components/Inspector/SocketDetail.js +327 -0
  205. package/dist/esm/components/Inspector/SocketFilterModal.d.ts +13 -0
  206. package/dist/esm/components/Inspector/SocketFilterModal.js +69 -0
  207. package/dist/esm/components/Inspector/SocketTab.d.ts +3 -0
  208. package/dist/esm/components/Inspector/SocketTab.js +112 -0
  209. package/dist/esm/components/Inspector/StorageTab.js +139 -137
  210. package/dist/esm/components/Inspector/SupportModal.d.ts +7 -0
  211. package/dist/esm/components/Inspector/SupportModal.js +125 -0
  212. package/dist/esm/components/Inspector/SupportPage.d.ts +5 -0
  213. package/dist/esm/components/Inspector/SupportPage.js +227 -0
  214. package/dist/esm/components/Inspector/TabBar.js +36 -35
  215. package/dist/esm/components/Inspector/UpdateAvailableModal.js +22 -22
  216. package/dist/esm/components/JsonViewer.js +66 -64
  217. package/dist/esm/components/LogCard.d.ts +3 -2
  218. package/dist/esm/components/LogCard.js +57 -55
  219. package/dist/esm/components/LogSyntaxHighlighter.js +5 -5
  220. package/dist/esm/components/ModuleErrorBoundary.js +23 -23
  221. package/dist/esm/components/NetworkIcons.d.ts +42 -0
  222. package/dist/esm/components/NetworkIcons.js +139 -6
  223. package/dist/esm/components/SegmentedTabs.js +7 -7
  224. package/dist/esm/constants/version.d.ts +1 -1
  225. package/dist/esm/constants/version.js +1 -1
  226. package/dist/esm/crash.d.ts +1 -1
  227. package/dist/esm/crash.js +1 -1
  228. package/dist/esm/customHooks/crashHandler.d.ts +2 -1
  229. package/dist/esm/customHooks/crashHandler.js +7 -7
  230. package/dist/esm/customHooks/socketLogger.d.ts +26 -0
  231. package/dist/esm/customHooks/socketLogger.js +2 -0
  232. package/dist/esm/editor.d.ts +13 -0
  233. package/dist/esm/editor.js +1 -0
  234. package/dist/esm/helpers/index.d.ts +1 -0
  235. package/dist/esm/helpers/index.js +4 -4
  236. package/dist/esm/helpers/memoryManager.d.ts +1 -0
  237. package/dist/esm/helpers/memoryManager.js +1 -1
  238. package/dist/esm/helpers/searchQueryParser.d.ts +1 -1
  239. package/dist/esm/helpers/searchQueryParser.js +5 -1
  240. package/dist/esm/helpers/settingsStore.d.ts +1 -0
  241. package/dist/esm/helpers/settingsStore.js +1 -1
  242. package/dist/esm/helpers/shareFormatter.d.ts +3 -1
  243. package/dist/esm/helpers/shareFormatter.js +8 -7
  244. package/dist/esm/helpers/telemetry.js +1 -1
  245. package/dist/esm/i18n/index.d.ts +99 -1
  246. package/dist/esm/i18n/index.js +1 -1
  247. package/dist/esm/i18n/locales/ar.json +1 -1
  248. package/dist/esm/i18n/locales/bn.json +1 -0
  249. package/dist/esm/i18n/locales/de.json +1 -1
  250. package/dist/esm/i18n/locales/en.json +1 -1
  251. package/dist/esm/i18n/locales/es.json +1 -1
  252. package/dist/esm/i18n/locales/fr.json +1 -1
  253. package/dist/esm/i18n/locales/gu.json +1 -0
  254. package/dist/esm/i18n/locales/hi.json +1 -1
  255. package/dist/esm/i18n/locales/id.json +1 -0
  256. package/dist/esm/i18n/locales/it.json +1 -0
  257. package/dist/esm/i18n/locales/ja.json +1 -1
  258. package/dist/esm/i18n/locales/kn.json +1 -0
  259. package/dist/esm/i18n/locales/ko.json +1 -1
  260. package/dist/esm/i18n/locales/ml.json +1 -0
  261. package/dist/esm/i18n/locales/mr.json +1 -0
  262. package/dist/esm/i18n/locales/nl.json +1 -0
  263. package/dist/esm/i18n/locales/pa.json +1 -0
  264. package/dist/esm/i18n/locales/pl.json +1 -0
  265. package/dist/esm/i18n/locales/pt.json +1 -1
  266. package/dist/esm/i18n/locales/ru.json +1 -1
  267. package/dist/esm/i18n/locales/ta.json +1 -0
  268. package/dist/esm/i18n/locales/te.json +1 -0
  269. package/dist/esm/i18n/locales/tr.json +1 -0
  270. package/dist/esm/i18n/locales/vi.json +1 -0
  271. package/dist/esm/i18n/locales/zh.json +1 -1
  272. package/dist/esm/index.d.ts +16 -13
  273. package/dist/esm/index.js +8 -7
  274. package/dist/esm/native/NativeInspector.d.ts +4 -0
  275. package/dist/esm/native/NativeInspector.js +1 -1
  276. package/dist/esm/native/NativeNetworkInspector.d.ts +4 -0
  277. package/dist/esm/socket.d.ts +4 -0
  278. package/dist/esm/socket.js +1 -0
  279. package/dist/esm/styles/AppColors.d.ts +42 -2
  280. package/dist/esm/styles/AppColors.js +1 -1
  281. package/dist/esm/styles/common.js +1 -1
  282. package/dist/esm/styles/index.d.ts +30 -0
  283. package/dist/esm/styles/index.js +1 -1
  284. package/dist/esm/types/editor.d.ts +96 -0
  285. package/dist/esm/types/editor.js +1 -0
  286. package/dist/esm/types/enums.d.ts +40 -0
  287. package/dist/esm/types/enums.js +1 -1
  288. package/dist/esm/types/interfaces.d.ts +143 -1
  289. package/editor.d.ts +1 -0
  290. package/editor.js +1 -0
  291. package/ios/NetworkInspectorModule.mm +531 -12
  292. package/package.json +213 -2
  293. package/react-native-inapp-inspector.podspec +1 -1
  294. package/src/native/NativeInspector.ts +65 -4
  295. 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 > 30.0) fps = 15.0;
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
- dispatch_sync(dispatch_get_main_queue(), ^{
1344
- if (!strongSelf || !strongSelf->_isSoftwareRecordingActive) return;
1345
- if (!strongSelf->_softwareAssetWriter || strongSelf->_softwareAssetWriter.status != AVAssetWriterStatusWriting) return;
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
- if (frameImg && strongSelf->_softwareWriterInput.isReadyForMoreMediaData) {
1349
- CVPixelBufferRef buffer = [strongSelf createPixelBufferFromUIImage:frameImg size:videoSize];
1350
- if (buffer) {
1351
- CMTime presentTime = CMTimeMake((int64_t)(strongSelf->_softwareFrameIndex * 1000 / fps), 1000);
1352
- [strongSelf->_softwarePixelBufferAdaptor appendPixelBuffer:buffer withPresentationTime:presentTime];
1353
- CVPixelBufferRelease(buffer);
1354
- strongSelf->_softwareFrameIndex++;
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: