From 39c49d4cdbacbed12546cb66515898fb91705908 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 18 Oct 2024 22:27:26 +0200 Subject: [PATCH] wip --- app/(auth)/player.tsx | 66 ++-- app/(auth)/vlc-player.tsx | 4 +- components/PlayButton.tsx | 6 +- components/music/SongsListItem.tsx | 2 +- components/settings/SettingToggles.tsx | 36 +- components/video-player/Controls.tsx | 336 ++++++++++++++++--- components/video-player/VlcControls.tsx | 10 +- components/vlc/VideoDebugInfo.tsx | 11 +- modules/vlc-player/ios/VlcPlayerModule.swift | 6 +- modules/vlc-player/ios/VlcPlayerView.swift | 27 +- modules/vlc-player/src/VlcPlayer.types.ts | 2 +- modules/vlc-player/src/VlcPlayerView.tsx | 4 +- utils/atoms/settings.ts | 2 - utils/jellyfin/media/getStreamUrl.ts | 10 +- utils/time.ts | 18 +- 15 files changed, 381 insertions(+), 159 deletions(-) diff --git a/app/(auth)/player.tsx b/app/(auth)/player.tsx index 0ae91048..d0b18a7a 100644 --- a/app/(auth)/player.tsx +++ b/app/(auth)/player.tsx @@ -3,6 +3,7 @@ import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; import { useOrientation } from "@/hooks/useOrientation"; import { useOrientationSettings } from "@/hooks/useOrientationSettings"; import { useWebSocket } from "@/hooks/useWebsockets"; +import { TrackInfo } from "@/modules/vlc-player"; import { apiAtom } from "@/providers/JellyfinProvider"; import { PlaybackType, @@ -12,6 +13,7 @@ import { useSettings } from "@/utils/atoms/settings"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { secondsToTicks } from "@/utils/secondsToTicks"; +import { ticksToSeconds } from "@/utils/time"; import { Api } from "@jellyfin/sdk"; import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api"; import * as Haptics from "expo-haptics"; @@ -22,6 +24,7 @@ import { Pressable, StatusBar, useWindowDimensions, View } from "react-native"; import { useSharedValue } from "react-native-reanimated"; import Video, { OnProgressData, + SelectedTrack, SelectedTrackType, VideoRef, } from "react-native-video"; @@ -112,6 +115,13 @@ export default function page() { reportPlaybackStopped(); }, [videoRef]); + const seek = useCallback( + (ticks: number) => { + videoRef.current?.seek(ticksToSeconds(ticks)); + }, + [videoRef] + ); + const reportPlaybackStopped = async () => { await getPlaystateApi(api).onPlaybackStopped({ itemId: playSettings?.item?.Id!, @@ -141,12 +151,20 @@ export default function page() { if (isSeeking.value === true) return; if (isPlaybackStopped === true) return; - const ticks = data.currentTime * 10000000; + console.log({ + data, + isSeeking: isSeeking.value, + isPlaybackStopped, + }); - progress.value = secondsToTicks(data.currentTime); + const ticks = secondsToTicks(data.currentTime); + + progress.value = ticks; cacheProgress.value = secondsToTicks(data.playableDuration); setIsBuffering(data.playableDuration === 0); + console.log("progress.value", progress.value); + if (!playSettings?.item?.Id || data.currentTime === 0) return; await getPlaystateApi(api).onPlaybackProgress({ @@ -188,15 +206,11 @@ export default function page() { stopPlayback: stop, }); - const selectedSubtitleTrack = useMemo(() => { - const a = playSettings?.mediaSource?.MediaStreams?.find( - (s) => s.Index === playSettings.subtitleIndex - ); - console.log(a); - return a; - }, [playSettings]); + const [selectedTextTrack, setSelectedTextTrack] = useState< + SelectedTrack | undefined + >(); - const [hlsSubTracks, setHlsSubTracks] = useState< + const [embededTextTracks, setEmbededTextTracks] = useState< { index: number; language?: string | undefined; @@ -206,17 +220,12 @@ export default function page() { }[] >([]); - const selectedTextTrack = useMemo(() => { - for (let st of hlsSubTracks) { - if (st.title === selectedSubtitleTrack?.DisplayTitle) { - return { - type: SelectedTrackType.TITLE, - value: selectedSubtitleTrack?.DisplayTitle ?? "", - }; - } - } - return undefined; - }, [hlsSubTracks]); + const getSubtitleTracks = (): TrackInfo[] => { + return embededTextTracks.map((t) => ({ + name: t.title ?? "", + index: t.index, + })); + }; return ( { console.log("onTextTracks ~", data); - setHlsSubTracks(data.textTracks as any); + setEmbededTextTracks(data.textTracks as any); }} selectedTextTrack={selectedTextTrack} /> @@ -293,6 +302,16 @@ export default function page() { setShowControls={setShowControls} setIgnoreSafeAreas={setIgnoreSafeAreas} ignoreSafeAreas={ignoreSafeAreas} + seek={seek} + play={play} + pause={pause} + getSubtitleTracks={getSubtitleTracks} + setSubtitleTrack={(i) => + setSelectedTextTrack({ + type: SelectedTrackType.INDEX, + value: i, + }) + } /> ); @@ -334,6 +353,9 @@ export function useVideoSource( return { uri: playUrl, + textTracks: { + + }, isNetwork: true, startPosition, headers: getAuthHeaders(api), diff --git a/app/(auth)/vlc-player.tsx b/app/(auth)/vlc-player.tsx index bdaf4536..6c49f5e9 100644 --- a/app/(auth)/vlc-player.tsx +++ b/app/(auth)/vlc-player.tsx @@ -1,3 +1,4 @@ +import { Controls } from "@/components/video-player/Controls"; import { VlcControls } from "@/components/video-player/VlcControls"; import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; import { useOrientation } from "@/hooks/useOrientation"; @@ -264,7 +265,7 @@ export default function page() { {videoRef.current && ( - )} diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index a1d57698..feff2abe 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -84,7 +84,7 @@ export const PlayButton: React.FC = ({ ...props }) => { } if (Platform.OS === "ios") router.push("/vlc-player"); - else router.push("/play-video"); + else router.push("/player"); return; } @@ -125,7 +125,6 @@ export const PlayButton: React.FC = ({ ...props }) => { audioStreamIndex: playSettings?.audioIndex ?? 0, subtitleStreamIndex: playSettings?.subtitleIndex ?? -1, userId: user?.Id, - forceDirectPlay: settings?.forceDirectPlay, }); if (!data?.url) { @@ -206,7 +205,8 @@ export const PlayButton: React.FC = ({ ...props }) => { }); break; case 1: - router.push("/vlc-player"); + if (Platform.OS === "ios") router.push("/vlc-player"); + else router.push("/player"); break; case cancelButtonIndex: break; diff --git a/components/music/SongsListItem.tsx b/components/music/SongsListItem.tsx index 367b763b..552baa69 100644 --- a/components/music/SongsListItem.tsx +++ b/components/music/SongsListItem.tsx @@ -103,7 +103,7 @@ export const SongsListItem: React.FC = ({ }); } else { console.log("Playing on device", data.url, item.Id); - router.push("/play-music"); + router.push("/music-player"); } }, []); diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index fc9cbf00..6d218051 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -10,6 +10,7 @@ import { registerBackgroundFetchAsync, unregisterBackgroundFetchAsync, } from "@/utils/background-tasks"; +import { getStatistics } from "@/utils/optimize-server"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import * as BackgroundFetch from "expo-background-fetch"; @@ -18,7 +19,6 @@ import * as TaskManager from "expo-task-manager"; import { useAtom } from "jotai"; import { useEffect, useState } from "react"; import { - ActivityIndicator, Linking, Switch, TouchableOpacity, @@ -32,8 +32,6 @@ import { Input } from "../common/Input"; import { Text } from "../common/Text"; import { Loader } from "../Loader"; import { MediaToggles } from "./MediaToggles"; -import axios from "axios"; -import { getStatistics } from "@/utils/optimize-server"; interface Props extends ViewProps {} @@ -248,22 +246,6 @@ export const SettingToggles: React.FC = ({ ...props }) => { - - - Use external player (VLC) - - Open all videos in VLC instead of the default player. This - requries VLC to be installed on the phone. - - - { - updateSettings({ openInVLC: value, forceDirectPlay: value }); - }} - /> - - @@ -334,22 +316,6 @@ export const SettingToggles: React.FC = ({ ...props }) => { )} - - - Force direct play - - This will always request direct play. This is good if you want - to try to stream movies you think the device supports. - - - - updateSettings({ forceDirectPlay: value }) - } - /> - - ; + videoRef: React.MutableRefObject; isPlaying: boolean; isSeeking: SharedValue; cacheProgress: SharedValue; @@ -47,11 +60,27 @@ interface Props { enableTrickplay?: boolean; togglePlay: (ticks: number) => void; setShowControls: (shown: boolean) => void; + offline?: boolean; + isVideoLoaded?: boolean; + mediaSource?: MediaSourceInfo | null; + seek: (ticks: number) => void; + play: (() => Promise) | (() => void); + pause: () => void; + getAudioTracks?: () => Promise; + getSubtitleTracks?: (() => Promise) | (() => TrackInfo[]); + setSubtitleURL?: (url: string, customName: string) => void; + setSubtitleTrack?: (index: number) => void; + setAudioTrack?: (index: number) => void; + stop?: (() => Promise) | (() => void); + isVlc?: boolean; } export const Controls: React.FC = ({ item, videoRef, + seek, + play, + pause, togglePlay, isPlaying, isSeeking, @@ -62,19 +91,29 @@ export const Controls: React.FC = ({ setShowControls, ignoreSafeAreas, setIgnoreSafeAreas, + mediaSource, + isVideoLoaded, + getAudioTracks, + getSubtitleTracks, + setSubtitleURL, + setSubtitleTrack, + setAudioTrack, + stop, + offline = false, enableTrickplay = true, + isVlc = false, }) => { const [settings] = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); - const { setPlaySettings } = usePlaySettings(); - + const { setPlaySettings, playSettings } = usePlaySettings(); + const api = useAtomValue(apiAtom); const windowDimensions = Dimensions.get("window"); const { previousItem, nextItem } = useAdjacentItems({ item }); const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay( item, - enableTrickplay + !offline && enableTrickplay ); const [currentTime, setCurrentTime] = useState(0); // Seconds @@ -84,23 +123,20 @@ export const Controls: React.FC = ({ const max = useSharedValue(item.RunTimeTicks || 0); const wasPlayingRef = useRef(false); - - const seek = (ticks: number) => { - videoRef.current?.seek(ticks); - }; + const lastProgressRef = useRef(0); const { showSkipButton, skipIntro } = useIntroSkipper( - item.Id, + offline ? undefined : item.Id, currentTime, seek, - () => videoRef.current?.resume() + play ); const { showSkipCreditButton, skipCredit } = useCreditSkipper( - item.Id, + offline ? undefined : item.Id, currentTime, seek, - () => videoRef.current?.resume() + play ); const goToPreviousItem = useCallback(() => { @@ -117,7 +153,8 @@ export const Controls: React.FC = ({ subtitleIndex, }); - router.replace("/play-video"); + if (Platform.OS === "ios") router.replace("/vlc-player"); + else router.replace("/player"); }, [previousItem, settings]); const goToNextItem = useCallback(() => { @@ -134,13 +171,16 @@ export const Controls: React.FC = ({ subtitleIndex, }); - router.replace("/play-video"); + if (Platform.OS === "ios") router.replace("/vlc-player"); + else router.replace("/player"); }, [nextItem, settings]); const updateTimes = useCallback( (currentProgress: number, maxValue: number) => { - const current = ticksToSeconds(currentProgress); - const remaining = ticksToSeconds(maxValue - currentProgress); + const current = isVlc ? currentProgress : ticksToSeconds(currentProgress); + const remaining = isVlc + ? maxValue - currentProgress + : ticksToSeconds(maxValue - currentProgress); setCurrentTime(current); setRemainingTime(remaining); @@ -151,7 +191,7 @@ export const Controls: React.FC = ({ goToNextItem(); } }, - [goToNextItem] + [goToNextItem, isVlc] ); useAnimatedReaction( @@ -170,19 +210,27 @@ export const Controls: React.FC = ({ useEffect(() => { if (item) { - progress.value = item?.UserData?.PlaybackPositionTicks || 0; - max.value = item.RunTimeTicks || 0; + progress.value = isVlc + ? ticksToMs(item?.UserData?.PlaybackPositionTicks) + : item?.UserData?.PlaybackPositionTicks || 0; + max.value = isVlc + ? ticksToMs(item.RunTimeTicks || 0) + : item.RunTimeTicks || 0; } - }, [item]); + }, [item, isVlc]); const toggleControls = () => setShowControls(!showControls); - const handleSliderComplete = useCallback((value: number) => { - progress.value = value; - isSeeking.value = false; - videoRef.current?.seek(Math.max(0, Math.floor(value / 10000000))); - if (wasPlayingRef.current === true) videoRef.current?.resume(); - }, []); + const handleSliderComplete = useCallback( + async (value: number) => { + isSeeking.value = false; + progress.value = value; + + await seek(Math.max(0, Math.floor(isVlc ? value : value / 10000000))); + if (wasPlayingRef.current === true) play(); + }, + [isVlc] + ); const handleSliderChange = (value: number) => { calculateTrickplayUrl(value); @@ -190,49 +238,124 @@ export const Controls: React.FC = ({ const handleSliderStart = useCallback(() => { if (showControls === false) return; + wasPlayingRef.current = isPlaying; - videoRef.current?.pause(); + lastProgressRef.current = progress.value; + + pause(); isSeeking.value = true; }, [showControls, isPlaying]); const handleSkipBackward = useCallback(async () => { - console.log("handleSkipBackward"); if (!settings?.rewindSkipTime) return; wasPlayingRef.current = isPlaying; try { - const curr = await videoRef.current?.getCurrentPosition(); + const curr = progress.value; if (curr !== undefined) { - videoRef.current?.seek(Math.max(0, curr - settings.rewindSkipTime)); - setTimeout(() => { - if (wasPlayingRef.current === true) videoRef.current?.resume(); - }, 10); + const newTime = isVlc + ? Math.max(0, curr - secondsToMs(settings.rewindSkipTime)) + : Math.max(0, curr - settings.rewindSkipTime * 10000000); + await seek(newTime); + if (wasPlayingRef.current === true) play(); } } catch (error) { writeToLog("ERROR", "Error seeking video backwards", error); } - }, [settings, isPlaying]); + }, [settings, isPlaying, isVlc]); const handleSkipForward = useCallback(async () => { - console.log("handleSkipForward"); if (!settings?.forwardSkipTime) return; wasPlayingRef.current = isPlaying; try { - const curr = await videoRef.current?.getCurrentPosition(); + const curr = progress.value; if (curr !== undefined) { - videoRef.current?.seek(Math.max(0, curr + settings.forwardSkipTime)); - setTimeout(() => { - if (wasPlayingRef.current === true) videoRef.current?.resume(); - }, 10); + const newTime = isVlc + ? curr + secondsToMs(settings.forwardSkipTime) + : curr + settings.forwardSkipTime * 10000000; + await seek(Math.max(0, newTime)); + if (wasPlayingRef.current === true) play(); } } catch (error) { writeToLog("ERROR", "Error seeking video forwards", error); } - }, [settings, isPlaying]); + }, [settings, isPlaying, isVlc]); const toggleIgnoreSafeAreas = useCallback(() => { setIgnoreSafeAreas((prev) => !prev); }, []); + const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState< + MediaStream | undefined + >(undefined); + + const [audioTracks, setAudioTracks] = useState(null); + const [subtitleTracks, setSubtitleTracks] = useState( + null + ); + + useEffect(() => { + const fetchTracks = async () => { + if (getAudioTracks && getSubtitleTracks) { + const audio = await getAudioTracks(); + const subtitles = await getSubtitleTracks(); + setAudioTracks(audio); + setSubtitleTracks(subtitles); + } + }; + + fetchTracks(); + }, [isVideoLoaded, getAudioTracks, getSubtitleTracks]); + + type EmbeddedSubtitle = { + name: string; + index: number; + isExternal: false; + }; + + type ExternalSubtitle = { + name: string; + index: number; + isExternal: true; + deliveryUrl: string; + }; + + const allSubtitleTracks = useMemo(() => { + const embeddedSubs = + subtitleTracks?.map((s) => ({ + name: s.name, + index: s.index, + isExternal: false, + deliveryUrl: undefined, + })) || []; + + const externalSubs = + mediaSource?.MediaStreams?.filter( + (stream) => + stream.Type === "Subtitle" && + stream.IsExternal && + !!stream.DeliveryUrl + ).map((s) => ({ + name: s.DisplayTitle!, + index: s.Index!, + isExternal: true, + deliveryUrl: s.DeliveryUrl, + })) || []; + + // Create a Set of embedded subtitle names for quick lookup + const embeddedSubNames = new Set(embeddedSubs.map((sub) => sub.name)); + + // Filter out external subs that have the same name as embedded subs + const uniqueExternalSubs = externalSubs.filter( + (sub) => !embeddedSubNames.has(sub.name) + ); + + // Combine embedded and unique external subs + return [...embeddedSubs, ...uniqueExternalSubs] as ( + | EmbeddedSubtitle + | ExternalSubtitle + )[]; + }, [item, isVideoLoaded, subtitleTracks, mediaSource]); + return ( = ({ }, ]} > + {/* */} + + {setSubtitleURL && setSubtitleTrack && setAudioTrack && ( + + + + + + + + + + + Subtitle + + + {allSubtitleTracks.length > 0 + ? allSubtitleTracks?.map((sub, idx: number) => ( + { + console.log("Trying to set subtitle..."); + if (sub.isExternal) { + console.log("Setting external sub:", sub); + setSubtitleURL( + api?.basePath + sub.deliveryUrl, + sub.name + ); + return; + } + + console.log("Setting sub with index:", sub.index); + setSubtitleTrack(sub.index); + }} + > + + + {sub.name} + + + )) + : null} + + + + + Audio + + + {audioTracks?.length + ? audioTracks?.map((a, idx: number) => ( + { + setAudioTrack(a.index); + }} + > + + + {a.name} + + + )) + : null} + + + + + + )} + = ({ }, ]} 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 `} > = ({ /> { + onPress={async () => { + if (stop) await stop(); router.back(); }} className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" @@ -489,10 +723,10 @@ export const Controls: React.FC = ({ /> - {formatTimeString(currentTime)} + {formatTimeString(currentTime, isVlc ? "ms" : "s")} - -{formatTimeString(remainingTime)} + -{formatTimeString(remainingTime, isVlc ? "ms" : "s")} diff --git a/components/video-player/VlcControls.tsx b/components/video-player/VlcControls.tsx index 0dd45aac..875a2e1e 100644 --- a/components/video-player/VlcControls.tsx +++ b/components/video-player/VlcControls.tsx @@ -151,7 +151,8 @@ export const VlcControls: React.FC = ({ subtitleIndex, }); - router.replace("/play-video"); + if (Platform.OS === "ios") router.replace("/vlc-player"); + else router.replace("/player"); }, [previousItem, settings]); const goToNextItem = useCallback(() => { @@ -168,7 +169,8 @@ export const VlcControls: React.FC = ({ subtitleIndex, }); - router.replace("/play-video"); + if (Platform.OS === "ios") router.replace("/vlc-player"); + else router.replace("/player"); }, [nextItem, settings]); const updateTimes = useCallback( @@ -266,10 +268,6 @@ export const VlcControls: React.FC = ({ setIgnoreSafeAreas((prev) => !prev); }, []); - const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState< - MediaStream | undefined - >(undefined); - const [audioTracks, setAudioTracks] = useState(null); const [subtitleTracks, setSubtitleTracks] = useState( null diff --git a/components/vlc/VideoDebugInfo.tsx b/components/vlc/VideoDebugInfo.tsx index fee68c51..5ae04517 100644 --- a/components/vlc/VideoDebugInfo.tsx +++ b/components/vlc/VideoDebugInfo.tsx @@ -1,14 +1,11 @@ import { - PlaybackStatePayload, - ProgressUpdatePayload, - VlcPlayerViewRef, TrackInfo, + VlcPlayerViewRef, } from "@/modules/vlc-player/src/VlcPlayer.types"; -import { useState, useEffect } from "react"; -import { View, TouchableOpacity, ViewProps } from "react-native"; -import { Text } from "../common/Text"; -import React from "react"; +import React, { useEffect, useState } from "react"; +import { TouchableOpacity, View, ViewProps } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Text } from "../common/Text"; interface Props extends ViewProps { playerRef: React.RefObject; diff --git a/modules/vlc-player/ios/VlcPlayerModule.swift b/modules/vlc-player/ios/VlcPlayerModule.swift index 0d65f6ba..c5f4484c 100644 --- a/modules/vlc-player/ios/VlcPlayerModule.swift +++ b/modules/vlc-player/ios/VlcPlayerModule.swift @@ -77,10 +77,10 @@ public class VlcPlayerModule: Module { return view.getVideoCropGeometry() } - AsyncFunction("setSubtitleURL") { (view: VlcPlayerView, url: String) in - view.setSubtitleURL(url) + AsyncFunction("setSubtitleURL") { + (view: VlcPlayerView, url: String, name: String) in + view.setSubtitleURL(url, name: name) } - } } } diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift index 6ab82021..26a6a935 100644 --- a/modules/vlc-player/ios/VlcPlayerView.swift +++ b/modules/vlc-player/ios/VlcPlayerView.swift @@ -11,6 +11,7 @@ class VlcPlayerView: ExpoView { private var lastReportedState: VLCMediaPlayerState? private var lastReportedIsPlaying: Bool? private var isMediaReady: Bool = false + private var customSubtitles: [(internalName: String, originalName: String)] = [] // MARK: - Initialization @@ -198,14 +199,6 @@ 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 @@ -296,7 +289,7 @@ class VlcPlayerView: ExpoView { } } - @objc func setSubtitleURL(_ subtitleURL: String) { + @objc func setSubtitleURL(_ subtitleURL: String, name: String) { DispatchQueue.main.async { [weak self] in guard let self = self, let url = URL(string: subtitleURL) else { print("Error: Invalid subtitle URL") @@ -305,7 +298,9 @@ class VlcPlayerView: ExpoView { let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true) if let result = result { - print("Subtitle added with result: \(result)") + let internalName = "Track \(self.customSubtitles.count + 1)" + print("Subtitle added with result: \(result) \(internalName)") + self.customSubtitles.append((internalName: internalName, originalName: name)) } else { print("Failed to add subtitle") } @@ -318,10 +313,7 @@ class VlcPlayerView: ExpoView { } let count = mediaPlayer.numberOfSubtitlesTracks - - print( - "Debug: Number of subtitle tracks: \(count)" - ) + print("Debug: Number of subtitle tracks: \(count)") guard count > 0 else { return nil @@ -333,10 +325,15 @@ class VlcPlayerView: ExpoView { let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber] { for (index, name) in zip(indexes, names) { - tracks.append(["name": name, "index": index.intValue]) + if let customSubtitle = customSubtitles.first(where: { $0.internalName == name }) { + tracks.append(["name": customSubtitle.originalName, "index": index.intValue]) + } else { + tracks.append(["name": name, "index": index.intValue]) + } } } + print("Debug: Subtitle tracks: \(tracks)") return tracks } diff --git a/modules/vlc-player/src/VlcPlayer.types.ts b/modules/vlc-player/src/VlcPlayer.types.ts index 3bd61fbe..1e9c8754 100644 --- a/modules/vlc-player/src/VlcPlayer.types.ts +++ b/modules/vlc-player/src/VlcPlayer.types.ts @@ -82,5 +82,5 @@ export interface VlcPlayerViewRef { getChapters: () => Promise; setVideoCropGeometry: (geometry: string | null) => Promise; getVideoCropGeometry: () => Promise; - setSubtitleURL: (url: string) => Promise; + setSubtitleURL: (url: string, name: string) => Promise; } diff --git a/modules/vlc-player/src/VlcPlayerView.tsx b/modules/vlc-player/src/VlcPlayerView.tsx index fb08496a..df44090e 100644 --- a/modules/vlc-player/src/VlcPlayerView.tsx +++ b/modules/vlc-player/src/VlcPlayerView.tsx @@ -78,8 +78,8 @@ const VlcPlayerView = React.forwardRef( const geometry = await nativeRef.current?.getVideoCropGeometry(); return geometry ?? null; }, - setSubtitleURL: async (url: string) => { - await nativeRef.current?.setSubtitleURL(url); + setSubtitleURL: async (url: string, name: string) => { + await nativeRef.current?.setSubtitleURL(url, name); }, })); diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 72583b12..584f9ed8 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -59,7 +59,6 @@ export type Settings = { forceLandscapeInVideoPlayer?: boolean; usePopularPlugin?: boolean; deviceProfile?: "Expo" | "Native" | "Old"; - forceDirectPlay?: boolean; mediaListCollectionIds?: string[]; searchEngine: "Marlin" | "Jellyfin"; marlinServerUrl?: string; @@ -90,7 +89,6 @@ const loadSettings = async (): Promise => { forceLandscapeInVideoPlayer: false, usePopularPlugin: false, deviceProfile: "Expo", - forceDirectPlay: false, mediaListCollectionIds: [], searchEngine: "Jellyfin", marlinServerUrl: "", diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 38af5c7f..3b739e18 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -19,7 +19,6 @@ export const getStreamUrl = async ({ deviceProfile = native, audioStreamIndex = 0, subtitleStreamIndex = undefined, - forceDirectPlay = false, mediaSourceId, }: { api: Api | null | undefined; @@ -31,7 +30,6 @@ export const getStreamUrl = async ({ deviceProfile: any; audioStreamIndex?: number; subtitleStreamIndex?: number; - forceDirectPlay?: boolean; height?: number; mediaSourceId?: string | null; }): Promise<{ @@ -114,17 +112,17 @@ export const getStreamUrl = async ({ ); if (item.MediaType === "Video") { - if (mediaSource?.SupportsDirectPlay || forceDirectPlay === true) { + if (mediaSource?.TranscodingUrl) { return { - url: `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData?.PlaySessionId}&mediaSourceId=${mediaSource?.Id}&static=true&subtitleStreamIndex=${subtitleStreamIndex}&audioStreamIndex=${audioStreamIndex}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`, + url: `${api.basePath}${mediaSource.TranscodingUrl}`, sessionId: sessionId, mediaSource, }; } - if (mediaSource?.TranscodingUrl) { + if (mediaSource?.SupportsDirectPlay) { return { - url: `${api.basePath}${mediaSource.TranscodingUrl}`, + url: `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData?.PlaySessionId}&mediaSourceId=${mediaSource?.Id}&static=true&subtitleStreamIndex=${subtitleStreamIndex}&audioStreamIndex=${audioStreamIndex}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`, sessionId: sessionId, mediaSource, }; diff --git a/utils/time.ts b/utils/time.ts index df4cd22d..43adaea2 100644 --- a/utils/time.ts +++ b/utils/time.ts @@ -39,13 +39,23 @@ export const runtimeTicksToSeconds = ( // t: ms export const formatTimeString = ( t: number | null | undefined, - tick = false + unit: "s" | "ms" | "tick" = "ms" ): string => { if (t === null || t === undefined) return "0:00"; - let seconds = t / 1000; - if (tick) { - seconds = Math.floor(t / 10000000); // Convert ticks to seconds + let seconds: number; + switch (unit) { + case "s": + seconds = Math.floor(t); + break; + case "ms": + seconds = Math.floor(t / 1000); + break; + case "tick": + seconds = Math.floor(t / 10000000); + break; + default: + seconds = Math.floor(t / 1000); // Default to ms if an invalid type is provided } if (seconds < 0) return "0:00";