mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-26 00:36:41 +01:00
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:
@@ -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}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
66
components/settings/MpvVoSettings.tsx
Normal file
66
components/settings/MpvVoSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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={[
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user