diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx index 1d6c7188..bee527e5 100644 --- a/app/(auth)/player/_layout.tsx +++ b/app/(auth)/player/_layout.tsx @@ -36,15 +36,6 @@ export default function Layout() { animation: "fade", }} /> - ); diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx deleted file mode 100644 index b1768b36..00000000 --- a/app/(auth)/player/transcoding-player.tsx +++ /dev/null @@ -1,546 +0,0 @@ -import { Text } from "@/components/common/Text"; -import { Loader } from "@/components/Loader"; -import { Controls } from "@/components/video-player/controls/Controls"; -import { useOrientation } from "@/hooks/useOrientation"; -import { useOrientationSettings } from "@/hooks/useOrientationSettings"; -import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; -import { useWebSocket } from "@/hooks/useWebsockets"; -import { TrackInfo } from "@/modules/vlc-player"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useSettings } from "@/utils/atoms/settings"; -import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; -import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; -import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; -import transcoding from "@/utils/profiles/transcoding"; -import { secondsToTicks } from "@/utils/secondsToTicks"; -import { Api } from "@jellyfin/sdk"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; -import { - getPlaystateApi, - getUserLibraryApi, -} from "@jellyfin/sdk/lib/utils/api"; -import { useQuery } from "@tanstack/react-query"; -import { useHaptic } from "@/hooks/useHaptic"; -import { useFocusEffect, useLocalSearchParams } from "expo-router"; -import { useAtomValue } from "jotai"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { View } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; -import Video, { - OnProgressData, - SelectedTrack, - SelectedTrackType, - VideoRef, -} from "react-native-video"; -import { SubtitleHelper } from "@/utils/SubtitleHelper"; -import { useTranslation } from "react-i18next"; - -const Player = () => { - console.log("Transcoding Player"); - - const api = useAtomValue(apiAtom); - const user = useAtomValue(userAtom); - const [settings] = useSettings(); - const videoRef = useRef(null); - const { t } = useTranslation(); - - const firstTime = useRef(true); - const revalidateProgressCache = useInvalidatePlaybackProgressCache(); - const lightHapticFeedback = useHaptic("light"); - - const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); - const [showControls, _setShowControls] = useState(true); - const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); - const [isPlaying, setIsPlaying] = useState(false); - const [isBuffering, setIsBuffering] = useState(true); - const [isVideoLoaded, setIsVideoLoaded] = useState(false); - - const setShowControls = useCallback((show: boolean) => { - _setShowControls(show); - lightHapticFeedback(); - }, []); - - const progress = useSharedValue(0); - const isSeeking = useSharedValue(false); - const cacheProgress = useSharedValue(0); - - const { - itemId, - audioIndex: audioIndexStr, - subtitleIndex: subtitleIndexStr, - mediaSourceId, - bitrateValue: bitrateValueStr, - } = useLocalSearchParams<{ - itemId: string; - audioIndex: string; - subtitleIndex: string; - mediaSourceId: string; - bitrateValue: string; - }>(); - - const audioIndex = audioIndexStr ? parseInt(audioIndexStr, 10) : undefined; - const subtitleIndex = subtitleIndexStr - ? parseInt(subtitleIndexStr, 10) - : undefined; - const bitrateValue = bitrateValueStr - ? parseInt(bitrateValueStr, 10) - : undefined; - - const { - data: item, - isLoading: isLoadingItem, - isError: isErrorItem, - } = useQuery({ - queryKey: ["item", itemId], - queryFn: async () => { - if (!api) { - throw new Error("No api"); - } - - if (!itemId) { - console.warn("No itemId"); - return null; - } - - const res = await getUserLibraryApi(api).getItem({ - itemId, - userId: user?.Id, - }); - - return res.data; - }, - staleTime: 0, - }); - - // TODO: NEED TO FIND A WAY TO FROM SWITCHING TO IMAGE BASED TO TEXT BASED SUBTITLES, THERE IS A BUG. - // MOST LIKELY LIKELY NEED A MASSIVE REFACTOR. - const { - data: stream, - isLoading: isLoadingStreamUrl, - isError: isErrorStreamUrl, - } = useQuery({ - queryKey: ["stream-url", itemId, bitrateValue, mediaSourceId, audioIndex], - - queryFn: async () => { - if (!api) { - throw new Error("No api"); - } - - if (!item) { - console.warn("No item", itemId, item); - return null; - } - - const res = await getStreamUrl({ - api, - item, - startTimeTicks: item?.UserData?.PlaybackPositionTicks!, - userId: user?.Id, - audioStreamIndex: audioIndex, - maxStreamingBitrate: bitrateValue, - mediaSourceId: mediaSourceId, - subtitleStreamIndex: subtitleIndex, - deviceProfile: transcoding, - }); - - if (!res) return null; - - const { mediaSource, sessionId, url } = res; - - if (!sessionId || !mediaSource || !url) { - console.warn("No sessionId or mediaSource or url", url); - return null; - } - - return { - mediaSource, - sessionId, - url, - }; - }, - enabled: !!item, - staleTime: 0, - }); - - const poster = usePoster(item, api); - const videoSource = useVideoSource(item, api, poster, stream?.url); - - const togglePlay = useCallback(async () => { - lightHapticFeedback(); - if (isPlaying) { - videoRef.current?.pause(); - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(progress.value), - isPaused: true, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId, - }); - } else { - videoRef.current?.resume(); - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item?.Id!, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(progress.value), - isPaused: false, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId, - }); - } - }, [ - isPlaying, - api, - item, - videoRef, - settings, - stream, - audioIndex, - subtitleIndex, - mediaSourceId, - ]); - - const play = useCallback(() => { - videoRef.current?.resume(); - reportPlaybackStart(); - }, [videoRef]); - - const pause = useCallback(() => { - videoRef.current?.pause(); - }, [videoRef]); - - const seek = useCallback( - (seconds: number) => { - videoRef.current?.seek(seconds); - }, - [videoRef] - ); - - const reportPlaybackStopped = async () => { - if (!item?.Id) return; - await getPlaystateApi(api!).onPlaybackStopped({ - itemId: item.Id, - mediaSourceId: mediaSourceId, - positionTicks: Math.floor(progress.value), - playSessionId: stream?.sessionId, - }); - revalidateProgressCache(); - }; - - const stop = useCallback(() => { - reportPlaybackStopped(); - videoRef.current?.pause(); - setIsPlaybackStopped(true); - }, [videoRef, reportPlaybackStopped]); - - const reportPlaybackStart = async () => { - if (!item?.Id) return; - await getPlaystateApi(api!).onPlaybackStart({ - itemId: item.Id, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId, - }); - }; - - const onProgress = useCallback( - async (data: OnProgressData) => { - if (isSeeking.value === true) return; - if (isPlaybackStopped === true) return; - - const ticks = secondsToTicks(data.currentTime); - - progress.value = ticks; - cacheProgress.value = secondsToTicks(data.playableDuration); - - // TODO: Use this when streaming with HLS url, but NOT when direct playing - // TODO: since playable duration is always 0 then. - setIsBuffering(data.playableDuration === 0); - - if (!item?.Id || data.currentTime === 0) { - return; - } - - await getPlaystateApi(api!).onPlaybackProgress({ - itemId: item.Id, - audioStreamIndex: audioIndex ? audioIndex : undefined, - subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined, - mediaSourceId: mediaSourceId, - positionTicks: Math.round(ticks), - isPaused: !isPlaying, - playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", - playSessionId: stream?.sessionId, - }); - }, - [ - item, - isPlaying, - api, - isPlaybackStopped, - isSeeking, - stream, - mediaSourceId, - audioIndex, - subtitleIndex, - ] - ); - - useWebSocket({ - isPlaying: isPlaying, - togglePlay: togglePlay, - stopPlayback: stop, - offline: false, - }); - - const [selectedTextTrack, setSelectedTextTrack] = useState< - SelectedTrack | undefined - >(); - - const [embededTextTracks, setEmbededTextTracks] = useState< - { - index: number; - language?: string | undefined; - selected?: boolean | undefined; - title?: string | undefined; - type: any; - }[] - >([]); - - const [audioTracks, setAudioTracks] = useState([]); - const [selectedAudioTrack, setSelectedAudioTrack] = useState< - SelectedTrack | undefined - >(undefined); - - useEffect(() => { - if (selectedTextTrack === undefined) { - const subtitleHelper = new SubtitleHelper( - stream?.mediaSource.MediaStreams ?? [] - ); - const embeddedTrackIndex = subtitleHelper.getEmbeddedTrackIndex( - subtitleIndex! - ); - - // Most likely the subtitle is burned in. - if (embeddedTrackIndex === -1) return; - - setSelectedTextTrack({ - type: SelectedTrackType.INDEX, - value: embeddedTrackIndex, - }); - } - }, [embededTextTracks]); - - const getAudioTracks = (): TrackInfo[] => { - return audioTracks.map((t) => ({ - name: t.name, - index: t.index, - })); - }; - - const getSubtitleTracks = (): TrackInfo[] => { - return embededTextTracks.map((t) => ({ - name: t.title ?? "", - index: t.index, - language: t.language, - })); - }; - - useFocusEffect( - React.useCallback(() => { - return async () => { - stop(); - }; - }, []) - ); - - if (isLoadingItem || isLoadingStreamUrl) - return ( - - - - ); - - if (isErrorItem || isErrorStreamUrl) - return ( - - {t("player.error")} - - ); - - return ( - - - {videoSource ? ( - <> - - - {item && ( - { - if (i === -1) { - setSelectedTextTrack({ - type: SelectedTrackType.DISABLED, - value: undefined, - }); - return; - } - setSelectedTextTrack({ - type: SelectedTrackType.INDEX, - value: i, - }); - }} - getAudioTracks={getAudioTracks} - setAudioTrack={(i) => { - setSelectedAudioTrack({ - type: SelectedTrackType.INDEX, - value: i, - }); - }} - /> - )} - - ); -}; - -export function usePoster( - item: BaseItemDto | null | undefined, - api: Api | null -): string | undefined { - const poster = useMemo(() => { - if (!item || !api) return undefined; - return item.Type === "Audio" - ? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200` - : getBackdropUrl({ - api, - item: item, - quality: 70, - width: 200, - }); - }, [item, api]); - - return poster ?? undefined; -} - -export function useVideoSource( - item: BaseItemDto | null | undefined, - api: Api | null, - poster: string | undefined, - url?: string | null -) { - const videoSource = useMemo(() => { - if (!item || !api || !url) { - return null; - } - - const startPosition = item?.UserData?.PlaybackPositionTicks - ? Math.round(item.UserData.PlaybackPositionTicks / 10000) - : 0; - - return { - uri: url, - isNetwork: true, - startPosition, - headers: getAuthHeaders(api), - metadata: { - title: item?.Name || "Unknown", - description: item?.Overview ?? undefined, - imageUri: poster, - subtitle: item?.Album ?? undefined, - }, - }; - }, [item, api, poster, url]); - - return videoSource; -} - -export default Player; diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index a9195aa6..4a6a2e30 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -73,11 +73,7 @@ export const PlayButton: React.FC = ({ const goToPlayer = useCallback( (q: string, bitrateValue: number | undefined) => { - if (!bitrateValue) { - router.push(`/player/direct-player?${q}`); - return; - } - router.push(`/player/transcoding-player?${q}`); + router.push(`/player/direct-player?${q}`); }, [router] ); diff --git a/components/PlayButton.tv.tsx b/components/PlayButton.tv.tsx index 65f70ad3..128c2184 100644 --- a/components/PlayButton.tv.tsx +++ b/components/PlayButton.tv.tsx @@ -58,11 +58,7 @@ export const PlayButton: React.FC = ({ const goToPlayer = useCallback( (q: string, bitrateValue: number | undefined) => { - if (!bitrateValue) { - router.push(`/player/direct-player?${q}`); - return; - } - router.push(`/player/transcoding-player?${q}`); + router.push(`/player/direct-player?${q}`); }, [router] ); diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index b52e98c7..4198259e 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -24,7 +24,7 @@ import { ticksToMs, ticksToSeconds, } from "@/utils/time"; -import {Ionicons, MaterialIcons} from "@expo/vector-icons"; +import { Ionicons, MaterialIcons } from "@expo/vector-icons"; import { BaseItemDto, MediaSourceInfo, @@ -35,7 +35,12 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import { useAtom } from "jotai"; import { debounce } from "lodash"; import React, { useCallback, useEffect, useRef, useState } from "react"; -import {Platform, TouchableOpacity, useWindowDimensions, View} from "react-native"; +import { + Platform, + TouchableOpacity, + useWindowDimensions, + View, +} from "react-native"; import { Slider } from "react-native-awesome-slider"; import { runOnJS, @@ -214,15 +219,10 @@ export const Controls: React.FC = ({ bitrateValue: bitrateValue.toString(), }).toString(); - stop() + stop(); - if (!bitrateValue) { - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - return; - } // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); + router.replace(`player/direct-player?${queryParams}`); }, [previousItem, settings, subtitleIndex, audioIndex]); const goToNextItem = useCallback(() => { @@ -254,15 +254,10 @@ export const Controls: React.FC = ({ bitrateValue: bitrateValue.toString(), }).toString(); - stop() + stop(); - if (!bitrateValue) { - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - return; - } // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); + router.replace(`player/direct-player?${queryParams}`); }, [nextItem, settings, subtitleIndex, audioIndex]); const updateTimes = useCallback( @@ -419,15 +414,10 @@ export const Controls: React.FC = ({ bitrateValue: bitrateValue.toString(), }).toString(); - stop() + stop(); - if (!bitrateValue) { - // @ts-expect-error - router.replace(`player/direct-player?${queryParams}`); - return; - } // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); + router.replace(`player/direct-player?${queryParams}`); } catch (error) { console.error("Error in gotoEpisode:", error); } @@ -508,7 +498,7 @@ export const Controls: React.FC = ({ }, [trickPlayUrl, trickplayInfo, time]); const onClose = async () => { - stop() + stop(); lightHapticFeedback(); await ScreenOrientation.lockAsync( ScreenOrientation.OrientationLock.PORTRAIT_UP @@ -569,9 +559,7 @@ export const Controls: React.FC = ({ {!Platform.isTV && ( - + = ({ showControls }) => { return [disableSubtitle, ...transcodedSubtitle]; }, [item, isVideoLoaded, subtitleTracks, mediaSource?.MediaStreams]); + // TODO: Make image based subtitles work with transcoding streams in VLC const changeToImageBasedSub = useCallback( (subtitleIndex: number) => { const queryParams = new URLSearchParams({ @@ -88,8 +89,7 @@ const DropdownView: React.FC = ({ showControls }) => { bitrateValue: bitrateValue, }).toString(); - // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); + // router.replace(`player/transcoding-player?${queryParams}`); }, [mediaSource] ); @@ -101,6 +101,7 @@ const DropdownView: React.FC = ({ showControls }) => { index: x.Index!, })) || []; + // TODO: Make audio work with transcoding streams in VLC const ChangeTranscodingAudio = useCallback( (audioIndex: number) => { const queryParams = new URLSearchParams({ @@ -111,8 +112,7 @@ const DropdownView: React.FC = ({ showControls }) => { bitrateValue: bitrateValue, }).toString(); - // @ts-expect-error - router.replace(`player/transcoding-player?${queryParams}`); + // router.replace(`player/transcoding-player?${queryParams}`); }, [mediaSource, subtitleIndex, audioIndex] );