diff --git a/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt b/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt index 48b795ba..f49a22c9 100644 --- a/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt +++ b/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt @@ -342,6 +342,119 @@ class MPVView @JvmOverloads constructor( } } + // Subtitle Styling Methods + + fun setSubtitleSize(size: Int) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle size: $size") + MPVLib.setPropertyInt("sub-font-size", size) + } + } + + fun setSubtitleColor(color: String) { + if (isMpvInitialized) { + // MPV expects color in #AARRGGBB format, but we receive #RRGGBB + // Convert to MPV format with full opacity + val mpvColor = if (color.length == 7) "#FF${color.substring(1)}" else color + Log.d(TAG, "Setting subtitle color: $mpvColor") + MPVLib.setPropertyString("sub-color", mpvColor) + } + } + + fun setSubtitleBackgroundColor(color: String, opacity: Float) { + if (isMpvInitialized) { + // Convert opacity (0-1) to hex (00-FF) + val alphaHex = (opacity * 255).toInt().coerceIn(0, 255).let { + String.format("%02X", it) + } + // MPV format: #AARRGGBB + val baseColor = if (color.startsWith("#")) color.substring(1) else color + val mpvColor = "#${alphaHex}${baseColor.takeLast(6)}" + Log.d(TAG, "Setting subtitle background: $mpvColor (opacity: $opacity)") + MPVLib.setPropertyString("sub-back-color", mpvColor) + } + } + + fun setSubtitleBorderSize(size: Int) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle border size: $size") + MPVLib.setPropertyInt("sub-border-size", size) + } + } + + fun setSubtitleBorderColor(color: String) { + if (isMpvInitialized) { + val mpvColor = if (color.length == 7) "#FF${color.substring(1)}" else color + Log.d(TAG, "Setting subtitle border color: $mpvColor") + MPVLib.setPropertyString("sub-border-color", mpvColor) + } + } + + fun setSubtitleShadow(enabled: Boolean, offset: Int) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle shadow: enabled=$enabled, offset=$offset") + if (enabled) { + MPVLib.setPropertyInt("sub-shadow-offset", offset) + MPVLib.setPropertyString("sub-shadow-color", "#80000000") + } else { + MPVLib.setPropertyInt("sub-shadow-offset", 0) + } + } + } + + fun setSubtitlePosition(pos: Int) { + if (isMpvInitialized) { + // sub-pos: 0=top, 100=bottom, can go beyond 100 for more offset + // UI sends bottomOffset (0=at bottom, higher=more up from bottom) + // Convert: MPV pos = 100 - (bottomOffset / screenHeightFactor) + // Simplified: just pass pos directly, UI should convert + Log.d(TAG, "Setting subtitle position: $pos") + MPVLib.setPropertyInt("sub-pos", pos) + } + } + + fun setSubtitleDelay(delaySec: Double) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle delay: $delaySec seconds") + MPVLib.setPropertyDouble("sub-delay", delaySec) + } + } + + fun setSubtitleScale(scale: Double) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle scale: $scale") + MPVLib.setPropertyDouble("sub-scale", scale) + } + } + + fun setSubtitleAlignment(align: String) { + if (isMpvInitialized) { + // MPV sub-justify values: left, center, right, auto + val mpvAlign = when (align) { + "left" -> "left" + "right" -> "right" + "center" -> "center" + else -> "center" + } + Log.d(TAG, "Setting subtitle alignment: $mpvAlign") + MPVLib.setPropertyString("sub-justify", mpvAlign) + } + } + + fun setSubtitleBold(bold: Boolean) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle bold: $bold") + MPVLib.setPropertyString("sub-bold", if (bold) "yes" else "no") + } + } + + fun setSubtitleItalic(italic: Boolean) { + if (isMpvInitialized) { + Log.d(TAG, "Setting subtitle italic: $italic") + MPVLib.setPropertyString("sub-italic", if (italic) "yes" else "no") + } + } + // MPVLib.EventObserver implementation override fun eventProperty(property: String) { diff --git a/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt b/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt index ef8a3b1e..5054e04c 100644 --- a/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt +++ b/android/app/src/main/java/com/nuvio/app/mpv/MpvPlayerViewManager.kt @@ -190,4 +190,52 @@ class MpvPlayerViewManager( fun setGpuMode(view: MPVView, gpuMode: String?) { view.gpuMode = gpuMode ?: "gpu" } + + // Subtitle Styling Props + + @ReactProp(name = "subtitleSize", defaultInt = 48) + fun setSubtitleSize(view: MPVView, size: Int) { + view.setSubtitleSize(size) + } + + @ReactProp(name = "subtitleColor") + fun setSubtitleColor(view: MPVView, color: String?) { + view.setSubtitleColor(color ?: "#FFFFFF") + } + + @ReactProp(name = "subtitleBackgroundOpacity", defaultFloat = 0.0f) + fun setSubtitleBackgroundOpacity(view: MPVView, opacity: Float) { + // Black background with user-specified opacity + view.setSubtitleBackgroundColor("#000000", opacity) + } + + @ReactProp(name = "subtitleBorderSize", defaultInt = 3) + fun setSubtitleBorderSize(view: MPVView, size: Int) { + view.setSubtitleBorderSize(size) + } + + @ReactProp(name = "subtitleBorderColor") + fun setSubtitleBorderColor(view: MPVView, color: String?) { + view.setSubtitleBorderColor(color ?: "#000000") + } + + @ReactProp(name = "subtitleShadowEnabled", defaultBoolean = true) + fun setSubtitleShadowEnabled(view: MPVView, enabled: Boolean) { + view.setSubtitleShadow(enabled, if (enabled) 2 else 0) + } + + @ReactProp(name = "subtitlePosition", defaultInt = 100) + fun setSubtitlePosition(view: MPVView, pos: Int) { + view.setSubtitlePosition(pos) + } + + @ReactProp(name = "subtitleDelay", defaultFloat = 0.0f) + fun setSubtitleDelay(view: MPVView, delay: Float) { + view.setSubtitleDelay(delay.toDouble()) + } + + @ReactProp(name = "subtitleAlignment") + fun setSubtitleAlignment(view: MPVView, align: String?) { + view.setSubtitleAlignment(align ?: "center") + } } diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 82450952..15b2de0b 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -570,6 +570,16 @@ const AndroidVideoPlayer: React.FC = () => { screenDimensions={playerState.screenDimensions} decoderMode={settings.decoderMode} gpuMode={settings.gpuMode} + // Subtitle Styling - pass to MPV for built-in subtitle customization + subtitleSize={subtitleSize} + subtitleColor={subtitleTextColor} + subtitleBackgroundOpacity={subtitleBackground ? subtitleBgOpacity : 0} + subtitleBorderSize={subtitleOutline ? subtitleOutlineWidth : 0} + subtitleBorderColor={subtitleOutlineColor} + subtitleShadowEnabled={subtitleTextShadow} + subtitlePosition={100 - Math.floor(subtitleBottomOffset / 2)} // Convert bottomOffset to MPV position + subtitleDelay={subtitleOffsetSec} + subtitleAlignment={subtitleAlign} /> )} diff --git a/src/components/player/android/MpvPlayer.tsx b/src/components/player/android/MpvPlayer.tsx index 51aa87f9..b84628e8 100644 --- a/src/components/player/android/MpvPlayer.tsx +++ b/src/components/player/android/MpvPlayer.tsx @@ -27,6 +27,16 @@ export interface MpvPlayerProps { onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void; decoderMode?: 'auto' | 'sw' | 'hw' | 'hw+'; gpuMode?: 'gpu' | 'gpu-next'; + // Subtitle Styling + subtitleSize?: number; + subtitleColor?: string; + subtitleBackgroundOpacity?: number; + subtitleBorderSize?: number; + subtitleBorderColor?: string; + subtitleShadowEnabled?: boolean; + subtitlePosition?: number; + subtitleDelay?: number; + subtitleAlignment?: 'left' | 'center' | 'right'; } const MpvPlayer = forwardRef((props, ref) => { @@ -107,6 +117,16 @@ const MpvPlayer = forwardRef((props, ref) => { onTracksChanged={handleTracksChanged} decoderMode={props.decoderMode ?? 'auto'} gpuMode={props.gpuMode ?? 'gpu'} + // Subtitle Styling + subtitleSize={props.subtitleSize ?? 48} + subtitleColor={props.subtitleColor ?? '#FFFFFF'} + subtitleBackgroundOpacity={props.subtitleBackgroundOpacity ?? 0} + subtitleBorderSize={props.subtitleBorderSize ?? 3} + subtitleBorderColor={props.subtitleBorderColor ?? '#000000'} + subtitleShadowEnabled={props.subtitleShadowEnabled ?? true} + subtitlePosition={props.subtitlePosition ?? 100} + subtitleDelay={props.subtitleDelay ?? 0} + subtitleAlignment={props.subtitleAlignment ?? 'center'} /> ); }); diff --git a/src/components/player/android/components/VideoSurface.tsx b/src/components/player/android/components/VideoSurface.tsx index 16123f53..3692a2d1 100644 --- a/src/components/player/android/components/VideoSurface.tsx +++ b/src/components/player/android/components/VideoSurface.tsx @@ -34,6 +34,16 @@ interface VideoSurfaceProps { onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void; decoderMode?: 'auto' | 'sw' | 'hw' | 'hw+'; gpuMode?: 'gpu' | 'gpu-next'; + // Subtitle Styling + subtitleSize?: number; + subtitleColor?: string; + subtitleBackgroundOpacity?: number; + subtitleBorderSize?: number; + subtitleBorderColor?: string; + subtitleShadowEnabled?: boolean; + subtitlePosition?: number; + subtitleDelay?: number; + subtitleAlignment?: 'left' | 'center' | 'right'; } export const VideoSurface: React.FC = ({ @@ -59,6 +69,16 @@ export const VideoSurface: React.FC = ({ onTracksChanged, decoderMode, gpuMode, + // Subtitle Styling + subtitleSize, + subtitleColor, + subtitleBackgroundOpacity, + subtitleBorderSize, + subtitleBorderColor, + subtitleShadowEnabled, + subtitlePosition, + subtitleDelay, + subtitleAlignment, }) => { // Use the actual stream URL const streamUrl = currentStreamUrl || processedStreamUrl; @@ -119,6 +139,16 @@ export const VideoSurface: React.FC = ({ onTracksChanged={onTracksChanged} decoderMode={decoderMode} gpuMode={gpuMode} + // Subtitle Styling + subtitleSize={subtitleSize} + subtitleColor={subtitleColor} + subtitleBackgroundOpacity={subtitleBackgroundOpacity} + subtitleBorderSize={subtitleBorderSize} + subtitleBorderColor={subtitleBorderColor} + subtitleShadowEnabled={subtitleShadowEnabled} + subtitlePosition={subtitlePosition} + subtitleDelay={subtitleDelay} + subtitleAlignment={subtitleAlignment} /> {/* Gesture overlay - transparent, on top of the player */} diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index b1491d02..a1d1a70f 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -21,7 +21,7 @@ interface SubtitleModalsProps { isLoadingSubtitles: boolean; customSubtitles: SubtitleCue[]; availableSubtitles: WyzieSubtitle[]; - ksTextTracks: Array<{id: number, name: string, language?: string}>; + ksTextTracks: Array<{ id: number, name: string, language?: string }>; selectedTextTrack: number; useCustomSubtitles: boolean; isKsPlayerActive?: boolean; @@ -180,7 +180,7 @@ export const SubtitleModals: React.FC = ({ { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }} - style={{ padding: 5,paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', textAlignVertical: 'center' }} + style={{ padding: 5, paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', textAlignVertical: 'center' }} > {sub.display} @@ -328,7 +328,7 @@ export const SubtitleModals: React.FC = ({ Align - {([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => ( + {([{ key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' }] as const).map(a => ( setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}>