feat: MPV player for both Android and iOS with added HW decoding PiP (with subtitles) (#1332)

Co-authored-by: Alex Kim <alexkim@Alexs-MacBook-Pro.local>
Co-authored-by: Alex <111128610+Alexk2309@users.noreply.github.com>
Co-authored-by: Simon-Eklundh <simon.eklundh@proton.me>
This commit is contained in:
Fredrik Burmester
2026-01-10 19:35:27 +01:00
committed by GitHub
parent df2f44e086
commit f1575ca48b
98 changed files with 3257 additions and 7448 deletions

View File

@@ -1,40 +0,0 @@
import type React from "react";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Platform, Switch } from "react-native";
import { setHardwareDecode } from "@/modules/sf-player";
import { useSettings } from "@/utils/atoms/settings";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
export const KSPlayerSettings: React.FC = () => {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation();
const handleHardwareDecodeChange = useCallback(
(value: boolean) => {
updateSettings({ ksHardwareDecode: value });
setHardwareDecode(value);
},
[updateSettings],
);
if (Platform.OS !== "ios" || !settings) return null;
return (
<ListGroup
title={t("home.settings.subtitles.ksplayer_title")}
className='mt-4'
>
<ListItem
title={t("home.settings.subtitles.hardware_decode")}
subtitle={t("home.settings.subtitles.hardware_decode_description")}
>
<Switch
value={settings.ksHardwareDecode}
onValueChange={handleHardwareDecodeChange}
/>
</ListItem>
</ListGroup>
);
};

View File

@@ -0,0 +1,133 @@
import { Ionicons } from "@expo/vector-icons";
import { useMemo } from "react";
import { Platform, View, type ViewProps } from "react-native";
import { Stepper } from "@/components/inputs/Stepper";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
import { PlatformDropdown } from "../PlatformDropdown";
import { useMedia } from "./MediaContext";
interface Props extends ViewProps {}
type AlignX = "left" | "center" | "right";
type AlignY = "top" | "center" | "bottom";
export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
const isTv = Platform.isTV;
const media = useMedia();
const { settings, updateSettings } = media;
const alignXOptions: AlignX[] = ["left", "center", "right"];
const alignYOptions: AlignY[] = ["top", "center", "bottom"];
const alignXLabels: Record<AlignX, string> = {
left: "Left",
center: "Center",
right: "Right",
};
const alignYLabels: Record<AlignY, string> = {
top: "Top",
center: "Center",
bottom: "Bottom",
};
const alignXOptionGroups = useMemo(() => {
const options = alignXOptions.map((align) => ({
type: "radio" as const,
label: alignXLabels[align],
value: align,
selected: align === (settings?.mpvSubtitleAlignX ?? "center"),
onPress: () => updateSettings({ mpvSubtitleAlignX: align }),
}));
return [{ options }];
}, [settings?.mpvSubtitleAlignX, updateSettings]);
const alignYOptionGroups = useMemo(() => {
const options = alignYOptions.map((align) => ({
type: "radio" as const,
label: alignYLabels[align],
value: align,
selected: align === (settings?.mpvSubtitleAlignY ?? "bottom"),
onPress: () => updateSettings({ mpvSubtitleAlignY: align }),
}));
return [{ options }];
}, [settings?.mpvSubtitleAlignY, updateSettings]);
if (isTv) return null;
if (!settings) return null;
return (
<View {...props}>
<ListGroup
title='MPV Subtitle Settings'
description={
<Text className='text-[#8E8D91] text-xs'>
Advanced subtitle customization for MPV player
</Text>
}
>
<ListItem title='Subtitle Scale'>
<Stepper
value={settings.mpvSubtitleScale ?? 1.0}
step={0.1}
min={0.5}
max={2.0}
onUpdate={(value) =>
updateSettings({ mpvSubtitleScale: Math.round(value * 10) / 10 })
}
/>
</ListItem>
<ListItem title='Vertical Margin'>
<Stepper
value={settings.mpvSubtitleMarginY ?? 0}
step={5}
min={0}
max={100}
onUpdate={(value) => updateSettings({ mpvSubtitleMarginY: value })}
/>
</ListItem>
<ListItem title='Horizontal Alignment'>
<PlatformDropdown
groups={alignXOptionGroups}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{alignXLabels[settings?.mpvSubtitleAlignX ?? "center"]}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title='Horizontal Alignment'
/>
</ListItem>
<ListItem title='Vertical Alignment'>
<PlatformDropdown
groups={alignYOptionGroups}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{alignYLabels[settings?.mpvSubtitleAlignY ?? "bottom"]}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</View>
}
title='Vertical Alignment'
/>
</ListItem>
</ListGroup>
</View>
);
};

View File

@@ -141,36 +141,6 @@ export const OtherSettings: React.FC = () => {
/>
</ListItem>
{/* {(Platform.OS === "ios" || Platform.isTVOS)&& (
<ListItem
title={t("home.settings.other.video_player")}
disabled={pluginSettings?.defaultPlayer?.locked}
>
<Dropdown
data={Object.values(VideoPlayer).filter(isNumber)}
disabled={pluginSettings?.defaultPlayer?.locked}
keyExtractor={String}
titleExtractor={(item) => t(`home.settings.other.video_players.${VideoPlayer[item]}`)}
title={
<TouchableOpacity className="flex flex-row items-center justify-between py-1.5 pl-3">
<Text className="mr-1 text-[#8E8D91]">
{t(`home.settings.other.video_players.${VideoPlayer[settings.defaultPlayer]}`)}
</Text>
<Ionicons
name="chevron-expand-sharp"
size={18}
color="#5A5960"
/>
</TouchableOpacity>
}
label={t("home.settings.other.orientation")}
onSelected={(defaultPlayer) =>
updateSettings({ defaultPlayer })
}
/>
</ListItem>
)} */}
<ListItem
title={t("home.settings.other.show_custom_menu_links")}
disabled={pluginSettings?.showCustomMenuLinks?.locked}

View File

@@ -13,7 +13,6 @@ import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
import { VideoPlayerSettings } from "./VideoPlayerSettings";
export const PlaybackControlsSettings: React.FC = () => {
const { settings, updateSettings, pluginSettings } = useSettings();
@@ -231,8 +230,6 @@ export const PlaybackControlsSettings: React.FC = () => {
/>
</ListItem>
</ListGroup>
<VideoPlayerSettings />
</DisabledSetting>
);
};

View File

@@ -1,93 +0,0 @@
import { Ionicons } from "@expo/vector-icons";
import type React from "react";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Platform, Switch, View } from "react-native";
import { setHardwareDecode } from "@/modules/sf-player";
import { useSettings, VideoPlayerIOS } from "@/utils/atoms/settings";
import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem";
import { PlatformDropdown } from "../PlatformDropdown";
export const VideoPlayerSettings: React.FC = () => {
const { settings, updateSettings } = useSettings();
const { t } = useTranslation();
const handleHardwareDecodeChange = useCallback(
(value: boolean) => {
updateSettings({ ksHardwareDecode: value });
setHardwareDecode(value);
},
[updateSettings],
);
const videoPlayerOptions = useMemo(
() => [
{
options: [
{
type: "radio" as const,
label: t("home.settings.video_player.ksplayer"),
value: VideoPlayerIOS.KSPlayer,
selected: settings?.videoPlayerIOS === VideoPlayerIOS.KSPlayer,
onPress: () =>
updateSettings({ videoPlayerIOS: VideoPlayerIOS.KSPlayer }),
},
{
type: "radio" as const,
label: t("home.settings.video_player.vlc"),
value: VideoPlayerIOS.VLC,
selected: settings?.videoPlayerIOS === VideoPlayerIOS.VLC,
onPress: () =>
updateSettings({ videoPlayerIOS: VideoPlayerIOS.VLC }),
},
],
},
],
[settings?.videoPlayerIOS, t, updateSettings],
);
const getPlayerLabel = useCallback(() => {
switch (settings?.videoPlayerIOS) {
case VideoPlayerIOS.VLC:
return t("home.settings.video_player.vlc");
default:
return t("home.settings.video_player.ksplayer");
}
}, [settings?.videoPlayerIOS, t]);
if (Platform.OS !== "ios" || !settings) return null;
return (
<ListGroup title={t("home.settings.video_player.title")} className='mt-4'>
<ListItem
title={t("home.settings.video_player.video_player")}
subtitle={t("home.settings.video_player.video_player_description")}
>
<PlatformDropdown
groups={videoPlayerOptions}
trigger={
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>{getPlayerLabel()}</Text>
<Ionicons name='chevron-expand-sharp' size={18} color='#5A5960' />
</View>
}
title={t("home.settings.video_player.video_player")}
/>
</ListItem>
{settings.videoPlayerIOS === VideoPlayerIOS.KSPlayer && (
<ListItem
title={t("home.settings.subtitles.hardware_decode")}
subtitle={t("home.settings.subtitles.hardware_decode_description")}
>
<Switch
value={settings.ksHardwareDecode}
onValueChange={handleHardwareDecodeChange}
/>
</ListItem>
)}
</ListGroup>
);
};

View File

@@ -1,245 +0,0 @@
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>
);
};