From 6b0f8b833f5dd223dd568671ab932a529bb67d99 Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Mon, 25 May 2026 14:19:36 +0200 Subject: [PATCH] Chore: log cleanups, and Vo settings enablement Added the ability to swap VO options for android only between "GPU" and "GPU-next" Removed some console logs from previous debugging Added the ability to see what VO is being used to render in the video player Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/(tabs)/(home)/settings.tv.tsx | 38 +++++++++++ .../settings/playback-controls/page.tsx | 2 + app/(auth)/player/direct-player.tsx | 8 +-- components/settings/MpvVoSettings.tsx | 66 +++++++++++++++++++ .../video-player/controls/Controls.tv.tsx | 15 ----- .../controls/TechnicalInfoOverlay.tsx | 6 ++ .../modules/mpvplayer/MPVLayerRenderer.kt | 24 +++++-- .../expo/modules/mpvplayer/MpvPlayerModule.kt | 3 +- .../expo/modules/mpvplayer/MpvPlayerView.kt | 61 ++++++++++++----- modules/mpv-player/src/MpvPlayer.types.ts | 6 ++ translations/en.json | 6 ++ utils/atoms/settings.ts | 5 ++ 12 files changed, 200 insertions(+), 40 deletions(-) create mode 100644 components/settings/MpvVoSettings.tsx diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index 9dd56673..d80b07e3 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -26,6 +26,7 @@ import { AudioTranscodeMode, InactivityTimeout, type MpvCacheMode, + type MpvVoDriver, TVTypographyScale, useSettings, } from "@/utils/atoms/settings"; @@ -171,6 +172,7 @@ export default function SettingsTV() { const currentTypographyScale = settings.tvTypographyScale || TVTypographyScale.Default; const currentCacheMode = settings.mpvCacheEnabled ?? "auto"; + const currentVoDriver = settings.mpvVoDriver ?? "gpu-next"; const currentLanguage = settings.preferedLanguage; // Audio transcoding options @@ -285,6 +287,23 @@ export default function SettingsTV() { [t, currentCacheMode], ); + // VO driver options + const voDriverOptions: TVOptionItem[] = useMemo( + () => [ + { + label: t("home.settings.vo_driver.gpu_next"), + value: "gpu-next", + selected: currentVoDriver === "gpu-next", + }, + { + label: t("home.settings.vo_driver.gpu"), + value: "gpu", + selected: currentVoDriver === "gpu", + }, + ], + [t, currentVoDriver], + ); + // Typography scale options const typographyScaleOptions: TVOptionItem[] = useMemo( () => [ @@ -411,6 +430,11 @@ export default function SettingsTV() { return option?.label || t("home.settings.buffer.cache_auto"); }, [cacheModeOptions, t]); + const voDriverLabel = useMemo(() => { + const option = voDriverOptions.find((o) => o.selected); + return option?.label || t("home.settings.vo_driver.gpu_next"); + }, [voDriverOptions, t]); + const languageLabel = useMemo(() => { if (!currentLanguage) return t("home.settings.languages.system"); const option = APP_LANGUAGES.find((l) => l.value === currentLanguage); @@ -636,6 +660,20 @@ export default function SettingsTV() { }) } /> + + {/* Video Output Section */} + + + showOptions({ + title: t("home.settings.vo_driver.vo_mode"), + options: voDriverOptions, + onSelect: (value) => updateSettings({ mpvVoDriver: value }), + }) + } + /> + {!Platform.isTV && } diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 7a2e1781..eadc61fd 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -608,11 +608,6 @@ export default function page() { : (item?.UserData?.PlaybackPositionTicks ?? 0); const startPos = ticksToSeconds(startTicks); - console.log( - `[DirectPlayer] Resume position — ticks: ${startTicks}, seconds: ${startPos}, ` + - `fromUrl: ${playbackPositionFromUrl}, itemTicks: ${item?.UserData?.PlaybackPositionTicks ?? 0}`, - ); - // Build source config - headers only needed for online streaming const source: MpvVideoSource = { url: stream.url, @@ -627,6 +622,8 @@ export default function page() { maxBytes: settings.mpvDemuxerMaxBytes, maxBackBytes: settings.mpvDemuxerMaxBackBytes, }, + // Pass VO driver setting (Android only) + voDriver: settings.mpvVoDriver, }; // Add external subtitles only for online playback @@ -671,6 +668,7 @@ export default function page() { settings.mpvCacheSeconds, settings.mpvDemuxerMaxBytes, settings.mpvDemuxerMaxBackBytes, + settings.mpvVoDriver, ]); const volumeUpCb = useCallback(async () => { diff --git a/components/settings/MpvVoSettings.tsx b/components/settings/MpvVoSettings.tsx new file mode 100644 index 00000000..164829c4 --- /dev/null +++ b/components/settings/MpvVoSettings.tsx @@ -0,0 +1,66 @@ +import { Ionicons } from "@expo/vector-icons"; +import type React from "react"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform, View } from "react-native"; +import { PlatformDropdown } from "@/components/PlatformDropdown"; +import { type MpvVoDriver, useSettings } from "@/utils/atoms/settings"; +import { Text } from "../common/Text"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; + +const VO_DRIVER_OPTIONS: { key: string; value: MpvVoDriver }[] = [ + { key: "home.settings.vo_driver.gpu_next", value: "gpu-next" }, + { key: "home.settings.vo_driver.gpu", value: "gpu" }, +]; + +export const MpvVoSettings: React.FC = () => { + const { settings, updateSettings } = useSettings(); + const { t } = useTranslation(); + + const voDriverOptions = useMemo( + () => [ + { + options: VO_DRIVER_OPTIONS.map((option) => ({ + type: "radio" as const, + label: t(option.key), + value: option.value, + selected: option.value === (settings?.mpvVoDriver ?? "gpu-next"), + onPress: () => updateSettings({ mpvVoDriver: option.value }), + })), + }, + ], + [settings?.mpvVoDriver, t, updateSettings], + ); + + const currentVoDriverLabel = useMemo(() => { + const option = VO_DRIVER_OPTIONS.find( + (o) => o.value === (settings?.mpvVoDriver ?? "gpu-next"), + ); + return option ? t(option.key) : t("home.settings.vo_driver.gpu_next"); + }, [settings?.mpvVoDriver, t]); + + // Only show on Android + if (Platform.OS !== "android") return null; + + if (!settings) return null; + + return ( + + + + + {currentVoDriverLabel} + + + + } + title={t("home.settings.vo_driver.vo_mode")} + /> + + + ); +}; diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx index 85d65577..2e7fab49 100644 --- a/components/video-player/controls/Controls.tv.tsx +++ b/components/video-player/controls/Controls.tv.tsx @@ -912,20 +912,6 @@ export const Controls: FC = ({ setFocusPlayButton(false); }, [setShowControls]); - // On initial mount when controls start visible, focus the play button. - // playButtonRef transitions from null → View on first render; once set, - // this effect won't re-fire (playButtonRef is a stable reference). - const initialFocusDone = useRef(false); - useEffect(() => { - if (!initialFocusDone.current && playButtonRef && showControls) { - initialFocusDone.current = true; - const t = setTimeout(() => { - playButtonRef.focus(); - }, 100); - return () => clearTimeout(t); - } - }, [showControls, playButtonRef]); - // When controls hide (and no skip/countdown overlay is visible), move focus // to the invisible overlay so hidden buttons can't receive select events. useEffect(() => { @@ -1226,7 +1212,6 @@ export const Controls: FC = ({ = memo( Buffer: {info.cacheSeconds.toFixed(1)}s )} + {info?.voDriver && ( + + VO: {info.voDriver} + {info.hwdec ? ` / ${info.hwdec}` : ""} + + )} {info?.droppedFrames !== undefined && info.droppedFrames > 0 && ( Dropped: {info.droppedFrames} frames diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt index a10fa80d..98debe63 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt @@ -105,7 +105,12 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { val duration: Double get() = cachedDuration - fun start() { + /** + * The VO driver to use. Stored so attachSurface can re-enable the same driver. + */ + private var voDriver: String = "gpu-next" + + fun start(voDriver: String = "gpu-next") { if (isRunning) return try { @@ -159,7 +164,8 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.setOptionString("config-dir", mpvDir.path) // Configure mpv options before initialization (based on Findroid) - MPVLib.setOptionString("vo", "gpu") + this.voDriver = voDriver + MPVLib.setOptionString("vo", voDriver) MPVLib.setOptionString("gpu-context", "android") MPVLib.setOptionString("opengl-es", "yes") @@ -239,8 +245,8 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.attachSurface(surface) // Re-enable video output after attaching surface (Findroid approach) MPVLib.setOptionString("force-window", "yes") - MPVLib.setOptionString("vo", "gpu") - Log.i(TAG, "Surface attached, video output re-enabled") + MPVLib.setOptionString("vo", voDriver) + Log.i(TAG, "Surface attached, video output re-enabled (vo=$voDriver)") } } @@ -573,6 +579,16 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { info["droppedFrames"] = it } + // Active video output driver (read from MPV to confirm what's actually applied) + MPVLib.getPropertyString("vo")?.let { + info["voDriver"] = it + } + + // Active hardware decoder + MPVLib.getPropertyString("hwdec-active")?.let { + info["hwdec"] = it + } + return info } diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerModule.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerModule.kt index 3fa6d57f..3cd1fc64 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerModule.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerModule.kt @@ -37,7 +37,8 @@ class MpvPlayerModule : Module() { startPosition = (source["startPosition"] as? Number)?.toDouble(), autoplay = (source["autoplay"] as? Boolean) ?: true, initialSubtitleId = (source["initialSubtitleId"] as? Number)?.toInt(), - initialAudioId = (source["initialAudioId"] as? Number)?.toInt() + initialAudioId = (source["initialAudioId"] as? Number)?.toInt(), + voDriver = source["voDriver"] as? String ) view.loadVideo(config) diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt index 0fb6bdde..066afe90 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MpvPlayerView.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Color import android.os.Build import android.util.Log +import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView import android.widget.FrameLayout @@ -21,7 +22,8 @@ data class VideoLoadConfig( val startPosition: Double? = null, val autoplay: Boolean = true, val initialSubtitleId: Int? = null, - val initialAudioId: Int? = null + val initialAudioId: Int? = null, + val voDriver: String? = null ) /** @@ -52,10 +54,12 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context private var intendedPlayState: Boolean = false private var surfaceReady: Boolean = false private var pendingConfig: VideoLoadConfig? = null - + private var rendererStarted: Boolean = false + private var pendingSurface: Surface? = null + init { setBackgroundColor(Color.BLACK) - + // Create SurfaceView for video rendering surfaceView = SurfaceView(context).apply { layoutParams = FrameLayout.LayoutParams( @@ -65,11 +69,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context holder.addCallback(this@MpvPlayerView) } addView(surfaceView) - - // Initialize renderer - renderer = MPVLayerRenderer(context) - renderer?.delegate = this - + // Initialize PiP controller with Expo's AppContext for proper activity access pipController = PiPController(context, appContext) pipController?.setPlayerView(surfaceView) @@ -77,19 +77,39 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context override fun onPlay() { play() } - + override fun onPause() { pause() } - + override fun onSeekBy(seconds: Double) { seekBy(seconds) } } - - // Start the renderer + + // Renderer is created lazily in loadVideo once we have the voDriver setting + renderer = MPVLayerRenderer(context) + renderer?.delegate = this + } + + /** + * Start the renderer with the given VO driver. + * Called lazily on first loadVideo so the voDriver setting is available. + */ + private fun ensureRendererStarted(voDriver: String?) { + if (rendererStarted) return + try { - renderer?.start() + renderer?.start(voDriver ?: "gpu-next") + rendererStarted = true + Log.i(TAG, "Renderer started with vo=$voDriver") + + // If surface was created before renderer started, attach it now + pendingSurface?.let { surface -> + renderer?.attachSurface(surface) + pendingSurface = null + Log.i(TAG, "Attached pending surface after renderer start") + } } catch (e: Exception) { Log.e(TAG, "Failed to start renderer: ${e.message}") onError(mapOf("error" to "Failed to start renderer: ${e.message}")) @@ -101,10 +121,18 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context override fun surfaceCreated(holder: SurfaceHolder) { Log.i(TAG, "Surface created") surfaceReady = true - renderer?.attachSurface(holder.surface) - + + if (rendererStarted) { + renderer?.attachSurface(holder.surface) + } else { + // Renderer not started yet - store surface to attach after start + pendingSurface = holder.surface + Log.i(TAG, "Surface created before renderer started, storing as pending") + } + // If we have a pending load, execute it now pendingConfig?.let { config -> + ensureRendererStarted(config.voDriver) loadVideoInternal(config) pendingConfig = null } @@ -136,6 +164,9 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context return } + // Ensure renderer is started with the configured VO driver + ensureRendererStarted(config.voDriver) + loadVideoInternal(config) } diff --git a/modules/mpv-player/src/MpvPlayer.types.ts b/modules/mpv-player/src/MpvPlayer.types.ts index 552cbefa..bf934d87 100644 --- a/modules/mpv-player/src/MpvPlayer.types.ts +++ b/modules/mpv-player/src/MpvPlayer.types.ts @@ -54,6 +54,8 @@ export type VideoSource = { /** Maximum backward cache size in MB (default: 50, range: 25-200) */ maxBackBytes?: number; }; + /** MPV video output driver (Android only) */ + voDriver?: "gpu-next" | "gpu"; }; export type MpvPlayerViewProps = { @@ -137,4 +139,8 @@ export type TechnicalInfo = { audioBitrate?: number; cacheSeconds?: number; droppedFrames?: number; + /** Active video output driver (read from MPV at runtime) */ + voDriver?: string; + /** Active hardware decoder (read from MPV at runtime) */ + hwdec?: string; }; diff --git a/translations/en.json b/translations/en.json index e928bec0..00f1ab11 100644 --- a/translations/en.json +++ b/translations/en.json @@ -209,6 +209,12 @@ "max_cache_size": "Max Cache Size", "max_backward_cache": "Max Backward Cache" }, + "vo_driver": { + "title": "Video Output", + "vo_mode": "VO Driver", + "gpu_next": "gpu-next (Recommended)", + "gpu": "gpu" + }, "gesture_controls": { "gesture_controls_title": "Gesture Controls", "horizontal_swipe_skip": "Horizontal Swipe to Skip", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index f7dbf791..55b6aa52 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -168,6 +168,7 @@ export enum InactivityTimeout { // MPV cache mode - controls how caching is enabled export type MpvCacheMode = "auto" | "yes" | "no"; +export type MpvVoDriver = "gpu-next" | "gpu"; export type Settings = { home?: Home | null; @@ -221,6 +222,8 @@ export type Settings = { mpvCacheSeconds?: number; mpvDemuxerMaxBytes?: number; // MB mpvDemuxerMaxBackBytes?: number; // MB + // MPV video output driver (Android only) + mpvVoDriver?: MpvVoDriver; // Gesture controls enableHorizontalSwipeSkip: boolean; enableLeftSideBrightnessSwipe: boolean; @@ -322,6 +325,8 @@ export const defaultValues: Settings = { mpvCacheSeconds: 10, mpvDemuxerMaxBytes: 150, // MB mpvDemuxerMaxBackBytes: 50, // MB + // MPV video output driver defaults (Android only) + mpvVoDriver: "gpu-next", // Gesture controls enableHorizontalSwipeSkip: true, enableLeftSideBrightnessSwipe: true,