diff --git a/components/video-player/Controls.tsx b/components/video-player/Controls.tsx index a2484045..7f40ae47 100644 --- a/components/video-player/Controls.tsx +++ b/components/video-player/Controls.tsx @@ -8,10 +8,19 @@ import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings" import { writeToLog } from "@/utils/log"; import { formatTimeString, secondsToMs, ticksToMs } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { + BaseItemDto, + type MediaStream, +} from "@jellyfin/sdk/lib/generated-client"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { Dimensions, Platform, @@ -34,6 +43,8 @@ import { Text } from "../common/Text"; import { Loader } from "../Loader"; import { VlcPlayerViewRef } from "@/modules/vlc-player/src/VlcPlayer.types"; import { secondsToTicks } from "@/utils/secondsToTicks"; +import { VideoDebugInfo } from "../vlc/VideoDebugInfo"; +import * as DropdownMenu from "zeego/dropdown-menu"; interface Props { item: BaseItemDto; @@ -274,6 +285,14 @@ export const Controls: React.FC = ({ setIgnoreSafeAreas((prev) => !prev); }, []); + const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState< + MediaStream | undefined + >(undefined); + + const subtitleTracks = useMemo(() => { + return item.MediaStreams?.filter((stream) => stream.Type === "Subtitle"); + }, [item]); + return ( = ({ }, ]} > + {/* */} + + + + + + + + + + Subtitle tracks + {subtitleTracks?.map((sub, idx: number) => ( + { + if (!sub.Index !== undefined && sub.Index !== null) + videoRef.current?.setSubtitleTrack(sub.Index!); + }} + > + + {sub.DisplayTitle} + + + ))} + + + + = ({ animatedTopStyles, ]} pointerEvents={showControls ? "auto" : "none"} - className={`flex flex-row items-center space-x-2 z-10 p-4`} + className={`flex flex-row items-center space-x-2 z-10 p-4 `} > ; } -export const VideoDebugInfo: React.FC = ({ - playbackState, - progress, - playerRef, - ...props -}) => { +export const VideoDebugInfo: React.FC = ({ playerRef, ...props }) => { const [audioTracks, setAudioTracks] = useState(null); const [subtitleTracks, setSubtitleTracks] = useState( null @@ -39,36 +33,30 @@ export const VideoDebugInfo: React.FC = ({ fetchTracks(); }, [playerRef]); + const insets = useSafeAreaInsets(); + return ( - + Playback State: - {playbackState && ( - <> - Type: {playbackState.type} - Current Time: {playbackState.currentTime} - Duration: {playbackState.duration} - Is Buffering: {playbackState.isBuffering ? "Yes" : "No"} - Target: {playbackState.target} - - )} - Progress: - {progress && ( - <> - Current Time: {progress.currentTime} - Duration: {progress.duration.toFixed(2)} - - )} Audio Tracks: {audioTracks && - audioTracks.map((track) => ( - + audioTracks.map((track, index) => ( + {track.name} (Index: {track.index}) ))} Subtitle Tracks: {subtitleTracks && - subtitleTracks.map((track) => ( - + subtitleTracks.map((track, index) => ( + {track.name} (Index: {track.index}) ))} diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index 7892a255..800a4107 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -125,13 +125,29 @@ class VlcPlayerView: ExpoView { media = VLCMedia(path: uri) } } + // Apply subtitle options + let subtitleOptions = self.getSubtitleOptions() + media.addOptions(subtitleOptions) + print("Debug: Applied subtitle options: \(subtitleOptions)") - media.delegate = self + // Apply any additional media options if let mediaOptions = mediaOptions { media.addOptions(mediaOptions) + print("Debug: Applied additional media options: \(mediaOptions)") + } else { + print("Debug: No additional media options provided") + } + + let subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?? -1 + print("Debug: Subtitle track index from source: \(subtitleTrackIndex)") + + if subtitleTrackIndex >= -1 { + self.setSubtitleTrack(subtitleTrackIndex) + print("Debug: Set subtitle track to index: \(subtitleTrackIndex)") + } else { + print("Debug: Subtitle track index is less than -1, not setting") } - // Set the media without parsing self.mediaPlayer?.media = media if autoplay { @@ -142,6 +158,14 @@ class VlcPlayerView: ExpoView { } } + @objc func loadExternalSubtitle(_ subtitlePath: String) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.mediaPlayer?.addPlaybackSlave( + URL(fileURLWithPath: subtitlePath), type: .subtitle, enforce: true) + } + } + @objc func setMuted(_ muted: Bool) { DispatchQueue.main.async { self.mediaPlayer?.audio?.isMuted = muted @@ -209,16 +233,25 @@ class VlcPlayerView: ExpoView { // } // } // } - @objc func setSubtitleTrack(_ trackIndex: Int) { + print("Debug: Attempting to set subtitle track to index: \(trackIndex)") DispatchQueue.main.async { if trackIndex == -1 { + print("Debug: Disabling subtitles") // Disable subtitles self.mediaPlayer?.currentVideoSubTitleIndex = -1 } else { + print("Debug: Setting subtitle track to index: \(trackIndex)") // Set the subtitle track self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex) } + + // Print the result + if let currentIndex = self.mediaPlayer?.currentVideoSubTitleIndex { + print("Debug: Current subtitle track index after setting: \(currentIndex)") + } else { + print("Debug: Unable to retrieve current subtitle track index") + } } } @@ -418,6 +451,42 @@ class VlcPlayerView: ExpoView { completion?() } + private func getSubtitleOptions() -> [String: Any] { + return [ + // Text scaling (100 is default, increase for larger text) + "sub-text-scale": "105", + + // Text color (RRGGBB format, 16777215 is white) + "freetype-color": "16777215", + + // Outline thickness (reduced from 2 to 1 for less border) + "freetype-outline-thickness": "1", + + // Outline color (RRGGBB format, 0 is black) + "freetype-outline-color": "0", + + // Text opacity (0-255, 255 is fully opaque) + "freetype-opacity": "255", + + // Shadow opacity (increased from 128 to 180 for more shadow) + "freetype-shadow-opacity": "180", + + // Shadow offset (increased from 2 to 3 for more pronounced shadow) + "freetype-shadow-offset": "3", + + // Text alignment (0: center, 1: left, 2: right) + "sub-text-alignment": "0", + + // Vertical margin (from bottom of the screen, in pixels) + "sub-margin-bottom": "50", + + // Background opacity (0-255, 0 for no background) + "freetype-background-opacity": "64", + + // Background color (RRGGBB format) + "freetype-background-color": "0", + ] + } // MARK: - Expo Events @objc var onPlaybackStateChanged: RCTDirectEventBlock?