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.
- package/CustomCamera.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +833 -0
- package/android/build.gradle +135 -0
- package/android/consumer-rules.pro +47 -0
- package/android/src/main/AndroidManifest.xml +72 -0
- package/android/src/main/java/android/print/PdfPrint.java +51 -0
- package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
- package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
- package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
- package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
- package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
- package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
- package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
- package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
- package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
- package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
- package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
- package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
- package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
- package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
- package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
- package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
- package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
- package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
- package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
- package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
- package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
- package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
- package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
- package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
- package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
- package/android/src/main/java/com/customcamera/Localization.kt +206 -0
- package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
- package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
- package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
- package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
- package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
- package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
- package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
- package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
- package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
- package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
- package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
- package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
- package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
- package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
- package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
- package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
- package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
- package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
- package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
- package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
- package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
- package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
- package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
- package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
- package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
- package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
- package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
- package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
- package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
- package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
- package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
- package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
- package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
- package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
- package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
- package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
- package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
- package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
- package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
- package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
- package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
- package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
- package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
- package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
- package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
- package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
- package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
- package/android/src/main/res/drawable/bg_pill.xml +6 -0
- package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
- package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
- package/android/src/main/res/drawable/bg_send_button.xml +5 -0
- package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
- package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
- package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
- package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
- package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
- package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
- package/android/src/main/res/drawable/ic_audio.xml +10 -0
- package/android/src/main/res/drawable/ic_back.xml +10 -0
- package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
- package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
- package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
- package/android/src/main/res/drawable/ic_check.xml +17 -0
- package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
- package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
- package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
- package/android/src/main/res/drawable/ic_close.xml +10 -0
- package/android/src/main/res/drawable/ic_cover.xml +9 -0
- package/android/src/main/res/drawable/ic_crop.xml +10 -0
- package/android/src/main/res/drawable/ic_delete.xml +9 -0
- package/android/src/main/res/drawable/ic_document.xml +10 -0
- package/android/src/main/res/drawable/ic_done.xml +10 -0
- package/android/src/main/res/drawable/ic_download.xml +10 -0
- package/android/src/main/res/drawable/ic_emoji.xml +10 -0
- package/android/src/main/res/drawable/ic_filter.xml +10 -0
- package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
- package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
- package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
- package/android/src/main/res/drawable/ic_flip.xml +15 -0
- package/android/src/main/res/drawable/ic_gallery.xml +10 -0
- package/android/src/main/res/drawable/ic_gif.xml +24 -0
- package/android/src/main/res/drawable/ic_lock.xml +19 -0
- package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
- package/android/src/main/res/drawable/ic_marker.xml +10 -0
- package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
- package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
- package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
- package/android/src/main/res/drawable/ic_pause.xml +10 -0
- package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
- package/android/src/main/res/drawable/ic_play.xml +10 -0
- package/android/src/main/res/drawable/ic_redo.xml +10 -0
- package/android/src/main/res/drawable/ic_rotate.xml +9 -0
- package/android/src/main/res/drawable/ic_send.xml +10 -0
- package/android/src/main/res/drawable/ic_share.xml +10 -0
- package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
- package/android/src/main/res/drawable/ic_sticker.xml +24 -0
- package/android/src/main/res/drawable/ic_text.xml +10 -0
- package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
- package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
- package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
- package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
- package/android/src/main/res/drawable/ic_trim.xml +23 -0
- package/android/src/main/res/drawable/ic_undo.xml +10 -0
- package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
- package/android/src/main/res/drawable/ic_video_note.xml +13 -0
- package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
- package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
- package/android/src/main/res/drawable/sticker_heart.xml +9 -0
- package/android/src/main/res/drawable/sticker_smile.xml +9 -0
- package/android/src/main/res/drawable/sticker_star.xml +9 -0
- package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
- package/android/src/main/res/values/colors.xml +34 -0
- package/android/src/main/res/values/ids.xml +15 -0
- package/android/src/main/res/values/strings.xml +15 -0
- package/android/src/main/res/values/styles.xml +20 -0
- package/android/src/main/res/xml/file_paths.xml +34 -0
- package/ios/CustomCamera.h +5 -0
- package/ios/CustomCamera.mm +214 -0
- package/lib/module/AudioPlayerContext.js +233 -0
- package/lib/module/AudioPlayerContext.js.map +1 -0
- package/lib/module/CameraPreview.js +304 -0
- package/lib/module/CameraPreview.js.map +1 -0
- package/lib/module/NativeAnimatedImageView.js +41 -0
- package/lib/module/NativeAnimatedImageView.js.map +1 -0
- package/lib/module/NativeAudioPlayerView.js +33 -0
- package/lib/module/NativeAudioPlayerView.js.map +1 -0
- package/lib/module/NativeBlurView.js +35 -0
- package/lib/module/NativeBlurView.js.map +1 -0
- package/lib/module/NativeCustomCamera.js +128 -0
- package/lib/module/NativeCustomCamera.js.map +1 -0
- package/lib/module/NativeCustomCamera.web.js +143 -0
- package/lib/module/NativeCustomCamera.web.js.map +1 -0
- package/lib/module/NativeCustomVideoView.js +5 -0
- package/lib/module/NativeCustomVideoView.js.map +1 -0
- package/lib/module/NativeDocumentPreviewView.js +156 -0
- package/lib/module/NativeDocumentPreviewView.js.map +1 -0
- package/lib/module/NativeMediaThumbnailView.js +110 -0
- package/lib/module/NativeMediaThumbnailView.js.map +1 -0
- package/lib/module/NativeSvgIcon.js +32 -0
- package/lib/module/NativeSvgIcon.js.map +1 -0
- package/lib/module/ResponsivePreviewContainer.js +171 -0
- package/lib/module/ResponsivePreviewContainer.js.map +1 -0
- package/lib/module/accessibility.js +61 -0
- package/lib/module/accessibility.js.map +1 -0
- package/lib/module/config.js +352 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/cpuOptimization.js +296 -0
- package/lib/module/cpuOptimization.js.map +1 -0
- package/lib/module/db/DatabaseManager.js +191 -0
- package/lib/module/db/DatabaseManager.js.map +1 -0
- package/lib/module/db/MediaMapper.js +284 -0
- package/lib/module/db/MediaMapper.js.map +1 -0
- package/lib/module/db/MediaRepository.js +242 -0
- package/lib/module/db/MediaRepository.js.map +1 -0
- package/lib/module/db/index.js +41 -0
- package/lib/module/db/index.js.map +1 -0
- package/lib/module/db/migrations.js +180 -0
- package/lib/module/db/migrations.js.map +1 -0
- package/lib/module/db/react-native-sqlcipher.d.js +2 -0
- package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
- package/lib/module/db/secureKey.js +87 -0
- package/lib/module/db/secureKey.js.map +1 -0
- package/lib/module/db/types.js +57 -0
- package/lib/module/db/types.js.map +1 -0
- package/lib/module/db/useMediaDatabase.js +123 -0
- package/lib/module/db/useMediaDatabase.js.map +1 -0
- package/lib/module/defaultIcons.js +2259 -0
- package/lib/module/defaultIcons.js.map +1 -0
- package/lib/module/deviceCapabilities.js +408 -0
- package/lib/module/deviceCapabilities.js.map +1 -0
- package/lib/module/errors.js +267 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/iconConfig.js +796 -0
- package/lib/module/iconConfig.js.map +1 -0
- package/lib/module/icons.js +514 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/index.js +251 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/jest-globals.d.js +2 -0
- package/lib/module/jest-globals.d.js.map +1 -0
- package/lib/module/library/MediaLibrary.js +107 -0
- package/lib/module/library/MediaLibrary.js.map +1 -0
- package/lib/module/library/MediaLibraryScreen.js +2597 -0
- package/lib/module/library/MediaLibraryScreen.js.map +1 -0
- package/lib/module/library/components/AudioPreview.js +74 -0
- package/lib/module/library/components/AudioPreview.js.map +1 -0
- package/lib/module/library/components/DocumentPreview.js +331 -0
- package/lib/module/library/components/DocumentPreview.js.map +1 -0
- package/lib/module/library/components/ExpandableFab.js +343 -0
- package/lib/module/library/components/ExpandableFab.js.map +1 -0
- package/lib/module/library/components/FileTiles.js +251 -0
- package/lib/module/library/components/FileTiles.js.map +1 -0
- package/lib/module/library/components/ImagePreview.js +70 -0
- package/lib/module/library/components/ImagePreview.js.map +1 -0
- package/lib/module/library/components/MediaMetadataHeader.js +175 -0
- package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
- package/lib/module/library/components/RecordAudioScreen.js +816 -0
- package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
- package/lib/module/library/components/VideoPreview.js +453 -0
- package/lib/module/library/components/VideoPreview.js.map +1 -0
- package/lib/module/library/components/ViewerCounter.js +55 -0
- package/lib/module/library/components/ViewerCounter.js.map +1 -0
- package/lib/module/library/compressionEstimate.js +507 -0
- package/lib/module/library/compressionEstimate.js.map +1 -0
- package/lib/module/library/fileMetadata.js +196 -0
- package/lib/module/library/fileMetadata.js.map +1 -0
- package/lib/module/library/index.js +42 -0
- package/lib/module/library/index.js.map +1 -0
- package/lib/module/library/libraryIconRegistry.js +69 -0
- package/lib/module/library/libraryIconRegistry.js.map +1 -0
- package/lib/module/library/libraryIcons.js +858 -0
- package/lib/module/library/libraryIcons.js.map +1 -0
- package/lib/module/library/localizedFormat.js +181 -0
- package/lib/module/library/localizedFormat.js.map +1 -0
- package/lib/module/library/palette.js +225 -0
- package/lib/module/library/palette.js.map +1 -0
- package/lib/module/library/responsive.js +53 -0
- package/lib/module/library/responsive.js.map +1 -0
- package/lib/module/locales.js +416 -0
- package/lib/module/locales.js.map +1 -0
- package/lib/module/localization.js +1064 -0
- package/lib/module/localization.js.map +1 -0
- package/lib/module/logger.js +20 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/lowMemoryCache.js +206 -0
- package/lib/module/lowMemoryCache.js.map +1 -0
- package/lib/module/mediaHistory.js +33 -0
- package/lib/module/mediaHistory.js.map +1 -0
- package/lib/module/mediaStorageService.js +402 -0
- package/lib/module/mediaStorageService.js.map +1 -0
- package/lib/module/memory.js +79 -0
- package/lib/module/memory.js.map +1 -0
- package/lib/module/nativeActions.js +307 -0
- package/lib/module/nativeActions.js.map +1 -0
- package/lib/module/nativeComponentSupport.js +61 -0
- package/lib/module/nativeComponentSupport.js.map +1 -0
- package/lib/module/networkPerformance.js +24 -0
- package/lib/module/networkPerformance.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/performance.js +104 -0
- package/lib/module/performance.js.map +1 -0
- package/lib/module/performanceMonitor.js +155 -0
- package/lib/module/performanceMonitor.js.map +1 -0
- package/lib/module/permissions.js +370 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/rendering.js +40 -0
- package/lib/module/rendering.js.map +1 -0
- package/lib/module/security.js +18 -0
- package/lib/module/security.js.map +1 -0
- package/lib/module/state.js +266 -0
- package/lib/module/state.js.map +1 -0
- package/lib/module/storage.js +400 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/theme.js +390 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils.js +115 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/module/validation.js +288 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
- package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
- package/lib/typescript/src/CameraPreview.d.ts +13 -0
- package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
- package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
- package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
- package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
- package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
- package/lib/typescript/src/NativeBlurView.d.ts +12 -0
- package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
- package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
- package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
- package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
- package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
- package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
- package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
- package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
- package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
- package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
- package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
- package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
- package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
- package/lib/typescript/src/accessibility.d.ts +20 -0
- package/lib/typescript/src/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +145 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/cpuOptimization.d.ts +63 -0
- package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
- package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
- package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
- package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
- package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
- package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
- package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
- package/lib/typescript/src/db/index.d.ts +23 -0
- package/lib/typescript/src/db/index.d.ts.map +1 -0
- package/lib/typescript/src/db/migrations.d.ts +31 -0
- package/lib/typescript/src/db/migrations.d.ts.map +1 -0
- package/lib/typescript/src/db/secureKey.d.ts +10 -0
- package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
- package/lib/typescript/src/db/types.d.ts +115 -0
- package/lib/typescript/src/db/types.d.ts.map +1 -0
- package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
- package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
- package/lib/typescript/src/defaultIcons.d.ts +139 -0
- package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
- package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
- package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +95 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/iconConfig.d.ts +176 -0
- package/lib/typescript/src/iconConfig.d.ts.map +1 -0
- package/lib/typescript/src/icons.d.ts +217 -0
- package/lib/typescript/src/icons.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +104 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
- package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
- package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
- package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
- package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
- package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
- package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
- package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
- package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
- package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
- package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
- package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
- package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
- package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
- package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
- package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
- package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
- package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
- package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
- package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
- package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
- package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
- package/lib/typescript/src/library/index.d.ts +40 -0
- package/lib/typescript/src/library/index.d.ts.map +1 -0
- package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
- package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
- package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
- package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
- package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
- package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
- package/lib/typescript/src/library/palette.d.ts +104 -0
- package/lib/typescript/src/library/palette.d.ts.map +1 -0
- package/lib/typescript/src/library/responsive.d.ts +15 -0
- package/lib/typescript/src/library/responsive.d.ts.map +1 -0
- package/lib/typescript/src/locales.d.ts +103 -0
- package/lib/typescript/src/locales.d.ts.map +1 -0
- package/lib/typescript/src/localization.d.ts +1051 -0
- package/lib/typescript/src/localization.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +4 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
- package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
- package/lib/typescript/src/mediaHistory.d.ts +3 -0
- package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
- package/lib/typescript/src/mediaStorageService.d.ts +155 -0
- package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
- package/lib/typescript/src/memory.d.ts +21 -0
- package/lib/typescript/src/memory.d.ts.map +1 -0
- package/lib/typescript/src/nativeActions.d.ts +174 -0
- package/lib/typescript/src/nativeActions.d.ts.map +1 -0
- package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
- package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
- package/lib/typescript/src/networkPerformance.d.ts +12 -0
- package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
- package/lib/typescript/src/performance.d.ts +32 -0
- package/lib/typescript/src/performance.d.ts.map +1 -0
- package/lib/typescript/src/performanceMonitor.d.ts +45 -0
- package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
- package/lib/typescript/src/permissions.d.ts +132 -0
- package/lib/typescript/src/permissions.d.ts.map +1 -0
- package/lib/typescript/src/rendering.d.ts +9 -0
- package/lib/typescript/src/rendering.d.ts.map +1 -0
- package/lib/typescript/src/security.d.ts +6 -0
- package/lib/typescript/src/security.d.ts.map +1 -0
- package/lib/typescript/src/state.d.ts +102 -0
- package/lib/typescript/src/state.d.ts.map +1 -0
- package/lib/typescript/src/storage.d.ts +117 -0
- package/lib/typescript/src/storage.d.ts.map +1 -0
- package/lib/typescript/src/theme.d.ts +15 -0
- package/lib/typescript/src/theme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +802 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils.d.ts +50 -0
- package/lib/typescript/src/utils.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +52 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +204 -0
- package/react-native.config.js +17 -0
- package/src/AudioPlayerContext.tsx +220 -0
- package/src/CameraPreview.tsx +352 -0
- package/src/NativeAnimatedImageView.tsx +71 -0
- package/src/NativeAudioPlayerView.tsx +39 -0
- package/src/NativeBlurView.tsx +40 -0
- package/src/NativeCustomCamera.ts +305 -0
- package/src/NativeCustomCamera.web.ts +192 -0
- package/src/NativeCustomVideoView.ts +30 -0
- package/src/NativeDocumentPreviewView.tsx +196 -0
- package/src/NativeMediaThumbnailView.tsx +167 -0
- package/src/NativeSvgIcon.tsx +46 -0
- package/src/ResponsivePreviewContainer.tsx +227 -0
- package/src/accessibility.ts +80 -0
- package/src/config.ts +406 -0
- package/src/cpuOptimization.ts +365 -0
- package/src/db/DatabaseManager.ts +212 -0
- package/src/db/MediaMapper.ts +377 -0
- package/src/db/MediaRepository.ts +313 -0
- package/src/db/index.ts +73 -0
- package/src/db/migrations.ts +222 -0
- package/src/db/react-native-sqlcipher.d.ts +76 -0
- package/src/db/secureKey.ts +100 -0
- package/src/db/types.ts +123 -0
- package/src/db/useMediaDatabase.ts +191 -0
- package/src/defaultIcons.tsx +2390 -0
- package/src/deviceCapabilities.ts +438 -0
- package/src/errors.ts +350 -0
- package/src/iconConfig.tsx +962 -0
- package/src/icons.tsx +788 -0
- package/src/index.tsx +763 -0
- package/src/jest-globals.d.ts +9 -0
- package/src/library/MediaLibrary.tsx +141 -0
- package/src/library/MediaLibraryScreen.tsx +3003 -0
- package/src/library/components/AudioPreview.tsx +73 -0
- package/src/library/components/DocumentPreview.tsx +361 -0
- package/src/library/components/ExpandableFab.tsx +342 -0
- package/src/library/components/FileTiles.tsx +252 -0
- package/src/library/components/ImagePreview.tsx +72 -0
- package/src/library/components/MediaMetadataHeader.tsx +208 -0
- package/src/library/components/RecordAudioScreen.tsx +845 -0
- package/src/library/components/VideoPreview.tsx +478 -0
- package/src/library/components/ViewerCounter.tsx +71 -0
- package/src/library/compressionEstimate.ts +662 -0
- package/src/library/fileMetadata.ts +212 -0
- package/src/library/index.ts +66 -0
- package/src/library/libraryIconRegistry.tsx +86 -0
- package/src/library/libraryIcons.tsx +928 -0
- package/src/library/localizedFormat.ts +207 -0
- package/src/library/palette.ts +281 -0
- package/src/library/responsive.ts +49 -0
- package/src/locales.ts +381 -0
- package/src/localization.ts +1947 -0
- package/src/logger.ts +20 -0
- package/src/lowMemoryCache.ts +218 -0
- package/src/mediaHistory.ts +42 -0
- package/src/mediaStorageService.ts +546 -0
- package/src/memory.ts +102 -0
- package/src/nativeActions.ts +422 -0
- package/src/nativeComponentSupport.ts +60 -0
- package/src/networkPerformance.ts +30 -0
- package/src/performance.ts +137 -0
- package/src/performanceMonitor.ts +219 -0
- package/src/permissions.ts +403 -0
- package/src/rendering.ts +47 -0
- package/src/security.ts +19 -0
- package/src/state.ts +333 -0
- package/src/storage.ts +453 -0
- package/src/theme.tsx +467 -0
- package/src/types.ts +946 -0
- package/src/utils.ts +177 -0
- 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
|
+
}
|