feat(player): vlc subtitle options

This commit is contained in:
Fredrik Burmester
2026-01-07 22:17:27 +01:00
parent 588c8ffeb5
commit be8651357b
6 changed files with 379 additions and 2 deletions

View File

@@ -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() {
<MediaProvider>
<AudioToggles className='mb-4' />
<SubtitleToggles className='mb-4' />
<VlcSubtitleSettings className='mb-4' />
</MediaProvider>
</View>
</ScrollView>

View File

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

View File

@@ -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> = ({ ...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 (
<View {...props}>
<ListGroup
title={t("home.settings.vlc_subtitles.title")}
description={
<Text className='text-[#8E8D91] text-xs'>
{t("home.settings.vlc_subtitles.hint")}
</Text>
}
>
{/* Text Color */}
<ListItem title={t("home.settings.vlc_subtitles.text_color")}>
<PlatformDropdown
groups={textColorOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{settings.vlcTextColor || "White"}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.vlc_subtitles.text_color")}
/>
</ListItem>
{/* Background Color */}
<ListItem title={t("home.settings.vlc_subtitles.background_color")}>
<PlatformDropdown
groups={backgroundColorOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{settings.vlcBackgroundColor || "Black"}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.vlc_subtitles.background_color")}
/>
</ListItem>
{/* Background Opacity */}
<ListItem title={t("home.settings.vlc_subtitles.background_opacity")}>
<Stepper
value={Math.round(
((settings.vlcBackgroundOpacity ?? 128) / 255) * 100,
)}
step={10}
min={0}
max={100}
appendValue='%'
onUpdate={(value) =>
updateSettings({
vlcBackgroundOpacity: Math.round((value / 100) * 255),
})
}
/>
</ListItem>
{/* Outline Color */}
<ListItem title={t("home.settings.vlc_subtitles.outline_color")}>
<PlatformDropdown
groups={outlineColorOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{settings.vlcOutlineColor || "Black"}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.vlc_subtitles.outline_color")}
/>
</ListItem>
{/* Outline Opacity */}
<ListItem title={t("home.settings.vlc_subtitles.outline_opacity")}>
<Stepper
value={Math.round(
((settings.vlcOutlineOpacity ?? 255) / 255) * 100,
)}
step={10}
min={0}
max={100}
appendValue='%'
onUpdate={(value) =>
updateSettings({
vlcOutlineOpacity: Math.round((value / 100) * 255),
})
}
/>
</ListItem>
{/* Outline Thickness */}
<ListItem title={t("home.settings.vlc_subtitles.outline_thickness")}>
<PlatformDropdown
groups={outlineThicknessOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{settings.vlcOutlineThickness || "Normal"}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title={t("home.settings.vlc_subtitles.outline_thickness")}
/>
</ListItem>
{/* Bold Text */}
<ListItem title={t("home.settings.vlc_subtitles.bold")}>
<Switch
value={settings.vlcIsBold ?? false}
onValueChange={(value) => updateSettings({ vlcIsBold: value })}
/>
</ListItem>
{/* Subtitle Margin */}
<ListItem title={t("home.settings.vlc_subtitles.margin")}>
<Stepper
value={settings.vlcSubtitleMargin ?? 40}
step={10}
min={0}
max={200}
onUpdate={(value) =>
updateSettings({ vlcSubtitleMargin: Math.round(value) })
}
/>
</ListItem>
</ListGroup>
</View>
);
};

View File

@@ -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<string, number> = {
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<string, number> = {
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">;

View File

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

View File

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