diff --git a/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt b/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt index 00149c4..48b795b 100644 --- a/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt +++ b/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt @@ -24,8 +24,11 @@ class MPVView @JvmOverloads constructor( private var surface: Surface? = null private var httpHeaders: Map? = null - // Hardware decoding setting (default: false = software decoding) - var useHardwareDecoding: Boolean = false + // Decoder mode setting: 'auto', 'sw', 'hw', 'hw+' (default: auto) + var decoderMode: String = "auto" + + // GPU mode setting: 'gpu', 'gpu-next' (default: gpu) + var gpuMode: String = "gpu" // Flag to track if onLoad has been fired (prevents multiple fires for HLS streams) private var hasLoadEventFired: Boolean = false @@ -94,22 +97,47 @@ class MPVView @JvmOverloads constructor( private fun initOptions() { MPVLib.setOptionString("profile", "fast") - MPVLib.setOptionString("vo", "gpu") + + // GPU rendering mode (gpu or gpu-next) + MPVLib.setOptionString("vo", gpuMode) MPVLib.setOptionString("gpu-context", "android") MPVLib.setOptionString("opengl-es", "yes") - val hwdecValue = if (useHardwareDecoding) "mediacodec,mediacodec-copy" else "no" - Log.d(TAG, "Hardware decoding: $useHardwareDecoding, hwdec value: $hwdecValue") + // Decoder mode mapping (same as mpvKt) + val hwdecValue = when (decoderMode) { + "auto" -> "auto-copy" // Best balance: HW decode, copy to CPU for filters + "sw" -> "no" // Software decoding only + "hw" -> "mediacodec-copy" // HW decode with copy (safer) + "hw+" -> "mediacodec" // Full HW decode (fastest, may have issues) + else -> "auto-copy" + } + Log.d(TAG, "Decoder mode: $decoderMode, hwdec value: $hwdecValue, GPU mode: $gpuMode") MPVLib.setOptionString("hwdec", hwdecValue) - MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1") + // Note: Not setting hwdec-codecs explicitly - let mpv use defaults MPVLib.setOptionString("target-colorspace-hint", "yes") + + // HDR and Dolby Vision support + // target-prim: Signal target display primaries (auto = passthrough when display supports) + MPVLib.setOptionString("target-prim", "auto") + // target-trc: Signal target transfer characteristics (auto = passthrough when display supports) + MPVLib.setOptionString("target-trc", "auto") + // tone-mapping: How to handle HDR/DV content on SDR displays (auto = best automatic choice) + MPVLib.setOptionString("tone-mapping", "auto") + // hdr-compute-peak: Compute peak brightness for better tone mapping + MPVLib.setOptionString("hdr-compute-peak", "auto") + // Allow DV Profile 5 (HEVC with RPU) to be decoded by hardware decoder + MPVLib.setOptionString("vd-lavc-o", "strict=-2") + + // Workaround for https://github.com/mpv-player/mpv/issues/14651 MPVLib.setOptionString("vd-lavc-film-grain", "cpu") MPVLib.setOptionString("ao", "audiotrack,opensles") - MPVLib.setOptionString("demuxer-max-bytes", "67108864") - MPVLib.setOptionString("demuxer-max-back-bytes", "33554432") + // Limit demuxer cache based on Android version (like mpvKt) + val cacheMegs = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O_MR1) 64 else 32 + MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") + MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") MPVLib.setOptionString("cache", "yes") MPVLib.setOptionString("cache-secs", "30") @@ -126,9 +154,6 @@ class MPVView @JvmOverloads constructor( MPVLib.setOptionString("demuxer-seekable-cache", "yes") MPVLib.setOptionString("force-seekable", "yes") - MPVLib.setOptionString("demuxer-lavf-probesize", "10000000") - MPVLib.setOptionString("demuxer-lavf-analyzeduration", "10") - MPVLib.setOptionString("sub-auto", "fuzzy") MPVLib.setOptionString("sub-visibility", "yes") MPVLib.setOptionString("sub-font-size", "48") diff --git a/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt b/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt index c50f928..ef8a3b1 100644 --- a/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt +++ b/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt @@ -181,8 +181,13 @@ class MpvPlayerViewManager( } } - @ReactProp(name = "useHardwareDecoding") - fun setUseHardwareDecoding(view: MPVView, useHardwareDecoding: Boolean) { - view.useHardwareDecoding = useHardwareDecoding + @ReactProp(name = "decoderMode") + fun setDecoderMode(view: MPVView, decoderMode: String?) { + view.decoderMode = decoderMode ?: "auto" + } + + @ReactProp(name = "gpuMode") + fun setGpuMode(view: MPVView, gpuMode: String?) { + view.gpuMode = gpuMode ?: "gpu" } } diff --git a/android/build.gradle b/android/build.gradle index bdec99d..9d7137b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,6 +7,7 @@ buildscript { compileSdkVersion = 35 targetSdkVersion = 35 castFrameworkVersion = "22.1.0" + ndkVersion = "29.0.14206865" // Required for libmpv AAR built with NDK r29 } repositories { google() diff --git a/libmpv-android b/libmpv-android index 8c4778b..db3b10e 160000 --- a/libmpv-android +++ b/libmpv-android @@ -1 +1 @@ -Subproject commit 8c4778b5aad441bb0449a7f9b3d6d827fd3d6a2a +Subproject commit db3b10e64353349d0d72619ca7d779829e36fe4d diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 11d824f..8245095 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -568,7 +568,8 @@ const AndroidVideoPlayer: React.FC = () => { onPinchGestureEvent={() => { }} onPinchHandlerStateChange={() => { }} screenDimensions={playerState.screenDimensions} - useHardwareDecoding={settings.useHardwareDecoding} + decoderMode={settings.decoderMode} + gpuMode={settings.gpuMode} /> )} diff --git a/src/components/player/android/MpvPlayer.tsx b/src/components/player/android/MpvPlayer.tsx index 04c7b08..51aa87f 100644 --- a/src/components/player/android/MpvPlayer.tsx +++ b/src/components/player/android/MpvPlayer.tsx @@ -25,7 +25,8 @@ export interface MpvPlayerProps { onEnd?: () => void; onError?: (error: { error: string }) => void; onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void; - useHardwareDecoding?: boolean; + decoderMode?: 'auto' | 'sw' | 'hw' | 'hw+'; + gpuMode?: 'gpu' | 'gpu-next'; } const MpvPlayer = forwardRef((props, ref) => { @@ -104,7 +105,8 @@ const MpvPlayer = forwardRef((props, ref) => { onEnd={handleEnd} onError={handleError} onTracksChanged={handleTracksChanged} - useHardwareDecoding={props.useHardwareDecoding ?? false} + decoderMode={props.decoderMode ?? 'auto'} + gpuMode={props.gpuMode ?? 'gpu'} /> ); }); diff --git a/src/components/player/android/components/VideoSurface.tsx b/src/components/player/android/components/VideoSurface.tsx index e4e2d50..16123f5 100644 --- a/src/components/player/android/components/VideoSurface.tsx +++ b/src/components/player/android/components/VideoSurface.tsx @@ -32,7 +32,8 @@ interface VideoSurfaceProps { onPinchHandlerStateChange: any; screenDimensions: { width: number, height: number }; onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void; - useHardwareDecoding?: boolean; + decoderMode?: 'auto' | 'sw' | 'hw' | 'hw+'; + gpuMode?: 'gpu' | 'gpu-next'; } export const VideoSurface: React.FC = ({ @@ -56,7 +57,8 @@ export const VideoSurface: React.FC = ({ onPinchHandlerStateChange, screenDimensions, onTracksChanged, - useHardwareDecoding, + decoderMode, + gpuMode, }) => { // Use the actual stream URL const streamUrl = currentStreamUrl || processedStreamUrl; @@ -115,7 +117,8 @@ export const VideoSurface: React.FC = ({ onEnd={handleEnd} onError={handleError} onTracksChanged={onTracksChanged} - useHardwareDecoding={useHardwareDecoding} + decoderMode={decoderMode} + gpuMode={gpuMode} /> {/* Gesture overlay - transparent, on top of the player */} diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 21e9e81..c89257f 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -89,7 +89,8 @@ export interface AppSettings { enableStreamsBackdrop: boolean; // Enable blurred backdrop background on StreamsScreen mobile useExternalPlayerForDownloads: boolean; // Enable/disable external player for downloaded content // Android MPV player settings - useHardwareDecoding: boolean; // Enable hardware decoding for MPV player on Android (default: false for software decoding) + decoderMode: 'auto' | 'sw' | 'hw' | 'hw+'; // Decoder mode: auto (auto-copy), sw (software), hw (mediacodec-copy), hw+ (mediacodec) + gpuMode: 'gpu' | 'gpu-next'; // GPU rendering mode: gpu (standard) or gpu-next (advanced HDR/color) } export const DEFAULT_SETTINGS: AppSettings = { @@ -152,7 +153,8 @@ export const DEFAULT_SETTINGS: AppSettings = { streamCacheTTL: 60 * 60 * 1000, // Default: 1 hour in milliseconds enableStreamsBackdrop: true, // Enable by default (new behavior) // Android MPV player settings - useHardwareDecoding: false, // Default to software decoding (more compatible) + decoderMode: 'auto', // Default to auto (best compatibility and performance) + gpuMode: 'gpu', // Default to gpu (gpu-next for advanced HDR) }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/screens/PlayerSettingsScreen.tsx b/src/screens/PlayerSettingsScreen.tsx index 577da4d..930c424 100644 --- a/src/screens/PlayerSettingsScreen.tsx +++ b/src/screens/PlayerSettingsScreen.tsx @@ -335,51 +335,139 @@ const PlayerSettingsScreen: React.FC = () => { - {/* Hardware Decoding for Android Internal Player */} + {/* Decoder Mode for Android Internal Player */} {Platform.OS === 'android' && !settings.useExternalPlayer && ( - - - - + <> + + + + + + + + Decoder Mode + + + How video is decoded. Auto is recommended for best balance. + + - - - Hardware Decoding - - - Use GPU for video decoding. May improve performance but can cause issues on some devices. - + + {([ + { id: 'auto', label: 'Auto', desc: 'Best balance' }, + { id: 'sw', label: 'SW', desc: 'Software' }, + { id: 'hw', label: 'HW', desc: 'Hardware' }, + { id: 'hw+', label: 'HW+', desc: 'Full HW' }, + ] as const).map((option) => ( + { + updateSetting('decoderMode', option.id); + openAlert( + 'Restart Required', + 'Please restart the app for the decoder change to take effect.' + ); + }} + style={[ + styles.optionButton, + settings.decoderMode === option.id && { backgroundColor: currentTheme.colors.primary }, + ]} + > + + {option.label} + + + ))} - { - updateSetting('useHardwareDecoding', value); - openAlert( - 'Restart Required', - 'Please restart the app for the decoding change to take effect.' - ); - }} - thumbColor={settings.useHardwareDecoding ? currentTheme.colors.primary : undefined} - /> - + + {/* GPU Mode for Android Internal Player */} + + + + + + + + GPU Rendering + + + GPU-Next offers better HDR and color management. + + + + + {([ + { id: 'gpu', label: 'GPU', desc: 'Standard' }, + { id: 'gpu-next', label: 'GPU-Next', desc: 'Advanced' }, + ] as const).map((option) => ( + { + updateSetting('gpuMode', option.id); + openAlert( + 'Restart Required', + 'Please restart the app for the GPU mode change to take effect.' + ); + }} + style={[ + styles.optionButton, + styles.optionButtonWide, + settings.gpuMode === option.id && { backgroundColor: currentTheme.colors.primary }, + ]} + > + + {option.label} + + + ))} + + + )} {/* External Player for Downloads */} @@ -532,6 +620,28 @@ const styles = StyleSheet.create({ checkIcon: { marginLeft: 16, }, + optionButtonsRow: { + flexDirection: 'row', + marginTop: 12, + paddingHorizontal: 52, + gap: 8, + }, + optionButton: { + flex: 1, + paddingVertical: 10, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: 'rgba(255,255,255,0.1)', + alignItems: 'center', + justifyContent: 'center', + }, + optionButtonWide: { + flex: 1.5, + }, + optionButtonText: { + fontSize: 13, + fontWeight: '600', + }, }); export default PlayerSettingsScreen;