mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-28 16:50:29 +01:00
feat(mpv): add opaque subtitle background with adjustable opacity (iOS only)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Platform, View, type ViewProps } from "react-native";
|
import { Platform, Switch, View, type ViewProps } from "react-native";
|
||||||
import { Stepper } from "@/components/inputs/Stepper";
|
import { Stepper } from "@/components/inputs/Stepper";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
@@ -55,7 +55,6 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
return [{ options }];
|
return [{ options }];
|
||||||
}, [settings?.mpvSubtitleAlignY, updateSettings]);
|
}, [settings?.mpvSubtitleAlignY, updateSettings]);
|
||||||
|
|
||||||
if (isTv) return null;
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,53 +67,83 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem title='Vertical Margin'>
|
{!isTv && (
|
||||||
<Stepper
|
<>
|
||||||
value={settings.mpvSubtitleMarginY ?? 0}
|
<ListItem title='Vertical Margin'>
|
||||||
step={5}
|
<Stepper
|
||||||
min={0}
|
value={settings.mpvSubtitleMarginY ?? 0}
|
||||||
max={100}
|
step={5}
|
||||||
onUpdate={(value) => updateSettings({ mpvSubtitleMarginY: value })}
|
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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ListItem title='Opaque Background'>
|
||||||
|
<Switch
|
||||||
|
value={settings.mpvSubtitleBackgroundEnabled ?? false}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ mpvSubtitleBackgroundEnabled: value })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title='Horizontal Alignment'>
|
{settings.mpvSubtitleBackgroundEnabled && (
|
||||||
<PlatformDropdown
|
<ListItem title='Background Opacity'>
|
||||||
groups={alignXOptionGroups}
|
<Stepper
|
||||||
trigger={
|
value={settings.mpvSubtitleBackgroundOpacity ?? 75}
|
||||||
<View className='flex flex-row items-center justify-between py-1.5 pl-3'>
|
step={5}
|
||||||
<Text className='mr-1 text-[#8E8D91]'>
|
min={10}
|
||||||
{alignXLabels[settings?.mpvSubtitleAlignX ?? "center"]}
|
max={100}
|
||||||
</Text>
|
appendValue='%'
|
||||||
<Ionicons
|
onUpdate={(value) =>
|
||||||
name='chevron-expand-sharp'
|
updateSettings({ mpvSubtitleBackgroundOpacity: value })
|
||||||
size={18}
|
}
|
||||||
color='#5A5960'
|
/>
|
||||||
/>
|
</ListItem>
|
||||||
</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>
|
</ListGroup>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ final class MPVLayerRenderer {
|
|||||||
// CRITICAL: This option MUST be set immediately after vo=avfoundation, before hwdec options.
|
// CRITICAL: This option MUST be set immediately after vo=avfoundation, before hwdec options.
|
||||||
// On tvOS, moving this elsewhere causes the app to freeze when exiting the player.
|
// On tvOS, moving this elsewhere causes the app to freeze when exiting the player.
|
||||||
// - iOS: "yes" for PiP subtitle support (subtitles baked into video)
|
// - iOS: "yes" for PiP subtitle support (subtitles baked into video)
|
||||||
// - tvOS: "no" to prevent gray tint + frame drops with subtitles
|
// - tvOS: "no" - composite OSD breaks subtitle rendering entirely on tvOS
|
||||||
|
// Note: This means subtitle styling (background colors) won't work on tvOS
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "no"))
|
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "no"))
|
||||||
#else
|
#else
|
||||||
@@ -820,7 +821,22 @@ final class MPVLayerRenderer {
|
|||||||
func setSubtitleFontSize(_ size: Int) {
|
func setSubtitleFontSize(_ size: Int) {
|
||||||
setProperty(name: "sub-font-size", value: String(size))
|
setProperty(name: "sub-font-size", value: String(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSubtitleBackgroundColor(_ color: String) {
|
||||||
|
setProperty(name: "sub-back-color", value: color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubtitleBorderStyle(_ style: String) {
|
||||||
|
// "outline-and-shadow" (default) or "background-box" (enables background color)
|
||||||
|
setProperty(name: "sub-border-style", value: style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubtitleAssOverride(_ mode: String) {
|
||||||
|
// Controls whether to override ASS subtitle styles
|
||||||
|
// "no" = keep ASS styles, "force" = override with user settings
|
||||||
|
setProperty(name: "sub-ass-override", value: mode)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Audio Track Controls
|
// MARK: - Audio Track Controls
|
||||||
|
|
||||||
func getAudioTracks() -> [[String: Any]] {
|
func getAudioTracks() -> [[String: Any]] {
|
||||||
|
|||||||
@@ -157,7 +157,19 @@ public class MpvPlayerModule: Module {
|
|||||||
AsyncFunction("setSubtitleFontSize") { (view: MpvPlayerView, size: Int) in
|
AsyncFunction("setSubtitleFontSize") { (view: MpvPlayerView, size: Int) in
|
||||||
view.setSubtitleFontSize(size)
|
view.setSubtitleFontSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setSubtitleBackgroundColor") { (view: MpvPlayerView, color: String) in
|
||||||
|
view.setSubtitleBackgroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setSubtitleBorderStyle") { (view: MpvPlayerView, style: String) in
|
||||||
|
view.setSubtitleBorderStyle(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setSubtitleAssOverride") { (view: MpvPlayerView, mode: String) in
|
||||||
|
view.setSubtitleAssOverride(mode)
|
||||||
|
}
|
||||||
|
|
||||||
// Audio track functions
|
// Audio track functions
|
||||||
AsyncFunction("getAudioTracks") { (view: MpvPlayerView) -> [[String: Any]] in
|
AsyncFunction("getAudioTracks") { (view: MpvPlayerView) -> [[String: Any]] in
|
||||||
return view.getAudioTracks()
|
return view.getAudioTracks()
|
||||||
|
|||||||
@@ -319,6 +319,18 @@ class MpvPlayerView: ExpoView {
|
|||||||
renderer?.setSubtitleFontSize(size)
|
renderer?.setSubtitleFontSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSubtitleBackgroundColor(_ color: String) {
|
||||||
|
renderer?.setSubtitleBackgroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubtitleBorderStyle(_ style: String) {
|
||||||
|
renderer?.setSubtitleBorderStyle(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSubtitleAssOverride(_ mode: String) {
|
||||||
|
renderer?.setSubtitleAssOverride(mode)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Video Scaling
|
// MARK: - Video Scaling
|
||||||
|
|
||||||
func setZoomedToFill(_ zoomed: Bool) {
|
func setZoomedToFill(_ zoomed: Bool) {
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ export interface MpvPlayerViewRef {
|
|||||||
setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise<void>;
|
setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise<void>;
|
||||||
setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => Promise<void>;
|
setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => Promise<void>;
|
||||||
setSubtitleFontSize: (size: number) => Promise<void>;
|
setSubtitleFontSize: (size: number) => Promise<void>;
|
||||||
|
setSubtitleBackgroundColor: (color: string) => Promise<void>;
|
||||||
|
setSubtitleBorderStyle: (
|
||||||
|
style: "outline-and-shadow" | "background-box",
|
||||||
|
) => Promise<void>;
|
||||||
|
setSubtitleAssOverride: (mode: "no" | "force") => Promise<void>;
|
||||||
// Audio controls
|
// Audio controls
|
||||||
getAudioTracks: () => Promise<AudioTrack[]>;
|
getAudioTracks: () => Promise<AudioTrack[]>;
|
||||||
setAudioTrack: (trackId: number) => Promise<void>;
|
setAudioTrack: (trackId: number) => Promise<void>;
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ export default React.forwardRef<MpvPlayerViewRef, MpvPlayerViewProps>(
|
|||||||
setSubtitleFontSize: async (size: number) => {
|
setSubtitleFontSize: async (size: number) => {
|
||||||
await nativeRef.current?.setSubtitleFontSize(size);
|
await nativeRef.current?.setSubtitleFontSize(size);
|
||||||
},
|
},
|
||||||
|
setSubtitleBackgroundColor: async (color: string) => {
|
||||||
|
await nativeRef.current?.setSubtitleBackgroundColor(color);
|
||||||
|
},
|
||||||
|
setSubtitleBorderStyle: async (
|
||||||
|
style: "outline-and-shadow" | "background-box",
|
||||||
|
) => {
|
||||||
|
await nativeRef.current?.setSubtitleBorderStyle(style);
|
||||||
|
},
|
||||||
|
setSubtitleAssOverride: async (mode: "no" | "force") => {
|
||||||
|
await nativeRef.current?.setSubtitleAssOverride(mode);
|
||||||
|
},
|
||||||
// Audio controls
|
// Audio controls
|
||||||
getAudioTracks: async () => {
|
getAudioTracks: async () => {
|
||||||
return await nativeRef.current?.getAudioTracks();
|
return await nativeRef.current?.getAudioTracks();
|
||||||
|
|||||||
@@ -705,7 +705,8 @@
|
|||||||
"skip_credits": "Skip Credits",
|
"skip_credits": "Skip Credits",
|
||||||
"stopPlayback": "Stop Playback",
|
"stopPlayback": "Stop Playback",
|
||||||
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
"stopPlayingTitle": "Stop playing \"{{title}}\"?",
|
||||||
"stopPlayingConfirm": "Are you sure you want to stop playback?"
|
"stopPlayingConfirm": "Are you sure you want to stop playback?",
|
||||||
|
"downloaded": "Downloaded"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Next Up",
|
"next_up": "Next Up",
|
||||||
|
|||||||
@@ -678,7 +678,8 @@
|
|||||||
"skip_credits": "Hoppa över eftertexter",
|
"skip_credits": "Hoppa över eftertexter",
|
||||||
"stopPlayback": "Stoppa uppspelning",
|
"stopPlayback": "Stoppa uppspelning",
|
||||||
"stopPlayingTitle": "Sluta spela \"{{title}}\"?",
|
"stopPlayingTitle": "Sluta spela \"{{title}}\"?",
|
||||||
"stopPlayingConfirm": "Är du säker på att du vill stoppa uppspelningen?"
|
"stopPlayingConfirm": "Är du säker på att du vill stoppa uppspelningen?",
|
||||||
|
"downloaded": "Nedladdad"
|
||||||
},
|
},
|
||||||
"item_card": {
|
"item_card": {
|
||||||
"next_up": "Näst på tur",
|
"next_up": "Näst på tur",
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ export type Settings = {
|
|||||||
mpvSubtitleAlignX?: "left" | "center" | "right";
|
mpvSubtitleAlignX?: "left" | "center" | "right";
|
||||||
mpvSubtitleAlignY?: "top" | "center" | "bottom";
|
mpvSubtitleAlignY?: "top" | "center" | "bottom";
|
||||||
mpvSubtitleFontSize?: number;
|
mpvSubtitleFontSize?: number;
|
||||||
|
mpvSubtitleBackgroundEnabled?: boolean;
|
||||||
|
mpvSubtitleBackgroundOpacity?: number; // 0-100
|
||||||
// MPV buffer/cache settings
|
// MPV buffer/cache settings
|
||||||
mpvCacheEnabled?: MpvCacheMode;
|
mpvCacheEnabled?: MpvCacheMode;
|
||||||
mpvCacheSeconds?: number;
|
mpvCacheSeconds?: number;
|
||||||
@@ -313,6 +315,8 @@ export const defaultValues: Settings = {
|
|||||||
mpvSubtitleAlignX: undefined,
|
mpvSubtitleAlignX: undefined,
|
||||||
mpvSubtitleAlignY: undefined,
|
mpvSubtitleAlignY: undefined,
|
||||||
mpvSubtitleFontSize: undefined,
|
mpvSubtitleFontSize: undefined,
|
||||||
|
mpvSubtitleBackgroundEnabled: false,
|
||||||
|
mpvSubtitleBackgroundOpacity: 75,
|
||||||
// MPV buffer/cache defaults
|
// MPV buffer/cache defaults
|
||||||
mpvCacheEnabled: "auto",
|
mpvCacheEnabled: "auto",
|
||||||
mpvCacheSeconds: 10,
|
mpvCacheSeconds: 10,
|
||||||
|
|||||||
Reference in New Issue
Block a user