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
@@ -14,6 +14,7 @@ import android.os.Build
14
14
  import android.os.Debug
15
15
  import android.os.Environment
16
16
  import android.os.Handler
17
+ import android.os.HandlerThread
17
18
  import android.os.Looper
18
19
  import android.os.StatFs
19
20
  import android.util.Base64
@@ -37,6 +38,7 @@ import java.io.StringWriter
37
38
  import java.util.Timer
38
39
  import java.util.TimerTask
39
40
  import java.util.concurrent.Executors
41
+ import java.util.concurrent.atomic.AtomicBoolean
40
42
 
41
43
 
42
44
  class NetworkInspectorModule(private val reactContext: ReactApplicationContext) :
@@ -60,6 +62,9 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
60
62
  private var recordingTimer: Timer? = null
61
63
  private val recordedFrames = mutableListOf<Bitmap>()
62
64
  private var recordingFps = 15
65
+ private val isFrameCapturing = AtomicBoolean(false)
66
+ private var captureHandlerThread: HandlerThread? = null
67
+ private var captureBackgroundHandler: Handler? = null
63
68
 
64
69
  private var defaultHandler: Thread.UncaughtExceptionHandler? = null
65
70
  private var isProtectionEnabled = false
@@ -69,6 +74,34 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
69
74
  return MODULE_NAME
70
75
  }
71
76
 
77
+ override fun getConstants(): MutableMap<String, Any> {
78
+ val constants = HashMap<String, Any>()
79
+ try {
80
+ val packageManager = reactContext.packageManager
81
+ val packageName = reactContext.packageName
82
+ val packageInfo = packageManager.getPackageInfo(packageName, 0)
83
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
84
+ val appName = packageManager.getApplicationLabel(appInfo).toString()
85
+ val appVersion = packageInfo.versionName ?: "1.0"
86
+ val appBuild = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
87
+ packageInfo.longVersionCode.toString()
88
+ } else {
89
+ @Suppress("DEPRECATION")
90
+ packageInfo.versionCode.toString()
91
+ }
92
+ constants["appName"] = appName
93
+ constants["appVersion"] = appVersion
94
+ constants["appBuild"] = appBuild
95
+ constants["appPackageName"] = packageName
96
+ constants["appBundleId"] = packageName
97
+ } catch (e: Exception) {
98
+ constants["appName"] = "App"
99
+ constants["appVersion"] = "1.0"
100
+ constants["appBuild"] = "1"
101
+ }
102
+ return constants
103
+ }
104
+
72
105
  private fun setupNativeCrashProtection() {
73
106
  if (isProtectionEnabled) return
74
107
 
@@ -715,7 +748,8 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
715
748
  return
716
749
  }
717
750
 
718
- Handler(Looper.getMainLooper()).post {
751
+ val mainHandler = Handler(Looper.getMainLooper())
752
+ mainHandler.post {
719
753
  try {
720
754
  val decorView = targetWindow.decorView
721
755
  val metrics = reactContext.resources.displayMetrics
@@ -725,22 +759,19 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
725
759
  val targetHeight = (origHeight * scale).toInt().coerceAtLeast(1)
726
760
 
727
761
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
728
- val rawBitmap = Bitmap.createBitmap(origWidth, origHeight, Bitmap.Config.ARGB_8888)
762
+ val destBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888)
763
+ val copyHandler = captureBackgroundHandler ?: mainHandler
729
764
  try {
730
- PixelCopy.request(targetWindow, null, rawBitmap, { copyResult ->
765
+ PixelCopy.request(targetWindow, null, destBitmap, { copyResult ->
731
766
  if (copyResult == PixelCopy.SUCCESS) {
732
- if (scale != 1.0f) {
733
- val scaled = Bitmap.createScaledBitmap(rawBitmap, targetWidth, targetHeight, true)
734
- if (scaled != rawBitmap) rawBitmap.recycle()
735
- callback(scaled)
736
- } else {
737
- callback(rawBitmap)
738
- }
767
+ callback(destBitmap)
739
768
  } else {
769
+ destBitmap.recycle()
740
770
  fallbackCanvasCapture(decorView, targetWidth, targetHeight, scale, callback)
741
771
  }
742
- }, Handler(Looper.getMainLooper()))
772
+ }, copyHandler)
743
773
  } catch (e: Exception) {
774
+ destBitmap.recycle()
744
775
  fallbackCanvasCapture(decorView, targetWidth, targetHeight, scale, callback)
745
776
  }
746
777
  } else {
@@ -864,12 +895,23 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
864
895
  recordedFrames.clear()
865
896
  }
866
897
 
867
- val fps = if (options.hasKey("fps")) options.getInt("fps").coerceIn(5, 30) else 15
898
+ val fps = if (options.hasKey("fps")) options.getInt("fps").coerceIn(5, 60) else 30
899
+ val scale = if (options.hasKey("scale")) options.getDouble("scale").toFloat().coerceIn(0.2f, 1.0f) else 0.5f
900
+ val maxDuration = if (options.hasKey("maxDurationSeconds")) options.getInt("maxDurationSeconds").coerceIn(5, 300) else 120
868
901
  recordingFps = fps
869
- val intervalMs = (1000L / fps).coerceAtLeast(33L)
902
+ val intervalMs = (1000L / fps).coerceAtLeast(16L)
903
+ val maxFrames = (fps * maxDuration).coerceAtMost(3600)
870
904
  isRecordingVideo = true
905
+ isFrameCapturing.set(false)
871
906
  recordingStartTime = System.currentTimeMillis()
872
907
 
908
+ if (captureHandlerThread == null) {
909
+ val ht = HandlerThread("InAppInspector-PixelCopy")
910
+ ht.start()
911
+ captureHandlerThread = ht
912
+ captureBackgroundHandler = Handler(ht.looper)
913
+ }
914
+
873
915
  recordingTimer = Timer("InAppInspector-Recorder", true).apply {
874
916
  scheduleAtFixedRate(object : TimerTask() {
875
917
  override fun run() {
@@ -877,16 +919,34 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
877
919
  cancel()
878
920
  return
879
921
  }
880
- val curWindow = getActiveWindow() ?: return
881
- captureWindowBitmap(curWindow, 0.5f) { frame ->
882
- if (frame != null && isRecordingVideo) {
883
- synchronized(recordedFrames) {
884
- if (recordedFrames.size < 300) { // Keep memory safe (max 20s buffer)
885
- recordedFrames.add(frame)
886
- } else {
887
- frame.recycle()
922
+ if (!isFrameCapturing.compareAndSet(false, true)) {
923
+ // Previous frame still copying or processing; drop tick to guarantee smooth scrolling
924
+ return
925
+ }
926
+ // Auto-stop if max duration exceeded
927
+ val elapsed = System.currentTimeMillis() - recordingStartTime
928
+ if (elapsed > maxDuration * 1000L) {
929
+ isFrameCapturing.set(false)
930
+ return
931
+ }
932
+ val curWindow = getActiveWindow()
933
+ if (curWindow == null) {
934
+ isFrameCapturing.set(false)
935
+ return
936
+ }
937
+ captureWindowBitmap(curWindow, scale) { frame ->
938
+ try {
939
+ if (frame != null && isRecordingVideo) {
940
+ synchronized(recordedFrames) {
941
+ if (recordedFrames.size < maxFrames) {
942
+ recordedFrames.add(frame)
943
+ } else {
944
+ frame.recycle()
945
+ }
888
946
  }
889
947
  }
948
+ } finally {
949
+ isFrameCapturing.set(false)
890
950
  }
891
951
  }
892
952
  }
@@ -906,6 +966,7 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
906
966
  isRecordingVideo = false
907
967
  recordingTimer?.cancel()
908
968
  recordingTimer = null
969
+ isFrameCapturing.set(false)
909
970
 
910
971
  val durationMs = (System.currentTimeMillis() - recordingStartTime).coerceAtLeast(500L)
911
972
  val timestamp = System.currentTimeMillis()
@@ -1198,14 +1259,535 @@ class NetworkInspectorModule(private val reactContext: ReactApplicationContext)
1198
1259
  promise.resolve(map)
1199
1260
  }
1200
1261
  }
1262
+ // ─────────────────────────────────────────────────────────────────────────
1263
+ // 100% Native Media Editor: Photo Editing (Bitmap + ColorMatrix + Canvas)
1264
+ // ─────────────────────────────────────────────────────────────────────────
1265
+
1266
+ @ReactMethod
1267
+ fun editPhoto(options: ReadableMap, promise: Promise) {
1268
+ captureExecutor.execute {
1269
+ try {
1270
+ val rawUri = if (options.hasKey("uri")) options.getString("uri") else null
1271
+ if (rawUri.isNullOrEmpty()) {
1272
+ promise.reject("INVALID_URI", "Image URI is missing or empty")
1273
+ return@execute
1274
+ }
1275
+
1276
+ val cleanPath = when {
1277
+ rawUri.startsWith("file://") -> Uri.parse(rawUri).path ?: rawUri.substring(7)
1278
+ else -> rawUri
1279
+ }
1280
+
1281
+ val sourceBitmap = if (cleanPath.startsWith("content://")) {
1282
+ val input = reactContext.contentResolver.openInputStream(Uri.parse(cleanPath))
1283
+ android.graphics.BitmapFactory.decodeStream(input)
1284
+ } else {
1285
+ android.graphics.BitmapFactory.decodeFile(cleanPath)
1286
+ }
1287
+
1288
+ if (sourceBitmap == null) {
1289
+ promise.reject("IMAGE_LOAD_FAILED", "Failed to decode source image")
1290
+ return@execute
1291
+ }
1292
+
1293
+ // 1. Compute Matrix for Rotation and Flip
1294
+ val matrix = Matrix()
1295
+ val rotation = if (options.hasKey("rotation")) options.getInt("rotation") else 0
1296
+ val flipH = if (options.hasKey("flipHorizontal")) options.getBoolean("flipHorizontal") else false
1297
+ val flipV = if (options.hasKey("flipVertical")) options.getBoolean("flipVertical") else false
1298
+
1299
+ if (rotation != 0) {
1300
+ matrix.postRotate(rotation.toFloat())
1301
+ }
1302
+ val scaleX = if (flipH) -1f else 1f
1303
+ val scaleY = if (flipV) -1f else 1f
1304
+ if (flipH || flipV) {
1305
+ matrix.postScale(scaleX, scaleY)
1306
+ }
1307
+
1308
+ // 2. Compute Crop Bounds
1309
+ var cropX = 0
1310
+ var cropY = 0
1311
+ var cropW = sourceBitmap.width
1312
+ var cropH = sourceBitmap.height
1313
+
1314
+ if (options.hasKey("crop")) {
1315
+ val cropMap = options.getMap("crop")
1316
+ if (cropMap != null) {
1317
+ val isNorm = if (cropMap.hasKey("isNormalized")) cropMap.getBoolean("isNormalized") else false
1318
+ var x = if (cropMap.hasKey("x")) cropMap.getDouble("x").toFloat() else 0f
1319
+ var y = if (cropMap.hasKey("y")) cropMap.getDouble("y").toFloat() else 0f
1320
+ var w = if (cropMap.hasKey("width")) cropMap.getDouble("width").toFloat() else cropW.toFloat()
1321
+ var h = if (cropMap.hasKey("height")) cropMap.getDouble("height").toFloat() else cropH.toFloat()
1322
+
1323
+ if (isNorm) {
1324
+ x *= cropW
1325
+ y *= cropH
1326
+ w *= cropW
1327
+ h *= cropH
1328
+ }
1329
+
1330
+ cropX = x.toInt().coerceIn(0, cropW - 1)
1331
+ cropY = y.toInt().coerceIn(0, cropH - 1)
1332
+ cropW = w.toInt().coerceIn(1, sourceBitmap.width - cropX)
1333
+ cropH = h.toInt().coerceIn(1, sourceBitmap.height - cropY)
1334
+ }
1335
+ }
1336
+
1337
+ val croppedBitmap = Bitmap.createBitmap(sourceBitmap, cropX, cropY, cropW, cropH, matrix, true)
1338
+
1339
+ // 3. Color Grading & Adjustments via ColorMatrix
1340
+ val finalColorMatrix = android.graphics.ColorMatrix()
1341
+ var hasColorTransforms = false
1342
+
1343
+ if (options.hasKey("adjustments")) {
1344
+ val adj = options.getMap("adjustments")
1345
+ if (adj != null) {
1346
+ val brightness = if (adj.hasKey("brightness")) adj.getDouble("brightness").toFloat() else 0f
1347
+ val contrast = if (adj.hasKey("contrast")) adj.getDouble("contrast").toFloat() else 1f
1348
+ val saturation = if (adj.hasKey("saturation")) adj.getDouble("saturation").toFloat() else 1f
1349
+ val temperature = if (adj.hasKey("temperature")) adj.getDouble("temperature").toFloat() else 0f
1350
+
1351
+ if (saturation != 1f) {
1352
+ finalColorMatrix.setSaturation(saturation)
1353
+ hasColorTransforms = true
1354
+ }
1355
+ if (contrast != 1f || brightness != 0f) {
1356
+ val scale = contrast
1357
+ val translate = (brightness * 255f) + (1f - scale) * 128f
1358
+ val cm = android.graphics.ColorMatrix(floatArrayOf(
1359
+ scale, 0f, 0f, 0f, translate,
1360
+ 0f, scale, 0f, 0f, translate,
1361
+ 0f, 0f, scale, 0f, translate,
1362
+ 0f, 0f, 0f, 1f, 0f
1363
+ ))
1364
+ finalColorMatrix.postConcat(cm)
1365
+ hasColorTransforms = true
1366
+ }
1367
+ if (temperature != 0f) {
1368
+ val warmScaleR = 1f + (temperature * 0.15f)
1369
+ val coolScaleB = 1f - (temperature * 0.15f)
1370
+ val tempCm = android.graphics.ColorMatrix(floatArrayOf(
1371
+ warmScaleR, 0f, 0f, 0f, 0f,
1372
+ 0f, 1f, 0f, 0f, 0f,
1373
+ 0f, 0f, coolScaleB, 0f, 0f,
1374
+ 0f, 0f, 0f, 1f, 0f
1375
+ ))
1376
+ finalColorMatrix.postConcat(tempCm)
1377
+ hasColorTransforms = true
1378
+ }
1379
+ }
1380
+ }
1381
+
1382
+ // 4. Preset Filters
1383
+ val filterPreset = if (options.hasKey("filterPreset")) options.getString("filterPreset") else "none"
1384
+ if (!filterPreset.isNullOrEmpty() && filterPreset != "none") {
1385
+ val presetCm = android.graphics.ColorMatrix()
1386
+ when (filterPreset) {
1387
+ "mono", "noir" -> {
1388
+ presetCm.setSaturation(0f)
1389
+ if (filterPreset == "noir") {
1390
+ presetCm.postConcat(android.graphics.ColorMatrix(floatArrayOf(
1391
+ 1.2f, 0f, 0f, 0f, -20f,
1392
+ 0f, 1.2f, 0f, 0f, -20f,
1393
+ 0f, 0f, 1.2f, 0f, -20f,
1394
+ 0f, 0f, 0f, 1f, 0f
1395
+ )))
1396
+ }
1397
+ hasColorTransforms = true
1398
+ }
1399
+ "sepia" -> {
1400
+ val sepiaMatrix = floatArrayOf(
1401
+ 0.393f, 0.769f, 0.189f, 0f, 0f,
1402
+ 0.349f, 0.686f, 0.168f, 0f, 0f,
1403
+ 0.272f, 0.534f, 0.131f, 0f, 0f,
1404
+ 0f, 0f, 0f, 1f, 0f
1405
+ )
1406
+ presetCm.set(sepiaMatrix)
1407
+ hasColorTransforms = true
1408
+ }
1409
+ "vibrant" -> {
1410
+ presetCm.setSaturation(1.4f)
1411
+ hasColorTransforms = true
1412
+ }
1413
+ "warm" -> {
1414
+ presetCm.set(floatArrayOf(
1415
+ 1.15f, 0f, 0f, 0f, 0f,
1416
+ 0f, 1.05f, 0f, 0f, 0f,
1417
+ 0f, 0f, 0.88f, 0f, 0f,
1418
+ 0f, 0f, 0f, 1f, 0f
1419
+ ))
1420
+ hasColorTransforms = true
1421
+ }
1422
+ "cool" -> {
1423
+ presetCm.set(floatArrayOf(
1424
+ 0.90f, 0f, 0f, 0f, 0f,
1425
+ 0f, 1.0f, 0f, 0f, 0f,
1426
+ 0f, 0f, 1.20f, 0f, 0f,
1427
+ 0f, 0f, 0f, 1f, 0f
1428
+ ))
1429
+ hasColorTransforms = true
1430
+ }
1431
+ "fade" -> {
1432
+ presetCm.set(floatArrayOf(
1433
+ 0.9f, 0f, 0f, 0f, 25f,
1434
+ 0f, 0.9f, 0f, 0f, 25f,
1435
+ 0f, 0f, 0.9f, 0f, 25f,
1436
+ 0f, 0f, 0f, 1f, 0f
1437
+ ))
1438
+ hasColorTransforms = true
1439
+ }
1440
+ }
1441
+ finalColorMatrix.postConcat(presetCm)
1442
+ }
1443
+
1444
+ // Render into final Bitmap
1445
+ val finalBitmap: Bitmap
1446
+ if (hasColorTransforms) {
1447
+ finalBitmap = Bitmap.createBitmap(croppedBitmap.width, croppedBitmap.height, Bitmap.Config.ARGB_8888)
1448
+ val canvas = Canvas(finalBitmap)
1449
+ val paint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {
1450
+ colorFilter = android.graphics.ColorMatrixColorFilter(finalColorMatrix)
1451
+ }
1452
+ canvas.drawBitmap(croppedBitmap, 0f, 0f, paint)
1453
+ } else {
1454
+ finalBitmap = croppedBitmap
1455
+ }
1456
+
1457
+ // 5. Apply Redactions & Annotations Overlay if provided
1458
+ val mutableBitmap = if (finalBitmap.isMutable) finalBitmap else finalBitmap.copy(Bitmap.Config.ARGB_8888, true)
1459
+ val canvas = android.graphics.Canvas(mutableBitmap)
1460
+ val overlayPaint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG)
1461
+
1462
+ if (options.hasKey("redactions")) {
1463
+ val redactions = options.getArray("redactions")
1464
+ if (redactions != null) {
1465
+ overlayPaint.color = android.graphics.Color.BLACK
1466
+ overlayPaint.style = android.graphics.Paint.Style.FILL
1467
+ for (i in 0 until redactions.size()) {
1468
+ val box = redactions.getMap(i) ?: continue
1469
+ var x = if (box.hasKey("x")) box.getDouble("x").toFloat() else 0f
1470
+ var y = if (box.hasKey("y")) box.getDouble("y").toFloat() else 0f
1471
+ var w = if (box.hasKey("width")) box.getDouble("width").toFloat() else 0f
1472
+ var h = if (box.hasKey("height")) box.getDouble("height").toFloat() else 0f
1473
+ val isNorm = if (box.hasKey("isNormalized")) box.getBoolean("isNormalized") else false
1474
+ if (isNorm) {
1475
+ x *= mutableBitmap.width
1476
+ y *= mutableBitmap.height
1477
+ w *= mutableBitmap.width
1478
+ h *= mutableBitmap.height
1479
+ }
1480
+ canvas.drawRect(x, y, x + w, y + h, overlayPaint)
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ if (options.hasKey("annotations")) {
1486
+ val annotations = options.getArray("annotations")
1487
+ if (annotations != null) {
1488
+ overlayPaint.color = android.graphics.Color.RED
1489
+ overlayPaint.style = android.graphics.Paint.Style.STROKE
1490
+ overlayPaint.strokeWidth = 6f
1491
+ for (i in 0 until annotations.size()) {
1492
+ val ann = annotations.getMap(i) ?: continue
1493
+ var x = if (ann.hasKey("x")) ann.getDouble("x").toFloat() else 0f
1494
+ var y = if (ann.hasKey("y")) ann.getDouble("y").toFloat() else 0f
1495
+ var w = if (ann.hasKey("width")) ann.getDouble("width").toFloat() else 0f
1496
+ var h = if (ann.hasKey("height")) ann.getDouble("height").toFloat() else 0f
1497
+ val isNorm = if (ann.hasKey("isNormalized")) ann.getBoolean("isNormalized") else false
1498
+ if (isNorm) {
1499
+ x *= mutableBitmap.width
1500
+ y *= mutableBitmap.height
1501
+ w *= mutableBitmap.width
1502
+ h *= mutableBitmap.height
1503
+ }
1504
+ canvas.drawRect(x, y, x + w, y + h, overlayPaint)
1505
+ }
1506
+ }
1507
+ }
1508
+
1509
+ val format = if (options.hasKey("format")) options.getString("format")?.lowercase() ?: "jpeg" else "jpeg"
1510
+ val quality = if (options.hasKey("quality")) options.getDouble("quality").toFloat() else 0.9f
1511
+ val qualityInt = (quality * 100).toInt().coerceIn(1, 100)
1512
+
1513
+ val compressFormat: Bitmap.CompressFormat
1514
+ val ext: String
1515
+ val mimeType: String
1516
+
1517
+ when (format) {
1518
+ "png" -> {
1519
+ compressFormat = Bitmap.CompressFormat.PNG
1520
+ ext = "png"
1521
+ mimeType = "image/png"
1522
+ }
1523
+ "webp" -> {
1524
+ compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1525
+ Bitmap.CompressFormat.WEBP_LOSSY
1526
+ } else {
1527
+ @Suppress("DEPRECATION")
1528
+ Bitmap.CompressFormat.WEBP
1529
+ }
1530
+ ext = "webp"
1531
+ mimeType = "image/webp"
1532
+ }
1533
+ else -> {
1534
+ compressFormat = Bitmap.CompressFormat.JPEG
1535
+ ext = "jpg"
1536
+ mimeType = "image/jpeg"
1537
+ }
1538
+ }
1539
+
1540
+ val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss_SSS", java.util.Locale.US).format(java.util.Date())
1541
+ val outFile = File(getCapturesDirectory(), "edit_${timestamp}.$ext")
1542
+ FileOutputStream(outFile).use { out ->
1543
+ mutableBitmap.compress(compressFormat, qualityInt, out)
1544
+ out.flush()
1545
+ }
1546
+
1547
+ val resultMap = Arguments.createMap().apply {
1548
+ putString("uri", Uri.fromFile(outFile).toString())
1549
+ putInt("width", mutableBitmap.width)
1550
+ putInt("height", mutableBitmap.height)
1551
+ putDouble("size", outFile.length().toDouble())
1552
+ putString("mimeType", mimeType)
1553
+ putString("format", format)
1554
+ }
1555
+ promise.resolve(resultMap)
1556
+ } catch (e: Exception) {
1557
+ promise.reject("PHOTO_EDIT_ERROR", e.message ?: "Photo editing failed", e)
1558
+ }
1559
+ }
1560
+ }
1201
1561
 
1562
+ // ─────────────────────────────────────────────────────────────────────────
1563
+ // 100% Native Media Editor: Video Trimming (MediaExtractor + MediaMuxer)
1564
+ // ─────────────────────────────────────────────────────────────────────────
1202
1565
 
1203
1566
  @ReactMethod
1204
- fun addListener(eventName: String) {
1205
- // Required for React Native NativeEventEmitter
1567
+ fun trimVideo(options: ReadableMap, promise: Promise) {
1568
+ captureExecutor.execute {
1569
+ var extractor: android.media.MediaExtractor? = null
1570
+ var muxer: android.media.MediaMuxer? = null
1571
+ try {
1572
+ val rawUri = if (options.hasKey("uri")) options.getString("uri") else null
1573
+ if (rawUri.isNullOrEmpty()) {
1574
+ promise.reject("INVALID_URI", "Video URI is missing or empty")
1575
+ return@execute
1576
+ }
1577
+
1578
+ val cleanPath = when {
1579
+ rawUri.startsWith("file://") -> Uri.parse(rawUri).path ?: rawUri.substring(7)
1580
+ else -> rawUri
1581
+ }
1582
+
1583
+ var startMs = if (options.hasKey("startTimeMs")) options.getDouble("startTimeMs").toLong() else 0L
1584
+ var endMs = if (options.hasKey("endTimeMs")) options.getDouble("endTimeMs").toLong() else 0L
1585
+ val muteAudio = (if (options.hasKey("mute")) options.getBoolean("mute") else false) ||
1586
+ (if (options.hasKey("muteAudio")) options.getBoolean("muteAudio") else false)
1587
+
1588
+ extractor = android.media.MediaExtractor()
1589
+ if (cleanPath.startsWith("content://")) {
1590
+ extractor.setDataSource(reactContext, Uri.parse(cleanPath), null)
1591
+ } else {
1592
+ extractor.setDataSource(cleanPath)
1593
+ }
1594
+
1595
+ var totalDurationUs = 0L
1596
+ val trackCount = extractor.trackCount
1597
+ for (i in 0 until trackCount) {
1598
+ val format = extractor.getTrackFormat(i)
1599
+ if (format.containsKey(android.media.MediaFormat.KEY_DURATION)) {
1600
+ val dur = format.getLong(android.media.MediaFormat.KEY_DURATION)
1601
+ if (dur > totalDurationUs) {
1602
+ totalDurationUs = dur
1603
+ }
1604
+ }
1605
+ }
1606
+
1607
+ val totalDurationMs = totalDurationUs / 1000L
1608
+ if (endMs <= 0 || (totalDurationMs > 0 && endMs > totalDurationMs)) {
1609
+ endMs = if (totalDurationMs > 0) totalDurationMs else endMs
1610
+ }
1611
+ if (startMs >= endMs && endMs > 0) {
1612
+ startMs = 0L
1613
+ }
1614
+
1615
+ val startUs = startMs * 1000L
1616
+ val endUs = if (endMs > 0) endMs * 1000L else Long.MAX_VALUE
1617
+
1618
+ val timestamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss_SSS", java.util.Locale.US).format(java.util.Date())
1619
+ val outFile = File(getCapturesDirectory(), "trim_${timestamp}.mp4")
1620
+
1621
+ muxer = android.media.MediaMuxer(outFile.absolutePath, android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
1622
+
1623
+ val trackIndexMap = HashMap<Int, Int>()
1624
+ var videoWidth = 1080
1625
+ var videoHeight = 1920
1626
+
1627
+ for (i in 0 until trackCount) {
1628
+ val format = extractor.getTrackFormat(i)
1629
+ val mime = format.getString(android.media.MediaFormat.KEY_MIME) ?: ""
1630
+ if (mime.startsWith("video/")) {
1631
+ extractor.selectTrack(i)
1632
+ val muxerTrackIndex = muxer.addTrack(format)
1633
+ trackIndexMap[i] = muxerTrackIndex
1634
+ if (format.containsKey(android.media.MediaFormat.KEY_WIDTH)) {
1635
+ videoWidth = format.getInteger(android.media.MediaFormat.KEY_WIDTH)
1636
+ }
1637
+ if (format.containsKey(android.media.MediaFormat.KEY_HEIGHT)) {
1638
+ videoHeight = format.getInteger(android.media.MediaFormat.KEY_HEIGHT)
1639
+ }
1640
+ } else if (mime.startsWith("audio/") && !muteAudio) {
1641
+ extractor.selectTrack(i)
1642
+ val muxerTrackIndex = muxer.addTrack(format)
1643
+ trackIndexMap[i] = muxerTrackIndex
1644
+ }
1645
+ }
1646
+
1647
+ muxer.start()
1648
+
1649
+ extractor.seekTo(startUs, android.media.MediaExtractor.SEEK_TO_CLOSEST_SYNC)
1650
+
1651
+ val bufferSize = 1024 * 1024
1652
+ val buffer = java.nio.ByteBuffer.allocateDirect(bufferSize)
1653
+ val bufferInfo = android.media.MediaCodec.BufferInfo()
1654
+
1655
+ while (true) {
1656
+ val trackIndex = extractor.sampleTrackIndex
1657
+ if (trackIndex < 0) break
1658
+
1659
+ val sampleTime = extractor.sampleTime
1660
+ if (sampleTime > endUs) break
1661
+
1662
+ if (sampleTime >= startUs) {
1663
+ bufferInfo.offset = 0
1664
+ bufferInfo.size = extractor.readSampleData(buffer, 0)
1665
+ if (bufferInfo.size > 0) {
1666
+ bufferInfo.presentationTimeUs = sampleTime - startUs
1667
+ bufferInfo.flags = extractor.sampleFlags
1668
+ val muxerTrackIndex = trackIndexMap[trackIndex]
1669
+ if (muxerTrackIndex != null) {
1670
+ muxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo)
1671
+ }
1672
+ }
1673
+ }
1674
+ extractor.advance()
1675
+ }
1676
+
1677
+ muxer.stop()
1678
+ muxer.release()
1679
+ muxer = null
1680
+
1681
+ extractor.release()
1682
+ extractor = null
1683
+
1684
+ val durationMs = if (endMs > startMs) (endMs - startMs) else totalDurationMs
1685
+ val resultMap = Arguments.createMap().apply {
1686
+ putString("uri", Uri.fromFile(outFile).toString())
1687
+ putDouble("durationMs", durationMs.toDouble())
1688
+ putDouble("size", outFile.length().toDouble())
1689
+ putInt("width", videoWidth)
1690
+ putInt("height", videoHeight)
1691
+ putString("format", "mp4")
1692
+ }
1693
+ promise.resolve(resultMap)
1694
+ } catch (e: Exception) {
1695
+ try { muxer?.release() } catch (_: Exception) {}
1696
+ try { extractor?.release() } catch (_: Exception) {}
1697
+ promise.reject("VIDEO_TRIM_ERROR", e.message ?: "Video trim failed", e)
1698
+ }
1699
+ }
1700
+ }
1701
+
1702
+ // ─────────────────────────────────────────────────────────────────────────
1703
+ // 100% Native Media Editor: Filmstrip Generator (MediaMetadataRetriever)
1704
+ // ─────────────────────────────────────────────────────────────────────────
1705
+
1706
+ @ReactMethod
1707
+ fun generateFilmstrip(options: ReadableMap, promise: Promise) {
1708
+ captureExecutor.execute {
1709
+ var retriever: MediaMetadataRetriever? = null
1710
+ try {
1711
+ val rawUri = if (options.hasKey("uri")) options.getString("uri") else null
1712
+ if (rawUri.isNullOrEmpty()) {
1713
+ promise.reject("INVALID_URI", "Video URI is missing or empty")
1714
+ return@execute
1715
+ }
1716
+
1717
+ val cleanPath = when {
1718
+ rawUri.startsWith("file://") -> Uri.parse(rawUri).path ?: rawUri.substring(7)
1719
+ else -> rawUri
1720
+ }
1721
+
1722
+ val count = if (options.hasKey("count")) options.getInt("count").coerceIn(1, 50) else 10
1723
+ val targetW = if (options.hasKey("maxWidth")) options.getInt("maxWidth")
1724
+ else if (options.hasKey("targetWidth")) options.getInt("targetWidth") else 120
1725
+ val targetH = if (options.hasKey("maxHeight")) options.getInt("maxHeight")
1726
+ else if (options.hasKey("targetHeight")) options.getInt("targetHeight") else 120
1727
+ val qualityDouble = if (options.hasKey("quality")) options.getDouble("quality") else 0.7
1728
+ val qualityInt = (qualityDouble * 100).toInt().coerceIn(10, 100)
1729
+
1730
+ retriever = MediaMetadataRetriever()
1731
+ if (cleanPath.startsWith("content://")) {
1732
+ retriever.setDataSource(reactContext, Uri.parse(cleanPath))
1733
+ } else {
1734
+ retriever.setDataSource(cleanPath)
1735
+ }
1736
+
1737
+ val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
1738
+ val durationMs = durationStr?.toLongOrNull() ?: 0L
1739
+ if (durationMs <= 0L) {
1740
+ promise.reject("INVALID_DURATION", "Unable to determine video duration")
1741
+ return@execute
1742
+ }
1743
+
1744
+ val durationUs = durationMs * 1000L
1745
+ val stepUs = durationUs / count.coerceAtLeast(1)
1746
+ val thumbnailsArray = Arguments.createArray()
1747
+
1748
+ for (i in 0 until count) {
1749
+ val targetUs = i * stepUs
1750
+ val frameBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
1751
+ retriever.getScaledFrameAtTime(targetUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, targetW, targetH)
1752
+ } else {
1753
+ val original = retriever.getFrameAtTime(targetUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
1754
+ if (original != null) {
1755
+ Bitmap.createScaledBitmap(original, targetW, targetH, true)
1756
+ } else null
1757
+ }
1758
+
1759
+ if (frameBitmap != null) {
1760
+ val stream = ByteArrayOutputStream()
1761
+ frameBitmap.compress(Bitmap.CompressFormat.JPEG, qualityInt, stream)
1762
+ val b64 = Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP)
1763
+ val dataUrl = "data:image/jpeg;base64,$b64"
1764
+
1765
+ val thumbMap = Arguments.createMap().apply {
1766
+ putDouble("timeMs", (targetUs / 1000L).toDouble())
1767
+ putString("uri", dataUrl)
1768
+ putInt("index", i)
1769
+ }
1770
+ thumbnailsArray.pushMap(thumbMap)
1771
+ }
1772
+ }
1773
+
1774
+ retriever.release()
1775
+ retriever = null
1776
+
1777
+ val resultMap = Arguments.createMap().apply {
1778
+ putArray("thumbnails", thumbnailsArray)
1779
+ putDouble("durationMs", durationMs.toDouble())
1780
+ }
1781
+ promise.resolve(resultMap)
1782
+ } catch (e: Exception) {
1783
+ try { retriever?.release() } catch (_: Exception) {}
1784
+ promise.reject("FILMSTRIP_ERROR", e.message ?: "Filmstrip generation failed", e)
1785
+ }
1786
+ }
1206
1787
  }
1207
1788
 
1208
1789
 
1790
+
1209
1791
  @ReactMethod
1210
1792
  fun removeListeners(count: Double) {
1211
1793
  // Required for React Native NativeEventEmitter