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,