react-native-ensys-camera-x 1.0.7

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 (506) hide show
  1. package/CustomCamera.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +833 -0
  4. package/android/build.gradle +135 -0
  5. package/android/consumer-rules.pro +47 -0
  6. package/android/src/main/AndroidManifest.xml +72 -0
  7. package/android/src/main/java/android/print/PdfPrint.java +51 -0
  8. package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
  9. package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
  10. package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
  11. package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
  12. package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
  13. package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
  14. package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
  15. package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
  16. package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
  17. package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
  18. package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
  19. package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
  20. package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
  21. package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
  22. package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
  23. package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
  24. package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
  25. package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
  26. package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
  27. package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
  28. package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
  29. package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
  30. package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
  31. package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
  32. package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
  33. package/android/src/main/java/com/customcamera/Localization.kt +206 -0
  34. package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
  35. package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
  36. package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
  37. package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
  38. package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
  39. package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
  40. package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
  41. package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
  42. package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
  43. package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
  44. package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
  45. package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
  46. package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
  47. package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
  48. package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
  49. package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
  50. package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
  51. package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
  52. package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
  53. package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
  54. package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
  55. package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
  56. package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
  57. package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
  58. package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
  59. package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
  60. package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
  61. package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
  62. package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
  63. package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
  64. package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
  65. package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
  66. package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
  67. package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
  68. package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
  69. package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
  70. package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
  71. package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
  72. package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
  73. package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
  74. package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
  75. package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
  76. package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
  77. package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
  78. package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
  79. package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
  80. package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
  81. package/android/src/main/res/drawable/bg_pill.xml +6 -0
  82. package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
  83. package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
  84. package/android/src/main/res/drawable/bg_send_button.xml +5 -0
  85. package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
  86. package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
  87. package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
  88. package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
  89. package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
  90. package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
  91. package/android/src/main/res/drawable/ic_audio.xml +10 -0
  92. package/android/src/main/res/drawable/ic_back.xml +10 -0
  93. package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
  94. package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
  95. package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
  96. package/android/src/main/res/drawable/ic_check.xml +17 -0
  97. package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
  98. package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
  99. package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
  100. package/android/src/main/res/drawable/ic_close.xml +10 -0
  101. package/android/src/main/res/drawable/ic_cover.xml +9 -0
  102. package/android/src/main/res/drawable/ic_crop.xml +10 -0
  103. package/android/src/main/res/drawable/ic_delete.xml +9 -0
  104. package/android/src/main/res/drawable/ic_document.xml +10 -0
  105. package/android/src/main/res/drawable/ic_done.xml +10 -0
  106. package/android/src/main/res/drawable/ic_download.xml +10 -0
  107. package/android/src/main/res/drawable/ic_emoji.xml +10 -0
  108. package/android/src/main/res/drawable/ic_filter.xml +10 -0
  109. package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
  110. package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
  111. package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
  112. package/android/src/main/res/drawable/ic_flip.xml +15 -0
  113. package/android/src/main/res/drawable/ic_gallery.xml +10 -0
  114. package/android/src/main/res/drawable/ic_gif.xml +24 -0
  115. package/android/src/main/res/drawable/ic_lock.xml +19 -0
  116. package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
  117. package/android/src/main/res/drawable/ic_marker.xml +10 -0
  118. package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
  119. package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
  120. package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
  121. package/android/src/main/res/drawable/ic_pause.xml +10 -0
  122. package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
  123. package/android/src/main/res/drawable/ic_play.xml +10 -0
  124. package/android/src/main/res/drawable/ic_redo.xml +10 -0
  125. package/android/src/main/res/drawable/ic_rotate.xml +9 -0
  126. package/android/src/main/res/drawable/ic_send.xml +10 -0
  127. package/android/src/main/res/drawable/ic_share.xml +10 -0
  128. package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
  129. package/android/src/main/res/drawable/ic_sticker.xml +24 -0
  130. package/android/src/main/res/drawable/ic_text.xml +10 -0
  131. package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
  132. package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
  133. package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
  134. package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
  135. package/android/src/main/res/drawable/ic_trim.xml +23 -0
  136. package/android/src/main/res/drawable/ic_undo.xml +10 -0
  137. package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
  138. package/android/src/main/res/drawable/ic_video_note.xml +13 -0
  139. package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
  140. package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
  141. package/android/src/main/res/drawable/sticker_heart.xml +9 -0
  142. package/android/src/main/res/drawable/sticker_smile.xml +9 -0
  143. package/android/src/main/res/drawable/sticker_star.xml +9 -0
  144. package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
  145. package/android/src/main/res/values/colors.xml +34 -0
  146. package/android/src/main/res/values/ids.xml +15 -0
  147. package/android/src/main/res/values/strings.xml +15 -0
  148. package/android/src/main/res/values/styles.xml +20 -0
  149. package/android/src/main/res/xml/file_paths.xml +34 -0
  150. package/ios/CustomCamera.h +5 -0
  151. package/ios/CustomCamera.mm +214 -0
  152. package/lib/module/AudioPlayerContext.js +233 -0
  153. package/lib/module/AudioPlayerContext.js.map +1 -0
  154. package/lib/module/CameraPreview.js +304 -0
  155. package/lib/module/CameraPreview.js.map +1 -0
  156. package/lib/module/NativeAnimatedImageView.js +41 -0
  157. package/lib/module/NativeAnimatedImageView.js.map +1 -0
  158. package/lib/module/NativeAudioPlayerView.js +33 -0
  159. package/lib/module/NativeAudioPlayerView.js.map +1 -0
  160. package/lib/module/NativeBlurView.js +35 -0
  161. package/lib/module/NativeBlurView.js.map +1 -0
  162. package/lib/module/NativeCustomCamera.js +128 -0
  163. package/lib/module/NativeCustomCamera.js.map +1 -0
  164. package/lib/module/NativeCustomCamera.web.js +143 -0
  165. package/lib/module/NativeCustomCamera.web.js.map +1 -0
  166. package/lib/module/NativeCustomVideoView.js +5 -0
  167. package/lib/module/NativeCustomVideoView.js.map +1 -0
  168. package/lib/module/NativeDocumentPreviewView.js +156 -0
  169. package/lib/module/NativeDocumentPreviewView.js.map +1 -0
  170. package/lib/module/NativeMediaThumbnailView.js +110 -0
  171. package/lib/module/NativeMediaThumbnailView.js.map +1 -0
  172. package/lib/module/NativeSvgIcon.js +32 -0
  173. package/lib/module/NativeSvgIcon.js.map +1 -0
  174. package/lib/module/ResponsivePreviewContainer.js +171 -0
  175. package/lib/module/ResponsivePreviewContainer.js.map +1 -0
  176. package/lib/module/accessibility.js +61 -0
  177. package/lib/module/accessibility.js.map +1 -0
  178. package/lib/module/config.js +352 -0
  179. package/lib/module/config.js.map +1 -0
  180. package/lib/module/cpuOptimization.js +296 -0
  181. package/lib/module/cpuOptimization.js.map +1 -0
  182. package/lib/module/db/DatabaseManager.js +191 -0
  183. package/lib/module/db/DatabaseManager.js.map +1 -0
  184. package/lib/module/db/MediaMapper.js +284 -0
  185. package/lib/module/db/MediaMapper.js.map +1 -0
  186. package/lib/module/db/MediaRepository.js +242 -0
  187. package/lib/module/db/MediaRepository.js.map +1 -0
  188. package/lib/module/db/index.js +41 -0
  189. package/lib/module/db/index.js.map +1 -0
  190. package/lib/module/db/migrations.js +180 -0
  191. package/lib/module/db/migrations.js.map +1 -0
  192. package/lib/module/db/react-native-sqlcipher.d.js +2 -0
  193. package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
  194. package/lib/module/db/secureKey.js +87 -0
  195. package/lib/module/db/secureKey.js.map +1 -0
  196. package/lib/module/db/types.js +57 -0
  197. package/lib/module/db/types.js.map +1 -0
  198. package/lib/module/db/useMediaDatabase.js +123 -0
  199. package/lib/module/db/useMediaDatabase.js.map +1 -0
  200. package/lib/module/defaultIcons.js +2259 -0
  201. package/lib/module/defaultIcons.js.map +1 -0
  202. package/lib/module/deviceCapabilities.js +408 -0
  203. package/lib/module/deviceCapabilities.js.map +1 -0
  204. package/lib/module/errors.js +267 -0
  205. package/lib/module/errors.js.map +1 -0
  206. package/lib/module/iconConfig.js +796 -0
  207. package/lib/module/iconConfig.js.map +1 -0
  208. package/lib/module/icons.js +514 -0
  209. package/lib/module/icons.js.map +1 -0
  210. package/lib/module/index.js +251 -0
  211. package/lib/module/index.js.map +1 -0
  212. package/lib/module/jest-globals.d.js +2 -0
  213. package/lib/module/jest-globals.d.js.map +1 -0
  214. package/lib/module/library/MediaLibrary.js +107 -0
  215. package/lib/module/library/MediaLibrary.js.map +1 -0
  216. package/lib/module/library/MediaLibraryScreen.js +2597 -0
  217. package/lib/module/library/MediaLibraryScreen.js.map +1 -0
  218. package/lib/module/library/components/AudioPreview.js +74 -0
  219. package/lib/module/library/components/AudioPreview.js.map +1 -0
  220. package/lib/module/library/components/DocumentPreview.js +331 -0
  221. package/lib/module/library/components/DocumentPreview.js.map +1 -0
  222. package/lib/module/library/components/ExpandableFab.js +343 -0
  223. package/lib/module/library/components/ExpandableFab.js.map +1 -0
  224. package/lib/module/library/components/FileTiles.js +251 -0
  225. package/lib/module/library/components/FileTiles.js.map +1 -0
  226. package/lib/module/library/components/ImagePreview.js +70 -0
  227. package/lib/module/library/components/ImagePreview.js.map +1 -0
  228. package/lib/module/library/components/MediaMetadataHeader.js +175 -0
  229. package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
  230. package/lib/module/library/components/RecordAudioScreen.js +816 -0
  231. package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
  232. package/lib/module/library/components/VideoPreview.js +453 -0
  233. package/lib/module/library/components/VideoPreview.js.map +1 -0
  234. package/lib/module/library/components/ViewerCounter.js +55 -0
  235. package/lib/module/library/components/ViewerCounter.js.map +1 -0
  236. package/lib/module/library/compressionEstimate.js +507 -0
  237. package/lib/module/library/compressionEstimate.js.map +1 -0
  238. package/lib/module/library/fileMetadata.js +196 -0
  239. package/lib/module/library/fileMetadata.js.map +1 -0
  240. package/lib/module/library/index.js +42 -0
  241. package/lib/module/library/index.js.map +1 -0
  242. package/lib/module/library/libraryIconRegistry.js +69 -0
  243. package/lib/module/library/libraryIconRegistry.js.map +1 -0
  244. package/lib/module/library/libraryIcons.js +858 -0
  245. package/lib/module/library/libraryIcons.js.map +1 -0
  246. package/lib/module/library/localizedFormat.js +181 -0
  247. package/lib/module/library/localizedFormat.js.map +1 -0
  248. package/lib/module/library/palette.js +225 -0
  249. package/lib/module/library/palette.js.map +1 -0
  250. package/lib/module/library/responsive.js +53 -0
  251. package/lib/module/library/responsive.js.map +1 -0
  252. package/lib/module/locales.js +416 -0
  253. package/lib/module/locales.js.map +1 -0
  254. package/lib/module/localization.js +1064 -0
  255. package/lib/module/localization.js.map +1 -0
  256. package/lib/module/logger.js +20 -0
  257. package/lib/module/logger.js.map +1 -0
  258. package/lib/module/lowMemoryCache.js +206 -0
  259. package/lib/module/lowMemoryCache.js.map +1 -0
  260. package/lib/module/mediaHistory.js +33 -0
  261. package/lib/module/mediaHistory.js.map +1 -0
  262. package/lib/module/mediaStorageService.js +402 -0
  263. package/lib/module/mediaStorageService.js.map +1 -0
  264. package/lib/module/memory.js +79 -0
  265. package/lib/module/memory.js.map +1 -0
  266. package/lib/module/nativeActions.js +307 -0
  267. package/lib/module/nativeActions.js.map +1 -0
  268. package/lib/module/nativeComponentSupport.js +61 -0
  269. package/lib/module/nativeComponentSupport.js.map +1 -0
  270. package/lib/module/networkPerformance.js +24 -0
  271. package/lib/module/networkPerformance.js.map +1 -0
  272. package/lib/module/package.json +1 -0
  273. package/lib/module/performance.js +104 -0
  274. package/lib/module/performance.js.map +1 -0
  275. package/lib/module/performanceMonitor.js +155 -0
  276. package/lib/module/performanceMonitor.js.map +1 -0
  277. package/lib/module/permissions.js +370 -0
  278. package/lib/module/permissions.js.map +1 -0
  279. package/lib/module/rendering.js +40 -0
  280. package/lib/module/rendering.js.map +1 -0
  281. package/lib/module/security.js +18 -0
  282. package/lib/module/security.js.map +1 -0
  283. package/lib/module/state.js +266 -0
  284. package/lib/module/state.js.map +1 -0
  285. package/lib/module/storage.js +400 -0
  286. package/lib/module/storage.js.map +1 -0
  287. package/lib/module/theme.js +390 -0
  288. package/lib/module/theme.js.map +1 -0
  289. package/lib/module/types.js +4 -0
  290. package/lib/module/types.js.map +1 -0
  291. package/lib/module/utils.js +115 -0
  292. package/lib/module/utils.js.map +1 -0
  293. package/lib/module/validation.js +288 -0
  294. package/lib/module/validation.js.map +1 -0
  295. package/lib/typescript/package.json +1 -0
  296. package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
  297. package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
  298. package/lib/typescript/src/CameraPreview.d.ts +13 -0
  299. package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
  300. package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
  301. package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
  302. package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
  303. package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
  304. package/lib/typescript/src/NativeBlurView.d.ts +12 -0
  305. package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
  306. package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
  307. package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
  308. package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
  309. package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
  310. package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
  311. package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
  312. package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
  313. package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
  314. package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
  315. package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
  316. package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
  317. package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
  318. package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
  319. package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
  320. package/lib/typescript/src/accessibility.d.ts +20 -0
  321. package/lib/typescript/src/accessibility.d.ts.map +1 -0
  322. package/lib/typescript/src/config.d.ts +145 -0
  323. package/lib/typescript/src/config.d.ts.map +1 -0
  324. package/lib/typescript/src/cpuOptimization.d.ts +63 -0
  325. package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
  326. package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
  327. package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
  328. package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
  329. package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
  330. package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
  331. package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
  332. package/lib/typescript/src/db/index.d.ts +23 -0
  333. package/lib/typescript/src/db/index.d.ts.map +1 -0
  334. package/lib/typescript/src/db/migrations.d.ts +31 -0
  335. package/lib/typescript/src/db/migrations.d.ts.map +1 -0
  336. package/lib/typescript/src/db/secureKey.d.ts +10 -0
  337. package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
  338. package/lib/typescript/src/db/types.d.ts +115 -0
  339. package/lib/typescript/src/db/types.d.ts.map +1 -0
  340. package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
  341. package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
  342. package/lib/typescript/src/defaultIcons.d.ts +139 -0
  343. package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
  344. package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
  345. package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
  346. package/lib/typescript/src/errors.d.ts +95 -0
  347. package/lib/typescript/src/errors.d.ts.map +1 -0
  348. package/lib/typescript/src/iconConfig.d.ts +176 -0
  349. package/lib/typescript/src/iconConfig.d.ts.map +1 -0
  350. package/lib/typescript/src/icons.d.ts +217 -0
  351. package/lib/typescript/src/icons.d.ts.map +1 -0
  352. package/lib/typescript/src/index.d.ts +104 -0
  353. package/lib/typescript/src/index.d.ts.map +1 -0
  354. package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
  355. package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
  356. package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
  357. package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
  358. package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
  359. package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
  360. package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
  361. package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
  362. package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
  363. package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
  364. package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
  365. package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
  366. package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
  367. package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
  368. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
  369. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
  370. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
  371. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
  372. package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
  373. package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
  374. package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
  375. package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
  376. package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
  377. package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
  378. package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
  379. package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
  380. package/lib/typescript/src/library/index.d.ts +40 -0
  381. package/lib/typescript/src/library/index.d.ts.map +1 -0
  382. package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
  383. package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
  384. package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
  385. package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
  386. package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
  387. package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
  388. package/lib/typescript/src/library/palette.d.ts +104 -0
  389. package/lib/typescript/src/library/palette.d.ts.map +1 -0
  390. package/lib/typescript/src/library/responsive.d.ts +15 -0
  391. package/lib/typescript/src/library/responsive.d.ts.map +1 -0
  392. package/lib/typescript/src/locales.d.ts +103 -0
  393. package/lib/typescript/src/locales.d.ts.map +1 -0
  394. package/lib/typescript/src/localization.d.ts +1051 -0
  395. package/lib/typescript/src/localization.d.ts.map +1 -0
  396. package/lib/typescript/src/logger.d.ts +4 -0
  397. package/lib/typescript/src/logger.d.ts.map +1 -0
  398. package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
  399. package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
  400. package/lib/typescript/src/mediaHistory.d.ts +3 -0
  401. package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
  402. package/lib/typescript/src/mediaStorageService.d.ts +155 -0
  403. package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
  404. package/lib/typescript/src/memory.d.ts +21 -0
  405. package/lib/typescript/src/memory.d.ts.map +1 -0
  406. package/lib/typescript/src/nativeActions.d.ts +174 -0
  407. package/lib/typescript/src/nativeActions.d.ts.map +1 -0
  408. package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
  409. package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
  410. package/lib/typescript/src/networkPerformance.d.ts +12 -0
  411. package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
  412. package/lib/typescript/src/performance.d.ts +32 -0
  413. package/lib/typescript/src/performance.d.ts.map +1 -0
  414. package/lib/typescript/src/performanceMonitor.d.ts +45 -0
  415. package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
  416. package/lib/typescript/src/permissions.d.ts +132 -0
  417. package/lib/typescript/src/permissions.d.ts.map +1 -0
  418. package/lib/typescript/src/rendering.d.ts +9 -0
  419. package/lib/typescript/src/rendering.d.ts.map +1 -0
  420. package/lib/typescript/src/security.d.ts +6 -0
  421. package/lib/typescript/src/security.d.ts.map +1 -0
  422. package/lib/typescript/src/state.d.ts +102 -0
  423. package/lib/typescript/src/state.d.ts.map +1 -0
  424. package/lib/typescript/src/storage.d.ts +117 -0
  425. package/lib/typescript/src/storage.d.ts.map +1 -0
  426. package/lib/typescript/src/theme.d.ts +15 -0
  427. package/lib/typescript/src/theme.d.ts.map +1 -0
  428. package/lib/typescript/src/types.d.ts +802 -0
  429. package/lib/typescript/src/types.d.ts.map +1 -0
  430. package/lib/typescript/src/utils.d.ts +50 -0
  431. package/lib/typescript/src/utils.d.ts.map +1 -0
  432. package/lib/typescript/src/validation.d.ts +52 -0
  433. package/lib/typescript/src/validation.d.ts.map +1 -0
  434. package/package.json +204 -0
  435. package/react-native.config.js +17 -0
  436. package/src/AudioPlayerContext.tsx +220 -0
  437. package/src/CameraPreview.tsx +352 -0
  438. package/src/NativeAnimatedImageView.tsx +71 -0
  439. package/src/NativeAudioPlayerView.tsx +39 -0
  440. package/src/NativeBlurView.tsx +40 -0
  441. package/src/NativeCustomCamera.ts +305 -0
  442. package/src/NativeCustomCamera.web.ts +192 -0
  443. package/src/NativeCustomVideoView.ts +30 -0
  444. package/src/NativeDocumentPreviewView.tsx +196 -0
  445. package/src/NativeMediaThumbnailView.tsx +167 -0
  446. package/src/NativeSvgIcon.tsx +46 -0
  447. package/src/ResponsivePreviewContainer.tsx +227 -0
  448. package/src/accessibility.ts +80 -0
  449. package/src/config.ts +406 -0
  450. package/src/cpuOptimization.ts +365 -0
  451. package/src/db/DatabaseManager.ts +212 -0
  452. package/src/db/MediaMapper.ts +377 -0
  453. package/src/db/MediaRepository.ts +313 -0
  454. package/src/db/index.ts +73 -0
  455. package/src/db/migrations.ts +222 -0
  456. package/src/db/react-native-sqlcipher.d.ts +76 -0
  457. package/src/db/secureKey.ts +100 -0
  458. package/src/db/types.ts +123 -0
  459. package/src/db/useMediaDatabase.ts +191 -0
  460. package/src/defaultIcons.tsx +2390 -0
  461. package/src/deviceCapabilities.ts +438 -0
  462. package/src/errors.ts +350 -0
  463. package/src/iconConfig.tsx +962 -0
  464. package/src/icons.tsx +788 -0
  465. package/src/index.tsx +763 -0
  466. package/src/jest-globals.d.ts +9 -0
  467. package/src/library/MediaLibrary.tsx +141 -0
  468. package/src/library/MediaLibraryScreen.tsx +3003 -0
  469. package/src/library/components/AudioPreview.tsx +73 -0
  470. package/src/library/components/DocumentPreview.tsx +361 -0
  471. package/src/library/components/ExpandableFab.tsx +342 -0
  472. package/src/library/components/FileTiles.tsx +252 -0
  473. package/src/library/components/ImagePreview.tsx +72 -0
  474. package/src/library/components/MediaMetadataHeader.tsx +208 -0
  475. package/src/library/components/RecordAudioScreen.tsx +845 -0
  476. package/src/library/components/VideoPreview.tsx +478 -0
  477. package/src/library/components/ViewerCounter.tsx +71 -0
  478. package/src/library/compressionEstimate.ts +662 -0
  479. package/src/library/fileMetadata.ts +212 -0
  480. package/src/library/index.ts +66 -0
  481. package/src/library/libraryIconRegistry.tsx +86 -0
  482. package/src/library/libraryIcons.tsx +928 -0
  483. package/src/library/localizedFormat.ts +207 -0
  484. package/src/library/palette.ts +281 -0
  485. package/src/library/responsive.ts +49 -0
  486. package/src/locales.ts +381 -0
  487. package/src/localization.ts +1947 -0
  488. package/src/logger.ts +20 -0
  489. package/src/lowMemoryCache.ts +218 -0
  490. package/src/mediaHistory.ts +42 -0
  491. package/src/mediaStorageService.ts +546 -0
  492. package/src/memory.ts +102 -0
  493. package/src/nativeActions.ts +422 -0
  494. package/src/nativeComponentSupport.ts +60 -0
  495. package/src/networkPerformance.ts +30 -0
  496. package/src/performance.ts +137 -0
  497. package/src/performanceMonitor.ts +219 -0
  498. package/src/permissions.ts +403 -0
  499. package/src/rendering.ts +47 -0
  500. package/src/security.ts +19 -0
  501. package/src/state.ts +333 -0
  502. package/src/storage.ts +453 -0
  503. package/src/theme.tsx +467 -0
  504. package/src/types.ts +946 -0
  505. package/src/utils.ts +177 -0
  506. package/src/validation.ts +372 -0
@@ -0,0 +1,845 @@
1
+ package com.customcamera
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import android.graphics.Matrix
6
+ import android.graphics.SurfaceTexture
7
+ import android.media.MediaMetadataRetriever
8
+ import android.media.MediaPlayer
9
+ import android.net.Uri
10
+ import android.os.Build
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.view.Gravity
14
+ import android.view.Surface
15
+ import android.view.TextureView
16
+ import android.view.View
17
+ import android.widget.FrameLayout
18
+ import com.facebook.react.bridge.Arguments
19
+ import com.facebook.react.bridge.ReactContext
20
+ import com.facebook.react.bridge.ReadableArray
21
+ import com.facebook.react.bridge.WritableMap
22
+ import com.facebook.react.uimanager.SimpleViewManager
23
+ import com.facebook.react.uimanager.ThemedReactContext
24
+ import com.facebook.react.uimanager.UIManagerHelper
25
+ import com.facebook.react.uimanager.annotations.ReactProp
26
+ import com.facebook.react.uimanager.events.Event
27
+ import kotlin.math.roundToInt
28
+
29
+ /**
30
+ * ReactNative view manager for the full-screen media viewer's video cells.
31
+ *
32
+ * This is a rewrite that mirrors the proven [AspectRatioVideoView] used by the native
33
+ * auto-preview after recording. The previous implementation drove an
34
+ * [android.widget.VideoView] and computed its contain-box from `mp.videoWidth/videoHeight`
35
+ * — the clip's *coded* (pre-rotation) dimensions — with no rotation correction, so any
36
+ * portrait clip carrying 90/270 rotation metadata was laid out with a landscape aspect box
37
+ * and rendered cropped/stretched. It also never reset its cached dimensions when `src`
38
+ * changed on a recycled FlatList cell, so a reused view briefly laid out (and showed) the
39
+ * previous clip.
40
+ *
41
+ * The rewrite:
42
+ * - Renders through a [TextureView] + [MediaPlayer] so MediaPlayer applies the clip's
43
+ * rotation and hands us the fully-rotated frame.
44
+ * - Derives the aspect box from ROTATION-CORRECTED display dimensions (extracted via
45
+ * [MediaMetadataRetriever], swapping W/H for 90/270), never from the coded buffer.
46
+ * - Fully resets cached dimensions and releases the player on every `src` change, with a
47
+ * same-src guard so an unchanged rebind is a no-op — a recycled cell can never show or
48
+ * lay out with the previous clip.
49
+ * - Applies a deterministic aspect-fit (contain) transform and re-applies the caller's
50
+ * paused state once the player is prepared.
51
+ */
52
+ class NativeVideoViewManager : SimpleViewManager<AspectFitVideoContainer>() {
53
+ override fun getName() = "NativeCustomVideoView"
54
+
55
+ override fun createViewInstance(reactContext: ThemedReactContext): AspectFitVideoContainer {
56
+ return AspectFitVideoContainer(reactContext)
57
+ }
58
+
59
+ override fun onDropViewInstance(view: AspectFitVideoContainer) {
60
+ view.stopPlayback()
61
+ super.onDropViewInstance(view)
62
+ }
63
+
64
+ override fun getCommandsMap(): MutableMap<String, Int> = mutableMapOf("seekTo" to 1)
65
+
66
+ override fun receiveCommand(view: AspectFitVideoContainer, commandId: Int, args: ReadableArray?) {
67
+ when (commandId) {
68
+ 1 -> view.seekTo(args?.getInt(0) ?: 0)
69
+ }
70
+ }
71
+
72
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
73
+ val constants: MutableMap<String, Any> =
74
+ super.getExportedCustomDirectEventTypeConstants() ?: mutableMapOf()
75
+ constants["topPlaybackProgress"] = mutableMapOf("registrationName" to "onPlaybackProgress")
76
+ constants["topPlaybackEnded"] = mutableMapOf("registrationName" to "onPlaybackEnded")
77
+ return constants
78
+ }
79
+
80
+ @ReactProp(name = "src")
81
+ fun setSrc(view: AspectFitVideoContainer, src: String?) {
82
+ view.setSource(src)
83
+ }
84
+
85
+ @ReactProp(name = "paused")
86
+ fun setPaused(view: AspectFitVideoContainer, paused: Boolean) {
87
+ view.setPaused(paused)
88
+ }
89
+
90
+ /**
91
+ * Whether this slide is the currently-visible page of the full-screen viewer. The paging
92
+ * host sets it per cell so exactly one clip is ever "active". Distinct from `paused` (the
93
+ * user's manual play/pause of the visible clip): deactivating a slide fully releases its
94
+ * player and resets it to 00:00, so returning to it replays from the start rather than
95
+ * resuming, and only the visible clip ever holds a decoder.
96
+ *
97
+ * Defaults to true so single-video consumers that never set it (e.g. the editing screen)
98
+ * keep their existing behaviour.
99
+ */
100
+ @ReactProp(name = "active", defaultBoolean = true)
101
+ fun setActive(view: AspectFitVideoContainer, active: Boolean) {
102
+ view.setActive(active)
103
+ }
104
+ }
105
+
106
+ private class VideoPlaybackEvent(
107
+ surfaceId: Int,
108
+ viewId: Int,
109
+ private val name: String,
110
+ private val payload: WritableMap
111
+ ) : Event<VideoPlaybackEvent>(surfaceId, viewId) {
112
+ override fun getEventName() = name
113
+ override fun getEventData(): WritableMap = payload
114
+ override fun canCoalesce(): Boolean = false
115
+ }
116
+
117
+ /**
118
+ * Container that renders the fitted video frame on a PURE BLACK (`#000000`) surface so the
119
+ * clip is clearly distinguishable from the viewer's dark background, which the video would
120
+ * otherwise blend into (both are near-identical dark colors).
121
+ *
122
+ * It hosts two children, both centred:
123
+ * - a black [matte] View sized to the clip's exact aspect-fit rectangle, sitting behind the
124
+ * video, and
125
+ * - a non-opaque [AspectFitVideoView] (TextureView) filling the container, which draws the
126
+ * rotation-corrected, aspect-fit frame.
127
+ *
128
+ * Because the TextureView is non-opaque and the container itself is transparent, everything
129
+ * OUTSIDE the fitted rectangle stays transparent — the surrounding viewer background shows
130
+ * through unchanged. Only the video's own rectangle is backed by black. The video is never
131
+ * cropped, stretched, distorted, or rescaled: the matte merely matches the fit box the video
132
+ * already computes, exactly mirroring the black-container treatment the native auto-preview
133
+ * uses. All playback, controls, gestures, and navigation are unaffected.
134
+ */
135
+ class AspectFitVideoContainer(context: Context) : FrameLayout(context) {
136
+ // Backing matte sized to the video's fitted rectangle (stock: pure black). Hidden until
137
+ // the fit is known so nothing is drawn before the clip's dimensions are available.
138
+ private val matte = View(context).apply {
139
+ setBackgroundColor(
140
+ CameraTheme.get(context).componentColor("videoView", "matteBackground", Color.BLACK)
141
+ )
142
+ visibility = View.GONE
143
+ }
144
+
145
+ private val videoView = AspectFitVideoView(context).apply {
146
+ // Let the letterbox region stay transparent so the viewer background remains visible
147
+ // around the fitted video instead of painting the whole cell.
148
+ isOpaque = false
149
+ onFittedSizeChanged = { fittedW, fittedH -> updateMatte(fittedW, fittedH) }
150
+ }
151
+
152
+ fun seekTo(positionMs: Int) = videoView.seekTo(positionMs)
153
+
154
+ // Instant poster shown OVER the TextureView while the decoder prepares. The TextureView is
155
+ // deliberately masked (alpha 0) until MediaPlayer prepares and renders frame 0, which takes
156
+ // on the order of seconds — without a poster the slide is a blank rectangle for that whole
157
+ // time. The poster bitmap comes from the same background MediaMetadataRetriever pass that
158
+ // probes the clip's display dimensions (no extra file open), is FIT_CENTER like the video's
159
+ // own contain transform so it lands in the same rectangle, and is faded out only once a REAL
160
+ // decoded frame is flowing (onFirstFrame) — then re-shown whenever the surface is masked
161
+ // again (source swap / deactivation), so a blank frame is never visible.
162
+ private val poster = android.widget.ImageView(context).apply {
163
+ scaleType = android.widget.ImageView.ScaleType.FIT_CENTER
164
+ visibility = View.GONE
165
+ }
166
+
167
+ init {
168
+ // Matte first (behind), then the video, then the poster on top; all centred.
169
+ addView(
170
+ matte,
171
+ LayoutParams(0, 0, Gravity.CENTER)
172
+ )
173
+ addView(
174
+ videoView,
175
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER)
176
+ )
177
+ addView(
178
+ poster,
179
+ LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER)
180
+ )
181
+ videoView.onPosterFrame = { bitmap ->
182
+ poster.setImageBitmap(bitmap)
183
+ // Only reveal if the decoder hasn't already beaten the probe to a real frame.
184
+ if (!videoView.hasRenderedFrame()) showPoster()
185
+ }
186
+ videoView.onFirstFrame = {
187
+ // A real decoded frame is on the surface — retire the poster with a short cross-fade.
188
+ poster.animate().alpha(0f).setDuration(120).withEndAction {
189
+ poster.visibility = View.GONE
190
+ }.start()
191
+ }
192
+ videoView.onMasked = {
193
+ // Surface hidden (source swap or slide deactivated): fall back to the poster instantly
194
+ // so the cell shows the clip's content, never a blank/black rectangle.
195
+ showPoster()
196
+ }
197
+ // The inner TextureView is an untagged child, so it can't dispatch events to JS itself.
198
+ // It hands progress/ended values here and this container dispatches them with ITS React tag
199
+ // (the one RN assigned in createViewInstance) — the only tag the JS <NativeCustomVideoView>
200
+ // actually listens on.
201
+ videoView.onProgress = { currentTimeMs, durationMs, isPlaying ->
202
+ dispatchProgress(currentTimeMs, durationMs, isPlaying)
203
+ }
204
+ videoView.onEnded = { durationMs ->
205
+ dispatchEnded(durationMs)
206
+ }
207
+ }
208
+
209
+ private fun dispatchProgress(currentTimeMs: Int, durationMs: Int, isPlaying: Boolean) {
210
+ val reactContext = context as? ReactContext ?: return
211
+ // `id` here is THIS container's React tag (set by RN after createViewInstance), so the event
212
+ // reaches the mounted <NativeCustomVideoView> and its onPlaybackProgress handler fires.
213
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return
214
+ val payload = Arguments.createMap().apply {
215
+ putInt("currentTimeMs", currentTimeMs)
216
+ putInt("durationMs", durationMs)
217
+ putBoolean("isPlaying", isPlaying)
218
+ }
219
+ dispatcher.dispatchEvent(
220
+ VideoPlaybackEvent(UIManagerHelper.getSurfaceId(reactContext), id, "topPlaybackProgress", payload)
221
+ )
222
+ }
223
+
224
+ private fun dispatchEnded(durationMs: Int) {
225
+ val reactContext = context as? ReactContext ?: return
226
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) ?: return
227
+ val payload = Arguments.createMap().apply {
228
+ putInt("durationMs", durationMs)
229
+ }
230
+ dispatcher.dispatchEvent(
231
+ VideoPlaybackEvent(UIManagerHelper.getSurfaceId(reactContext), id, "topPlaybackEnded", payload)
232
+ )
233
+ }
234
+
235
+ private fun showPoster() {
236
+ if (poster.drawable == null) return
237
+ poster.animate().cancel()
238
+ poster.alpha = 1f
239
+ poster.visibility = View.VISIBLE
240
+ }
241
+
242
+ /** Resize/centre the black matte to exactly cover the video's current aspect-fit rectangle. */
243
+ private fun updateMatte(fittedW: Int, fittedH: Int) {
244
+ if (fittedW <= 0 || fittedH <= 0) {
245
+ matte.visibility = View.GONE
246
+ return
247
+ }
248
+ val lp = matte.layoutParams as LayoutParams
249
+ if (lp.width != fittedW || lp.height != fittedH) {
250
+ lp.width = fittedW
251
+ lp.height = fittedH
252
+ lp.gravity = Gravity.CENTER
253
+ matte.layoutParams = lp
254
+ }
255
+ matte.visibility = View.VISIBLE
256
+ }
257
+
258
+ // Mirrors the video view's same-src guard: a FlatList rebind that didn't change the clip
259
+ // must not clear the poster (setSource below would no-op and never restore it).
260
+ private var lastSrc: String? = null
261
+
262
+ fun setSource(src: String?) {
263
+ if (src != lastSrc) {
264
+ lastSrc = src
265
+ // Clear the previous clip's poster before binding the new one so a recycled cell can
266
+ // never flash the old clip's frame. A cached poster for the new clip re-shows instantly
267
+ // from inside setSource's probe (or the cache hit below).
268
+ poster.animate().cancel()
269
+ poster.setImageBitmap(null)
270
+ poster.visibility = View.GONE
271
+ }
272
+ videoView.setSource(src)
273
+ }
274
+
275
+ fun setPaused(paused: Boolean) = videoView.setPaused(paused)
276
+
277
+ fun setActive(active: Boolean) = videoView.setActive(active)
278
+
279
+ fun stopPlayback() = videoView.stopPlayback()
280
+ }
281
+
282
+ /**
283
+ * A [TextureView] that renders one clip at a time with aspect-fit (contain) scaling and
284
+ * rotation-corrected dimensions. Self-contained so it can be driven purely by the `src` and
285
+ * `paused` props coming from ReactNative.
286
+ */
287
+ class AspectFitVideoView(context: Context) : TextureView(context) {
288
+ private var mediaPlayer: MediaPlayer? = null
289
+ private var surface: Surface? = null
290
+ private val progressHandler = Handler(Looper.getMainLooper())
291
+ private var progressRunnable: Runnable? = null
292
+
293
+ // The clip currently bound to this view. Used as a same-src guard so re-binding the same
294
+ // uri (e.g. a FlatList rebind that didn't actually change the item) is a no-op.
295
+ private var currentUri: Uri? = null
296
+ private var pendingUri: Uri? = null
297
+ private var pendingSeekMs: Int? = null
298
+
299
+ // Rotation-corrected display dimensions of the current clip. Reset to 0 on every source
300
+ // change so a recycled view never lays out with the previous clip's aspect ratio.
301
+ private var displayW = 0
302
+ private var displayH = 0
303
+
304
+ // Desired playback state from the `paused` prop. Re-applied once the player is prepared,
305
+ // since start()/pause() are no-ops before then.
306
+ private var paused = true
307
+
308
+ // True once the clip has reached its end (natural completion or the progress monitor hitting
309
+ // the duration). While set, the underlying MediaPlayer is in (or effectively at) the
310
+ // PlaybackCompleted / paused-at-end state, where a plain start() is unreliable across OEMs
311
+ // (the exact symptom the JS replay tap hit: play at the end did nothing). start() consults this
312
+ // flag to restart deterministically by seeking first, then playing. Cleared whenever playback
313
+ // actually (re)starts or a fresh clip is opened.
314
+ private var completed = false
315
+
316
+ // Whether this slide is the currently-visible viewer page (the `active` prop). A player
317
+ // is created ONLY while active; deactivating releases it (freeing the decoder) and resets
318
+ // to 00:00, so re-activating replays from the start. Defaults to true so consumers that
319
+ // never set the prop (single-video editing screen) behave exactly as before.
320
+ private var active = true
321
+
322
+ private val mainHandler = Handler(Looper.getMainLooper())
323
+
324
+ // Monotonic token identifying the current source. A background dimension probe stamps the
325
+ // token it was launched for and only applies its result if the view still shows that clip.
326
+ private var sourceToken = 0
327
+
328
+ // Reports the video's current aspect-fit rectangle (in px, centred within this view) each
329
+ // time the transform is recomputed, so a host container can size a matching black backing.
330
+ var onFittedSizeChanged: ((Int, Int) -> Unit)? = null
331
+
332
+ // Poster pipeline (see AspectFitVideoContainer): a decoded frame of the CURRENT clip,
333
+ // delivered as soon as it is available (from cache: synchronously inside setSource).
334
+ var onPosterFrame: ((android.graphics.Bitmap) -> Unit)? = null
335
+
336
+ // Fired once per bound clip, the first time a REAL decoded frame reaches the surface.
337
+ var onFirstFrame: (() -> Unit)? = null
338
+
339
+ // Fired whenever the surface is masked (source swap / deactivation / player release), so
340
+ // the host can fall back to the poster instead of showing a blank rectangle.
341
+ var onMasked: (() -> Unit)? = null
342
+
343
+ // Whether the current clip has pushed at least one real frame to the surface.
344
+ private var renderedFrame = false
345
+ fun hasRenderedFrame(): Boolean = renderedFrame
346
+
347
+ // Last fitted size dispatched, so we only notify on an actual change.
348
+ private var lastFittedW = -1
349
+ private var lastFittedH = -1
350
+
351
+ init {
352
+ surfaceTextureListener = object : TextureView.SurfaceTextureListener {
353
+ override fun onSurfaceTextureAvailable(st: SurfaceTexture, width: Int, height: Int) {
354
+ surface = Surface(st)
355
+ updatePlaybackState()
356
+ applyAspectTransform()
357
+ }
358
+
359
+ override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, width: Int, height: Int) {
360
+ applyAspectTransform()
361
+ }
362
+
363
+ override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean {
364
+ release()
365
+ surface?.release()
366
+ surface = null
367
+ // The surface's pixels are gone with it — restore the poster so re-attaching (e.g.
368
+ // returning to this screen) shows the clip's content instead of a blank rectangle.
369
+ renderedFrame = false
370
+ onMasked?.invoke()
371
+ return true
372
+ }
373
+
374
+ override fun onSurfaceTextureUpdated(st: SurfaceTexture) {
375
+ // Backup unmask: once real frames are flowing the view must be visible, even if the
376
+ // prepared/error callbacks were missed for any reason. Cheap and idempotent.
377
+ if (alpha != 1f) alpha = 1f
378
+ if (!renderedFrame) {
379
+ renderedFrame = true
380
+ onFirstFrame?.invoke()
381
+ }
382
+ }
383
+ }
384
+ }
385
+
386
+ /** Bind (or rebind) the clip to display. No-op when the uri is unchanged. */
387
+ fun setSource(src: String?) {
388
+ val uri = src?.let { Uri.parse(it) }
389
+ if (uri == currentUri) return
390
+
391
+ currentUri = uri
392
+ pendingUri = uri
393
+ pendingSeekMs = null
394
+
395
+ // Full reset: forget the previous clip's aspect box and tear down its player so nothing
396
+ // from the old clip can be shown or laid out while the new one loads.
397
+ displayW = 0
398
+ displayH = 0
399
+ renderedFrame = false
400
+ // Clear the fitted rectangle too so a recycled cell never shows the previous clip's black
401
+ // backing before the new clip's dimensions arrive.
402
+ lastFittedW = -1
403
+ lastFittedH = -1
404
+ onFittedSizeChanged?.invoke(0, 0)
405
+ sourceToken++
406
+ // Hide the surface across the swap so a recycled cell can never show the OLD clip's last
407
+ // frame while the new one loads (or, if this cell is inactive, indefinitely). The fresh
408
+ // player's onPreparedListener re-reveals it; an inactive cell simply stays masked.
409
+ alpha = 0f
410
+ release()
411
+
412
+ if (uri == null) {
413
+ // A cleared source can never have a live player; reconcile so any existing one is
414
+ // released and the frame is hidden.
415
+ updatePlaybackState()
416
+ return
417
+ }
418
+
419
+ // Poster fast path: a clip seen before shows its cached poster frame synchronously, so
420
+ // paging back to it is instant with no blank gap at all.
421
+ posterCache.get(uri.toString())?.let { cached ->
422
+ onPosterFrame?.invoke(cached)
423
+ onMasked?.invoke()
424
+ }
425
+
426
+ probeDisplayDimensions(uri, sourceToken)
427
+ // Reconcile: opens the player now if this slide is active and the surface is ready,
428
+ // otherwise onSurfaceTextureAvailable()/setActive() will when their preconditions are met.
429
+ updatePlaybackState()
430
+ }
431
+
432
+ fun setPaused(paused: Boolean) {
433
+ this.paused = paused
434
+ if (paused) {
435
+ stopProgressMonitoring()
436
+ }
437
+ updatePlaybackState()
438
+ if (!paused) {
439
+ startProgressMonitoring()
440
+ }
441
+ }
442
+
443
+ fun setActive(active: Boolean) {
444
+ if (this.active == active) return
445
+ this.active = active
446
+ if (!paused) {
447
+ startProgressMonitoring()
448
+ }
449
+ updatePlaybackState()
450
+ }
451
+
452
+ /**
453
+ * Single source of truth for the player lifecycle. Enforces the invariant:
454
+ * a live MediaPlayer exists ⇔ active && surface != null && pendingUri != null
455
+ * and it plays ⇔ the above AND !paused
456
+ *
457
+ * Because deactivating releases the player and re-activating creates a fresh one (prepared
458
+ * at position 0), a returned-to clip always replays from 00:00 rather than resuming, and at
459
+ * most one decoder is ever alive across all slides — the low-end win. Order-independent, so
460
+ * every trigger (src / paused / active / surface) can just call this.
461
+ */
462
+ private fun updatePlaybackState() {
463
+ val shouldHavePlayer = active && surface != null && pendingUri != null
464
+ if (shouldHavePlayer) {
465
+ if (mediaPlayer == null) {
466
+ // Create fresh (starts at 00:00). onPreparedListener auto-starts iff !paused.
467
+ openVideo()
468
+ } else {
469
+ // Player already live for this clip — just honour the manual paused state.
470
+ if (paused) pause() else start()
471
+ }
472
+ } else if (mediaPlayer != null) {
473
+ // Not the visible clip (or no surface/uri): free the decoder and reset to 00:00. Mask
474
+ // the frame so the next activation can't flash the last-shown frame before it reloads.
475
+ alpha = 0f
476
+ renderedFrame = false
477
+ onMasked?.invoke()
478
+ release()
479
+ }
480
+ }
481
+
482
+ // Playback events are dispatched to JS by the CONTAINER, not here. This inner TextureView is an
483
+ // untagged child (createViewInstance returns AspectFitVideoContainer, so RN only ever sets a
484
+ // React tag on the container). Dispatching from here used this view's `id`, which is
485
+ // View.NO_ID (-1) — a tag no mounted React view owns — so every progress/ended event was
486
+ // silently dropped and the JS seek bar / timer never advanced. Forward the values to the
487
+ // container via these callbacks and let it dispatch with its own (valid) tag.
488
+ var onProgress: ((currentTimeMs: Int, durationMs: Int, isPlaying: Boolean) -> Unit)? = null
489
+ var onEnded: ((durationMs: Int) -> Unit)? = null
490
+
491
+ private fun emitProgress(currentTimeMs: Int, durationMs: Int) {
492
+ onProgress?.invoke(currentTimeMs, durationMs, !paused && mediaPlayer?.isPlaying == true)
493
+ }
494
+
495
+ private fun emitEnded(durationMs: Int) {
496
+ onEnded?.invoke(durationMs)
497
+ }
498
+
499
+ private fun startProgressMonitoring() {
500
+ if (progressRunnable != null) return
501
+ progressRunnable = object : Runnable {
502
+ override fun run() {
503
+ val mp = mediaPlayer ?: run {
504
+ progressRunnable = null
505
+ return
506
+ }
507
+ if (!active || paused) {
508
+ progressRunnable = null
509
+ return
510
+ }
511
+ try {
512
+ val current = mp.currentPosition
513
+ val duration = mp.duration
514
+ if (duration > 0) {
515
+ emitProgress(current.coerceIn(0, duration), duration)
516
+ if (current >= duration) {
517
+ mp.pause()
518
+ paused = true
519
+ // Mark end-of-clip so the next play restarts from the beginning deterministically,
520
+ // and drop any stale pending seek so a plain replay tap lands at 0 (not a leftover
521
+ // target). A manual seek after the end re-sets pendingSeekMs and is honoured.
522
+ completed = true
523
+ pendingSeekMs = null
524
+ emitEnded(duration)
525
+ progressRunnable = null
526
+ return
527
+ }
528
+ }
529
+ } catch (_: IllegalStateException) {
530
+ // Player can be mid-release or unprepared — stop the loop until next state change.
531
+ progressRunnable = null
532
+ return
533
+ }
534
+ progressHandler.postDelayed(this, 200L)
535
+ }
536
+ }
537
+ progressHandler.post(progressRunnable!!)
538
+ }
539
+
540
+ private fun stopProgressMonitoring() {
541
+ progressRunnable?.let { progressHandler.removeCallbacks(it) }
542
+ progressRunnable = null
543
+ }
544
+
545
+ /**
546
+ * Extract rotation-corrected display dimensions AND a poster frame off the main thread in
547
+ * ONE retriever pass (mirrors `readDimensions`/`extractVideoMetadata`: swap W/H for 90/270
548
+ * rotation) and apply them if the view still shows this clip. The dimensions give a correct
549
+ * aspect box even before/independent of MediaPlayer's version-specific size reporting; the
550
+ * poster covers the seconds-long gap until the decoder renders its first real frame.
551
+ */
552
+ private fun probeDisplayDimensions(uri: Uri, token: Int) {
553
+ Thread {
554
+ var poster: android.graphics.Bitmap? = null
555
+ val (w, h) = runCatching {
556
+ val retriever = MediaMetadataRetriever()
557
+ try {
558
+ retriever.setDataSource(context, uri)
559
+ var w = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull()
560
+ var h = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull()
561
+ val rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
562
+ if (rotation == 90 || rotation == 270) {
563
+ val temp = w
564
+ w = h
565
+ h = temp
566
+ }
567
+ // Poster: best-effort ONLY (getFrameAtTime returns null for some clips/URIs — the
568
+ // decoder's own frame remains authoritative; see MEMORY video-preview-poster-decoder).
569
+ // Skip when a cached poster already fired from setSource.
570
+ if (posterCache.get(uri.toString()) == null) {
571
+ // Ask the decoder for a screen-sized frame where the platform supports it (API 27+)
572
+ // instead of taking the coded frame and shrinking it below: a 4K clip's coded frame is
573
+ // a ~33 MB allocation that exists only to be discarded, and posters are produced right
574
+ // after a recording, when the heap is already carrying the capture pipeline.
575
+ val target = context.resources.displayMetrics
576
+ .let { maxOf(it.widthPixels, it.heightPixels) }
577
+ poster = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && target > 0) {
578
+ runCatching {
579
+ retriever.getScaledFrameAtTime(
580
+ 0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, target, target
581
+ ) ?: retriever.getScaledFrameAtTime(
582
+ 1_000L, MediaMetadataRetriever.OPTION_CLOSEST, target, target
583
+ )
584
+ }.getOrNull()
585
+ } else {
586
+ retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
587
+ ?: retriever.getFrameAtTime(1_000L, MediaMetadataRetriever.OPTION_CLOSEST)
588
+ }
589
+ }
590
+ Pair(w, h)
591
+ } finally {
592
+ retriever.release()
593
+ }
594
+ }.getOrDefault(Pair(null, null))
595
+
596
+ // Frames come out at coded size; downscale to roughly the screen so a 4K clip's poster
597
+ // doesn't hold a ~33 MB bitmap in the cache. The ImageView scales it to the fit box.
598
+ val scaledPoster = poster?.let { raw ->
599
+ val target = context.resources.displayMetrics.let { maxOf(it.widthPixels, it.heightPixels) }
600
+ val maxDim = maxOf(raw.width, raw.height)
601
+ if (maxDim > target && maxDim > 0) {
602
+ val scale = target.toFloat() / maxDim
603
+ val scaled = android.graphics.Bitmap.createScaledBitmap(
604
+ raw,
605
+ (raw.width * scale).toInt().coerceAtLeast(1),
606
+ (raw.height * scale).toInt().coerceAtLeast(1),
607
+ true
608
+ )
609
+ if (scaled !== raw) raw.recycle()
610
+ scaled
611
+ } else raw
612
+ }
613
+
614
+ mainHandler.post {
615
+ // Ignore a stale probe whose clip has since been replaced.
616
+ if (token != sourceToken) {
617
+ scaledPoster?.recycle()
618
+ return@post
619
+ }
620
+ if (w != null && h != null && w > 0 && h > 0) {
621
+ displayW = w
622
+ displayH = h
623
+ applyAspectTransform()
624
+ }
625
+ if (scaledPoster != null) {
626
+ posterCache.put(uri.toString(), scaledPoster)
627
+ onPosterFrame?.invoke(scaledPoster)
628
+ }
629
+ }
630
+ }.start()
631
+ }
632
+
633
+ private fun openVideo() {
634
+ val uri = pendingUri ?: return
635
+ val surf = surface ?: return
636
+ release()
637
+ // A fresh player always begins at 00:00 and has not completed yet.
638
+ completed = false
639
+ // Hide the surface until the fresh player renders its first frame, so a recycled/returned
640
+ // cell can never briefly show the previous clip's last frame. The black matte shows
641
+ // through meanwhile, giving a clean freeze at 00:00.
642
+ alpha = 0f
643
+ mediaPlayer = MediaPlayer().apply {
644
+ setSurface(surf)
645
+ isLooping = false
646
+ setOnVideoSizeChangedListener { _, w, h ->
647
+ // Fall back to the player-reported size only when the metadata probe hasn't yet
648
+ // supplied rotation-corrected dimensions, so the transform still has an aspect.
649
+ if ((displayW <= 0 || displayH <= 0) && w > 0 && h > 0) {
650
+ displayW = w
651
+ displayH = h
652
+ }
653
+ applyAspectTransform()
654
+ }
655
+ setOnCompletionListener {
656
+ paused = true
657
+ // See the progress-monitor end branch: flag the completed state and forget any stale
658
+ // pending seek so the next play cleanly restarts from 0 (a manual post-end seek overrides).
659
+ completed = true
660
+ pendingSeekMs = null
661
+ stopProgressMonitoring()
662
+ val duration = try { duration } catch (_: IllegalStateException) { 0 }
663
+ if (duration > 0) {
664
+ emitProgress(duration, duration)
665
+ emitEnded(duration)
666
+ }
667
+ }
668
+ setOnPreparedListener { mp ->
669
+ mp.isLooping = false
670
+ applyAspectTransform()
671
+ // The player is prepared at 00:00 and ready to render — reveal the surface.
672
+ alpha = 1f
673
+ // Publish the real clip duration to JS the instant we're prepared, BEFORE the pending
674
+ // seek / auto-start below. The full-screen viewer opens every clip PAUSED on its poster,
675
+ // and progress is only emitted while PLAYING (startProgressMonitoring) — so without this
676
+ // the JS player never learned the clip length until the first play: the total-time
677
+ // readout sat at 0:00 and the seek bar had no scale to drag against (a paused scrub mapped
678
+ // to duration 0 → a no-op). A subsequent seekTo / progress tick refines the position.
679
+ val preparedDuration = try { mp.duration } catch (_: IllegalStateException) { 0 }
680
+ if (preparedDuration > 0) {
681
+ emitProgress(mp.currentPosition.coerceIn(0, preparedDuration), preparedDuration)
682
+ }
683
+ val targetSeek = pendingSeekMs
684
+ pendingSeekMs = null
685
+ if (targetSeek != null) {
686
+ seekTo(targetSeek)
687
+ }
688
+ // Re-apply the desired paused state now that start()/pause() will take effect.
689
+ if (!paused) {
690
+ try { mp.start(); startProgressMonitoring() } catch (e: IllegalStateException) {}
691
+ } else {
692
+ // Paused-at-open (poster mode): a prepared-but-never-started player renders
693
+ // NOTHING on many devices, leaving the slide on the black matte. Seeking to 0
694
+ // forces the decoder to push frame 0 to the surface — a true full-resolution
695
+ // poster, aspect-fitted by the same transform as playback, with no separate
696
+ // thumbnail pipeline. SEEK_CLOSEST (API 26+) decodes the exact frame instead
697
+ // of the nearest sync frame.
698
+ try {
699
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
700
+ mp.seekTo(0L, MediaPlayer.SEEK_CLOSEST)
701
+ } else {
702
+ mp.seekTo(0)
703
+ }
704
+ } catch (e: IllegalStateException) {}
705
+ }
706
+ }
707
+ setOnErrorListener { _, _, _ ->
708
+ // Never leave the view stuck hidden if preparation failed.
709
+ alpha = 1f
710
+ true
711
+ }
712
+ try {
713
+ setDataSource(context, uri)
714
+ prepareAsync()
715
+ } catch (e: Exception) {
716
+ // Best effort — leave the view blank rather than crash the viewer.
717
+ }
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Scale the (rotation-applied) frame MediaPlayer stretches across the whole SurfaceTexture
723
+ * back down to the clip's true display aspect ratio, centred and fitted inside the view.
724
+ * Aspect-preserving FIT (contain): the entire frame is always visible, never cropped,
725
+ * never stretched.
726
+ */
727
+ private fun applyAspectTransform() {
728
+ val vw = width
729
+ val vh = height
730
+ if (vw <= 0 || vh <= 0 || displayW <= 0 || displayH <= 0) return
731
+
732
+ val videoAspect = displayW.toFloat() / displayH.toFloat()
733
+ val viewAspect = vw.toFloat() / vh.toFloat()
734
+
735
+ val scaleX: Float
736
+ val scaleY: Float
737
+ if (videoAspect > viewAspect) {
738
+ // Clip is wider than the view: span the full width, shrink the height to fit.
739
+ scaleX = 1f
740
+ scaleY = viewAspect / videoAspect
741
+ } else {
742
+ // Clip is taller than (or equal to) the view: span the full height, shrink width.
743
+ scaleX = videoAspect / viewAspect
744
+ scaleY = 1f
745
+ }
746
+
747
+ val matrix = Matrix()
748
+ matrix.setScale(scaleX, scaleY, vw / 2f, vh / 2f)
749
+ setTransform(matrix)
750
+ invalidate()
751
+
752
+ // Publish the fitted rectangle (the visible video box) so a host can back it with a
753
+ // black surface sized exactly to the video — never larger, so surrounding background
754
+ // stays visible.
755
+ val fittedW = (vw * scaleX).roundToInt()
756
+ val fittedH = (vh * scaleY).roundToInt()
757
+ if (fittedW != lastFittedW || fittedH != lastFittedH) {
758
+ lastFittedW = fittedW
759
+ lastFittedH = fittedH
760
+ onFittedSizeChanged?.invoke(fittedW, fittedH)
761
+ }
762
+ }
763
+
764
+ fun seekTo(positionMs: Int) {
765
+ val clamped = positionMs.coerceAtLeast(0)
766
+ pendingSeekMs = clamped
767
+ val mp = mediaPlayer ?: return
768
+ try {
769
+ val duration = mp.duration
770
+ val target = if (duration > 0) clamped.coerceAtMost(duration) else clamped
771
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
772
+ mp.seekTo(target.toLong(), MediaPlayer.SEEK_CLOSEST)
773
+ } else {
774
+ mp.seekTo(target)
775
+ }
776
+ emitProgress(mp.currentPosition.coerceAtMost(duration), duration)
777
+ } catch (_: IllegalStateException) {
778
+ // Player may be preparing or already released — will be re-applied from `pendingSeekMs`.
779
+ }
780
+ }
781
+
782
+ private fun start() {
783
+ val mp = mediaPlayer ?: return
784
+ try {
785
+ if (completed) {
786
+ // The clip is sitting at its end (PlaybackCompleted, or paused-at-duration). A plain
787
+ // start() from here is unreliable across devices — on some it does nothing, which is the
788
+ // exact "play at the end does nothing" bug. Restart deterministically: seek FIRST, then
789
+ // play. Honour a pending seek target (a manual seek made after the clip ended) over 0, so
790
+ // "seek then play" continues from that position instead of the beginning.
791
+ val duration = try { mp.duration } catch (_: IllegalStateException) { 0 }
792
+ val target = (pendingSeekMs ?: 0).let { if (duration > 0) it.coerceIn(0, duration) else it.coerceAtLeast(0) }
793
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
794
+ mp.seekTo(target.toLong(), MediaPlayer.SEEK_CLOSEST)
795
+ } else {
796
+ mp.seekTo(target)
797
+ }
798
+ pendingSeekMs = null
799
+ // Reflect the restart position immediately so JS isn't briefly showing the end frame.
800
+ if (duration > 0) emitProgress(target.coerceIn(0, duration), duration)
801
+ }
802
+ mp.start()
803
+ completed = false
804
+ startProgressMonitoring()
805
+ } catch (e: IllegalStateException) {
806
+ // Player not ready / released — onPreparedListener re-applies playback.
807
+ }
808
+ }
809
+
810
+ private fun pause() {
811
+ try {
812
+ if (mediaPlayer?.isPlaying == true) mediaPlayer?.pause()
813
+ } catch (e: IllegalStateException) {
814
+ // Player not ready / released — nothing to pause.
815
+ }
816
+ stopProgressMonitoring()
817
+ }
818
+
819
+ fun stopPlayback() {
820
+ stopProgressMonitoring()
821
+ release()
822
+ }
823
+
824
+ private fun release() {
825
+ stopProgressMonitoring()
826
+ mediaPlayer?.let { mp ->
827
+ try { mp.stop() } catch (e: IllegalStateException) {}
828
+ try { mp.reset() } catch (e: Exception) {}
829
+ mp.release()
830
+ }
831
+ mediaPlayer = null
832
+ }
833
+
834
+ companion object {
835
+ // Small process-wide poster cache keyed by uri, shared across recycled viewer cells so
836
+ // paging back to a clip shows its poster synchronously. Screen-sized RGB bitmaps at
837
+ // ~4 slides' worth: bounded, and evicted purely by LRU.
838
+ private val posterCache = object : android.util.LruCache<String, android.graphics.Bitmap>(
839
+ (Runtime.getRuntime().maxMemory() / 1024L / 16L)
840
+ .coerceIn(4 * 1024L, 32 * 1024L).toInt()
841
+ ) {
842
+ override fun sizeOf(key: String, value: android.graphics.Bitmap): Int = value.byteCount / 1024
843
+ }
844
+ }
845
+ }