feat(mpv): add opaque subtitle background with adjustable opacity (iOS only)

This commit is contained in:
Fredrik Burmester
2026-02-01 17:29:31 +01:00
parent ab526f2c6b
commit bc575c26c1
9 changed files with 142 additions and 51 deletions

View File

@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
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 { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup";
@@ -55,7 +55,6 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
return [{ options }];
}, [settings?.mpvSubtitleAlignY, updateSettings]);
if (isTv) return null;
if (!settings) return null;
return (
@@ -68,53 +67,83 @@ export const MpvSubtitleSettings: React.FC<Props> = ({ ...props }) => {
</Text>
}
>
<ListItem title='Vertical Margin'>
<Stepper
value={settings.mpvSubtitleMarginY ?? 0}
step={5}
min={0}
max={100}
onUpdate={(value) => updateSettings({ mpvSubtitleMarginY: value })}
{!isTv && (
<>
<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>
</>
)}
<ListItem title='Opaque Background'>
<Switch
value={settings.mpvSubtitleBackgroundEnabled ?? false}
onValueChange={(value) =>
updateSettings({ mpvSubtitleBackgroundEnabled: 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>
{settings.mpvSubtitleBackgroundEnabled && (
<ListItem title='Background Opacity'>
<Stepper
value={settings.mpvSubtitleBackgroundOpacity ?? 75}
step={5}
min={10}
max={100}
appendValue='%'
onUpdate={(value) =>
updateSettings({ mpvSubtitleBackgroundOpacity: value })
}
/>
</ListItem>
)}
</ListGroup>
</View>
);

View File

@@ -195,7 +195,8 @@ final class MPVLayerRenderer {
// 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.
// - 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)
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "no"))
#else
@@ -820,7 +821,22 @@ final class MPVLayerRenderer {
func setSubtitleFontSize(_ size: Int) {
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
func getAudioTracks() -> [[String: Any]] {

View File

@@ -157,7 +157,19 @@ public class MpvPlayerModule: Module {
AsyncFunction("setSubtitleFontSize") { (view: MpvPlayerView, size: Int) in
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
AsyncFunction("getAudioTracks") { (view: MpvPlayerView) -> [[String: Any]] in
return view.getAudioTracks()

View File

@@ -319,6 +319,18 @@ class MpvPlayerView: ExpoView {
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
func setZoomedToFill(_ zoomed: Bool) {

View File

@@ -95,6 +95,11 @@ export interface MpvPlayerViewRef {
setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise<void>;
setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => 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
getAudioTracks: () => Promise<AudioTrack[]>;
setAudioTrack: (trackId: number) => Promise<void>;

View File

@@ -84,6 +84,17 @@ export default React.forwardRef<MpvPlayerViewRef, MpvPlayerViewProps>(
setSubtitleFontSize: async (size: number) => {
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
getAudioTracks: async () => {
return await nativeRef.current?.getAudioTracks();

View File

@@ -705,7 +705,8 @@
"skip_credits": "Skip Credits",
"stopPlayback": "Stop Playback",
"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": {
"next_up": "Next Up",

View File

@@ -678,7 +678,8 @@
"skip_credits": "Hoppa över eftertexter",
"stopPlayback": "Stoppa uppspelning",
"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": {
"next_up": "Näst på tur",

View File

@@ -214,6 +214,8 @@ export type Settings = {
mpvSubtitleAlignX?: "left" | "center" | "right";
mpvSubtitleAlignY?: "top" | "center" | "bottom";
mpvSubtitleFontSize?: number;
mpvSubtitleBackgroundEnabled?: boolean;
mpvSubtitleBackgroundOpacity?: number; // 0-100
// MPV buffer/cache settings
mpvCacheEnabled?: MpvCacheMode;
mpvCacheSeconds?: number;
@@ -313,6 +315,8 @@ export const defaultValues: Settings = {
mpvSubtitleAlignX: undefined,
mpvSubtitleAlignY: undefined,
mpvSubtitleFontSize: undefined,
mpvSubtitleBackgroundEnabled: false,
mpvSubtitleBackgroundOpacity: 75,
// MPV buffer/cache defaults
mpvCacheEnabled: "auto",
mpvCacheSeconds: 10,