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>
This commit is contained in:
Lance Chant
2026-05-25 14:19:36 +02:00
parent 4253f0d5ab
commit 6b0f8b833f
12 changed files with 200 additions and 40 deletions

View File

@@ -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<MpvVoDriver>[] = 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<TVTypographyScale>[] = 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 */}
<TVSectionHeader title={t("home.settings.vo_driver.title")} />
<TVSettingsOptionButton
label={t("home.settings.vo_driver.vo_mode")}
value={voDriverLabel}
onPress={() =>
showOptions({
title: t("home.settings.vo_driver.vo_mode"),
options: voDriverOptions,
onSelect: (value) => updateSettings({ mpvVoDriver: value }),
})
}
/>
<TVSettingsStepper
label={t("home.settings.buffer.buffer_duration")}
value={settings.mpvCacheSeconds ?? 10}

View File

@@ -4,6 +4,7 @@ import { GestureControls } from "@/components/settings/GestureControls";
import { MediaProvider } from "@/components/settings/MediaContext";
import { MediaToggles } from "@/components/settings/MediaToggles";
import { MpvBufferSettings } from "@/components/settings/MpvBufferSettings";
import { MpvVoSettings } from "@/components/settings/MpvVoSettings";
import { PlaybackControlsSettings } from "@/components/settings/PlaybackControlsSettings";
import { ChromecastSettings } from "../../../../../../components/settings/ChromecastSettings";
@@ -28,6 +29,7 @@ export default function PlaybackControlsPage() {
<GestureControls className='mb-4' />
<PlaybackControlsSettings />
<MpvBufferSettings />
<MpvVoSettings />
</MediaProvider>
</View>
{!Platform.isTV && <ChromecastSettings />}

View File

@@ -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 () => {

View File

@@ -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 (
<ListGroup title={t("home.settings.vo_driver.title")} className='mb-4'>
<ListItem title={t("home.settings.vo_driver.vo_mode")}>
<PlatformDropdown
groups={voDriverOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{currentVoDriverLabel}
</Text>
<Ionicons name='chevron-expand-sharp' size={18} color='#5A5960' />
</View>
}
title={t("home.settings.vo_driver.vo_mode")}
/>
</ListItem>
</ListGroup>
);
};

View File

@@ -912,20 +912,6 @@ export const Controls: FC<Props> = ({
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<Props> = ({
<Animated.View
style={[styles.bottomContainer, bottomAnimatedStyle]}
pointerEvents={showControls ? "auto" : "none"}
focusable={showControls}
>
<View
style={[

View File

@@ -350,6 +350,12 @@ export const TechnicalInfoOverlay: FC<TechnicalInfoOverlayProps> = memo(
Buffer: {info.cacheSeconds.toFixed(1)}s
</Text>
)}
{info?.voDriver && (
<Text style={textStyle}>
VO: {info.voDriver}
{info.hwdec ? ` / ${info.hwdec}` : ""}
</Text>
)}
{info?.droppedFrames !== undefined && info.droppedFrames > 0 && (
<Text style={[textStyle, styles.warningText]}>
Dropped: {info.droppedFrames} frames

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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;
};

View File

@@ -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",

View File

@@ -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,