mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
feat(player): vlc subtitle options
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
245
components/settings/VlcSubtitleSettings.tsx
Normal file
245
components/settings/VlcSubtitleSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
40
constants/SubtitleConstants.ts
Normal file
40
constants/SubtitleConstants.ts
Normal 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">;
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user