From bc575c26c1ce8fe69caadc2a55f7258c13f5ee89 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sun, 1 Feb 2026 17:29:31 +0100 Subject: [PATCH] feat(mpv): add opaque subtitle background with adjustable opacity (iOS only) --- components/settings/MpvSubtitleSettings.tsx | 121 +++++++++++------- modules/mpv-player/ios/MPVLayerRenderer.swift | 20 ++- modules/mpv-player/ios/MpvPlayerModule.swift | 14 +- modules/mpv-player/ios/MpvPlayerView.swift | 12 ++ modules/mpv-player/src/MpvPlayer.types.ts | 5 + modules/mpv-player/src/MpvPlayerView.tsx | 11 ++ translations/en.json | 3 +- translations/sv.json | 3 +- utils/atoms/settings.ts | 4 + 9 files changed, 142 insertions(+), 51 deletions(-) diff --git a/components/settings/MpvSubtitleSettings.tsx b/components/settings/MpvSubtitleSettings.tsx index c715cbe7..4ef2a800 100644 --- a/components/settings/MpvSubtitleSettings.tsx +++ b/components/settings/MpvSubtitleSettings.tsx @@ -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 }) => { return [{ options }]; }, [settings?.mpvSubtitleAlignY, updateSettings]); - if (isTv) return null; if (!settings) return null; return ( @@ -68,53 +67,83 @@ export const MpvSubtitleSettings: React.FC = ({ ...props }) => { } > - - updateSettings({ mpvSubtitleMarginY: value })} + {!isTv && ( + <> + + + updateSettings({ mpvSubtitleMarginY: value }) + } + /> + + + + + + {alignXLabels[settings?.mpvSubtitleAlignX ?? "center"]} + + + + } + title='Horizontal Alignment' + /> + + + + + + {alignYLabels[settings?.mpvSubtitleAlignY ?? "bottom"]} + + + + } + title='Vertical Alignment' + /> + + + )} + + + + updateSettings({ mpvSubtitleBackgroundEnabled: value }) + } /> - - - - {alignXLabels[settings?.mpvSubtitleAlignX ?? "center"]} - - - - } - title='Horizontal Alignment' - /> - - - - - - {alignYLabels[settings?.mpvSubtitleAlignY ?? "bottom"]} - - - - } - title='Vertical Alignment' - /> - + {settings.mpvSubtitleBackgroundEnabled && ( + + + updateSettings({ mpvSubtitleBackgroundOpacity: value }) + } + /> + + )} ); diff --git a/modules/mpv-player/ios/MPVLayerRenderer.swift b/modules/mpv-player/ios/MPVLayerRenderer.swift index 52b3afec..4d0f9577 100644 --- a/modules/mpv-player/ios/MPVLayerRenderer.swift +++ b/modules/mpv-player/ios/MPVLayerRenderer.swift @@ -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]] { diff --git a/modules/mpv-player/ios/MpvPlayerModule.swift b/modules/mpv-player/ios/MpvPlayerModule.swift index c85c7fa3..08665e28 100644 --- a/modules/mpv-player/ios/MpvPlayerModule.swift +++ b/modules/mpv-player/ios/MpvPlayerModule.swift @@ -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() diff --git a/modules/mpv-player/ios/MpvPlayerView.swift b/modules/mpv-player/ios/MpvPlayerView.swift index a0655803..5fdf5a97 100644 --- a/modules/mpv-player/ios/MpvPlayerView.swift +++ b/modules/mpv-player/ios/MpvPlayerView.swift @@ -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) { diff --git a/modules/mpv-player/src/MpvPlayer.types.ts b/modules/mpv-player/src/MpvPlayer.types.ts index c700cb82..552cbefa 100644 --- a/modules/mpv-player/src/MpvPlayer.types.ts +++ b/modules/mpv-player/src/MpvPlayer.types.ts @@ -95,6 +95,11 @@ export interface MpvPlayerViewRef { setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise; setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => Promise; setSubtitleFontSize: (size: number) => Promise; + setSubtitleBackgroundColor: (color: string) => Promise; + setSubtitleBorderStyle: ( + style: "outline-and-shadow" | "background-box", + ) => Promise; + setSubtitleAssOverride: (mode: "no" | "force") => Promise; // Audio controls getAudioTracks: () => Promise; setAudioTrack: (trackId: number) => Promise; diff --git a/modules/mpv-player/src/MpvPlayerView.tsx b/modules/mpv-player/src/MpvPlayerView.tsx index ad3fcdfa..cec13b0f 100644 --- a/modules/mpv-player/src/MpvPlayerView.tsx +++ b/modules/mpv-player/src/MpvPlayerView.tsx @@ -84,6 +84,17 @@ export default React.forwardRef( 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(); diff --git a/translations/en.json b/translations/en.json index b4c63420..e5352295 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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", diff --git a/translations/sv.json b/translations/sv.json index f356a528..2c95fe77 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -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", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 31540ccb..f7dbf791 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -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,