From be8651357bf117d5069b0733fdfecb551875008f Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 7 Jan 2026 22:17:27 +0100 Subject: [PATCH] feat(player): vlc subtitle options --- .../(home)/settings/audio-subtitles/page.tsx | 2 + app/(auth)/player/direct-player.tsx | 64 ++++- components/settings/VlcSubtitleSettings.tsx | 245 ++++++++++++++++++ constants/SubtitleConstants.ts | 40 +++ translations/en.json | 12 + utils/atoms/settings.ts | 18 ++ 6 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 components/settings/VlcSubtitleSettings.tsx create mode 100644 constants/SubtitleConstants.ts diff --git a/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx b/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx index 58415127..6be20d97 100644 --- a/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/audio-subtitles/page.tsx @@ -3,6 +3,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AudioToggles } from "@/components/settings/AudioToggles"; import { MediaProvider } from "@/components/settings/MediaContext"; import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; +import { VlcSubtitleSettings } from "@/components/settings/VlcSubtitleSettings"; export default function AudioSubtitlesPage() { const insets = useSafeAreaInsets(); @@ -22,6 +23,7 @@ export default function AudioSubtitlesPage() { + diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 949c084e..a70f9814 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -28,6 +28,7 @@ import { PlaybackSpeedScope, updatePlaybackSpeedSettings, } from "@/components/video-player/controls/utils/playback-speed-settings"; +import { OUTLINE_THICKNESS, VLC_COLORS } from "@/constants/SubtitleConstants"; import { useHaptic } from "@/hooks/useHaptic"; import { useOrientation } from "@/hooks/useOrientation"; import { usePlaybackManager } from "@/hooks/usePlaybackManager"; @@ -672,11 +673,62 @@ export default function page() { } } - // Add subtitle styling + // Add VLC subtitle styling from settings if (settings.subtitleSize) { initOptions.push(`--sub-text-scale=${settings.subtitleSize}`); } - initOptions.push("--sub-margin=40"); + initOptions.push(`--sub-margin=${settings.vlcSubtitleMargin ?? 40}`); + + // Text color + if ( + settings.vlcTextColor && + VLC_COLORS[settings.vlcTextColor] !== undefined + ) { + initOptions.push(`--freetype-color=${VLC_COLORS[settings.vlcTextColor]}`); + } + + // Background styling + if ( + settings.vlcBackgroundColor && + VLC_COLORS[settings.vlcBackgroundColor] !== undefined + ) { + initOptions.push( + `--freetype-background-color=${VLC_COLORS[settings.vlcBackgroundColor]}`, + ); + } + if (settings.vlcBackgroundOpacity !== undefined) { + initOptions.push( + `--freetype-background-opacity=${settings.vlcBackgroundOpacity}`, + ); + } + + // Outline styling + if ( + settings.vlcOutlineColor && + VLC_COLORS[settings.vlcOutlineColor] !== undefined + ) { + initOptions.push( + `--freetype-outline-color=${VLC_COLORS[settings.vlcOutlineColor]}`, + ); + } + if (settings.vlcOutlineOpacity !== undefined) { + initOptions.push( + `--freetype-outline-opacity=${settings.vlcOutlineOpacity}`, + ); + } + if ( + settings.vlcOutlineThickness && + OUTLINE_THICKNESS[settings.vlcOutlineThickness] !== undefined + ) { + initOptions.push( + `--freetype-outline-thickness=${OUTLINE_THICKNESS[settings.vlcOutlineThickness]}`, + ); + } + + // Bold text + if (settings.vlcIsBold) { + initOptions.push("--freetype-bold"); + } // For transcoded streams, the server already handles seeking via startTimeTicks, // so we should NOT also tell the player to seek (would cause double-seeking). @@ -703,6 +755,14 @@ export default function page() { subtitleIndex, audioIndex, settings.subtitleSize, + settings.vlcTextColor, + settings.vlcBackgroundColor, + settings.vlcBackgroundOpacity, + settings.vlcOutlineColor, + settings.vlcOutlineOpacity, + settings.vlcOutlineThickness, + settings.vlcIsBold, + settings.vlcSubtitleMargin, ]); const volumeUpCb = useCallback(async () => { diff --git a/components/settings/VlcSubtitleSettings.tsx b/components/settings/VlcSubtitleSettings.tsx new file mode 100644 index 00000000..3d72a177 --- /dev/null +++ b/components/settings/VlcSubtitleSettings.tsx @@ -0,0 +1,245 @@ +import { Ionicons } from "@expo/vector-icons"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Platform, View, type ViewProps } from "react-native"; +import { Switch } from "react-native-gesture-handler"; +import { + OUTLINE_THICKNESS_OPTIONS, + VLC_COLOR_OPTIONS, +} from "@/constants/SubtitleConstants"; +import { useSettings, VideoPlayerIOS } from "@/utils/atoms/settings"; +import { Text } from "../common/Text"; +import { Stepper } from "../inputs/Stepper"; +import { ListGroup } from "../list/ListGroup"; +import { ListItem } from "../list/ListItem"; +import { PlatformDropdown } from "../PlatformDropdown"; + +interface Props extends ViewProps {} + +/** + * VLC Subtitle Settings component + * Only shown when VLC is the active player (Android always, iOS when VLC selected) + * Note: These settings are applied via VLC init options and take effect on next playback + */ +export const VlcSubtitleSettings: React.FC = ({ ...props }) => { + const { t } = useTranslation(); + const { settings, updateSettings } = useSettings(); + + // Only show for VLC users + const isVlcPlayer = + Platform.OS === "android" || + (Platform.OS === "ios" && settings.videoPlayerIOS === VideoPlayerIOS.VLC); + + const textColorOptions = useMemo( + () => [ + { + options: VLC_COLOR_OPTIONS.map((color) => ({ + type: "radio" as const, + label: color, + value: color, + selected: settings.vlcTextColor === color, + onPress: () => updateSettings({ vlcTextColor: color }), + })), + }, + ], + [settings.vlcTextColor, updateSettings], + ); + + const backgroundColorOptions = useMemo( + () => [ + { + options: VLC_COLOR_OPTIONS.map((color) => ({ + type: "radio" as const, + label: color, + value: color, + selected: settings.vlcBackgroundColor === color, + onPress: () => updateSettings({ vlcBackgroundColor: color }), + })), + }, + ], + [settings.vlcBackgroundColor, updateSettings], + ); + + const outlineColorOptions = useMemo( + () => [ + { + options: VLC_COLOR_OPTIONS.map((color) => ({ + type: "radio" as const, + label: color, + value: color, + selected: settings.vlcOutlineColor === color, + onPress: () => updateSettings({ vlcOutlineColor: color }), + })), + }, + ], + [settings.vlcOutlineColor, updateSettings], + ); + + const outlineThicknessOptions = useMemo( + () => [ + { + options: OUTLINE_THICKNESS_OPTIONS.map((thickness) => ({ + type: "radio" as const, + label: thickness, + value: thickness, + selected: settings.vlcOutlineThickness === thickness, + onPress: () => updateSettings({ vlcOutlineThickness: thickness }), + })), + }, + ], + [settings.vlcOutlineThickness, updateSettings], + ); + + if (!isVlcPlayer) return null; + if (Platform.isTV) return null; + + return ( + + + {t("home.settings.vlc_subtitles.hint")} + + } + > + {/* Text Color */} + + + + {settings.vlcTextColor || "White"} + + + + } + title={t("home.settings.vlc_subtitles.text_color")} + /> + + + {/* Background Color */} + + + + {settings.vlcBackgroundColor || "Black"} + + + + } + title={t("home.settings.vlc_subtitles.background_color")} + /> + + + {/* Background Opacity */} + + + updateSettings({ + vlcBackgroundOpacity: Math.round((value / 100) * 255), + }) + } + /> + + + {/* Outline Color */} + + + + {settings.vlcOutlineColor || "Black"} + + + + } + title={t("home.settings.vlc_subtitles.outline_color")} + /> + + + {/* Outline Opacity */} + + + updateSettings({ + vlcOutlineOpacity: Math.round((value / 100) * 255), + }) + } + /> + + + {/* Outline Thickness */} + + + + {settings.vlcOutlineThickness || "Normal"} + + + + } + title={t("home.settings.vlc_subtitles.outline_thickness")} + /> + + + {/* Bold Text */} + + updateSettings({ vlcIsBold: value })} + /> + + + {/* Subtitle Margin */} + + + updateSettings({ vlcSubtitleMargin: Math.round(value) }) + } + /> + + + + ); +}; diff --git a/constants/SubtitleConstants.ts b/constants/SubtitleConstants.ts new file mode 100644 index 00000000..0a0df902 --- /dev/null +++ b/constants/SubtitleConstants.ts @@ -0,0 +1,40 @@ +/** + * VLC subtitle styling constants + * These values are used with VLC's FreeType subtitle rendering engine + */ + +// VLC color values (decimal representation of hex colors) +export const VLC_COLORS: Record = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, +}; + +// VLC color names for UI display +export const VLC_COLOR_OPTIONS = Object.keys(VLC_COLORS); + +// VLC outline thickness values in pixels +export const OUTLINE_THICKNESS: Record = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, +}; + +// Outline thickness options for UI +export const OUTLINE_THICKNESS_OPTIONS = Object.keys( + OUTLINE_THICKNESS, +) as Array<"None" | "Thin" | "Normal" | "Thick">; diff --git a/translations/en.json b/translations/en.json index 6fe0dcac..aefb1afe 100644 --- a/translations/en.json +++ b/translations/en.json @@ -189,6 +189,18 @@ "hardware_decode": "Hardware Decoding", "hardware_decode_description": "Use hardware acceleration for video decoding. Disable if you experience playback issues." }, + "vlc_subtitles": { + "title": "VLC Subtitle Settings", + "hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.", + "text_color": "Text Color", + "background_color": "Background Color", + "background_opacity": "Background Opacity", + "outline_color": "Outline Color", + "outline_opacity": "Outline Opacity", + "outline_thickness": "Outline Thickness", + "bold": "Bold Text", + "margin": "Bottom Margin" + }, "video_player": { "title": "Video Player", "video_player": "Video Player", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index f2e8d332..f56f7226 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -189,6 +189,15 @@ export type Settings = { ksSubtitleColor: string; ksSubtitleBackgroundColor: string; ksSubtitleFontName: string; + // VLC subtitle settings + vlcTextColor?: string; + vlcBackgroundColor?: string; + vlcBackgroundOpacity?: number; + vlcOutlineColor?: string; + vlcOutlineOpacity?: number; + vlcOutlineThickness?: "None" | "Thin" | "Normal" | "Thick"; + vlcIsBold?: boolean; + vlcSubtitleMargin?: number; // Gesture controls enableHorizontalSwipeSkip: boolean; enableLeftSideBrightnessSwipe: boolean; @@ -278,6 +287,15 @@ export const defaultValues: Settings = { ksSubtitleColor: "#FFFFFF", ksSubtitleBackgroundColor: "#00000080", ksSubtitleFontName: "System", + // VLC subtitle defaults + vlcTextColor: "White", + vlcBackgroundColor: "Black", + vlcBackgroundOpacity: 128, + vlcOutlineColor: "Black", + vlcOutlineOpacity: 255, + vlcOutlineThickness: "Normal", + vlcIsBold: false, + vlcSubtitleMargin: 40, // Gesture controls enableHorizontalSwipeSkip: true, enableLeftSideBrightnessSwipe: true,