mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
wip
This commit is contained in:
@@ -3,6 +3,7 @@ import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar";
|
|||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
||||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||||
|
import { TrackInfo } from "@/modules/vlc-player";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import {
|
import {
|
||||||
PlaybackType,
|
PlaybackType,
|
||||||
@@ -12,6 +13,7 @@ import { useSettings } from "@/utils/atoms/settings";
|
|||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { secondsToTicks } from "@/utils/secondsToTicks";
|
import { secondsToTicks } from "@/utils/secondsToTicks";
|
||||||
|
import { ticksToSeconds } from "@/utils/time";
|
||||||
import { Api } from "@jellyfin/sdk";
|
import { Api } from "@jellyfin/sdk";
|
||||||
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import * as Haptics from "expo-haptics";
|
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 { useSharedValue } from "react-native-reanimated";
|
||||||
import Video, {
|
import Video, {
|
||||||
OnProgressData,
|
OnProgressData,
|
||||||
|
SelectedTrack,
|
||||||
SelectedTrackType,
|
SelectedTrackType,
|
||||||
VideoRef,
|
VideoRef,
|
||||||
} from "react-native-video";
|
} from "react-native-video";
|
||||||
@@ -112,6 +115,13 @@ export default function page() {
|
|||||||
reportPlaybackStopped();
|
reportPlaybackStopped();
|
||||||
}, [videoRef]);
|
}, [videoRef]);
|
||||||
|
|
||||||
|
const seek = useCallback(
|
||||||
|
(ticks: number) => {
|
||||||
|
videoRef.current?.seek(ticksToSeconds(ticks));
|
||||||
|
},
|
||||||
|
[videoRef]
|
||||||
|
);
|
||||||
|
|
||||||
const reportPlaybackStopped = async () => {
|
const reportPlaybackStopped = async () => {
|
||||||
await getPlaystateApi(api).onPlaybackStopped({
|
await getPlaystateApi(api).onPlaybackStopped({
|
||||||
itemId: playSettings?.item?.Id!,
|
itemId: playSettings?.item?.Id!,
|
||||||
@@ -141,12 +151,20 @@ export default function page() {
|
|||||||
if (isSeeking.value === true) return;
|
if (isSeeking.value === true) return;
|
||||||
if (isPlaybackStopped === 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);
|
cacheProgress.value = secondsToTicks(data.playableDuration);
|
||||||
setIsBuffering(data.playableDuration === 0);
|
setIsBuffering(data.playableDuration === 0);
|
||||||
|
|
||||||
|
console.log("progress.value", progress.value);
|
||||||
|
|
||||||
if (!playSettings?.item?.Id || data.currentTime === 0) return;
|
if (!playSettings?.item?.Id || data.currentTime === 0) return;
|
||||||
|
|
||||||
await getPlaystateApi(api).onPlaybackProgress({
|
await getPlaystateApi(api).onPlaybackProgress({
|
||||||
@@ -188,15 +206,11 @@ export default function page() {
|
|||||||
stopPlayback: stop,
|
stopPlayback: stop,
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedSubtitleTrack = useMemo(() => {
|
const [selectedTextTrack, setSelectedTextTrack] = useState<
|
||||||
const a = playSettings?.mediaSource?.MediaStreams?.find(
|
SelectedTrack | undefined
|
||||||
(s) => s.Index === playSettings.subtitleIndex
|
>();
|
||||||
);
|
|
||||||
console.log(a);
|
|
||||||
return a;
|
|
||||||
}, [playSettings]);
|
|
||||||
|
|
||||||
const [hlsSubTracks, setHlsSubTracks] = useState<
|
const [embededTextTracks, setEmbededTextTracks] = useState<
|
||||||
{
|
{
|
||||||
index: number;
|
index: number;
|
||||||
language?: string | undefined;
|
language?: string | undefined;
|
||||||
@@ -206,17 +220,12 @@ export default function page() {
|
|||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const selectedTextTrack = useMemo(() => {
|
const getSubtitleTracks = (): TrackInfo[] => {
|
||||||
for (let st of hlsSubTracks) {
|
return embededTextTracks.map((t) => ({
|
||||||
if (st.title === selectedSubtitleTrack?.DisplayTitle) {
|
name: t.title ?? "",
|
||||||
return {
|
index: t.index,
|
||||||
type: SelectedTrackType.TITLE,
|
}));
|
||||||
value: selectedSubtitleTrack?.DisplayTitle ?? "",
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [hlsSubTracks]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -273,7 +282,7 @@ export default function page() {
|
|||||||
}}
|
}}
|
||||||
onTextTracks={(data) => {
|
onTextTracks={(data) => {
|
||||||
console.log("onTextTracks ~", data);
|
console.log("onTextTracks ~", data);
|
||||||
setHlsSubTracks(data.textTracks as any);
|
setEmbededTextTracks(data.textTracks as any);
|
||||||
}}
|
}}
|
||||||
selectedTextTrack={selectedTextTrack}
|
selectedTextTrack={selectedTextTrack}
|
||||||
/>
|
/>
|
||||||
@@ -293,6 +302,16 @@ export default function page() {
|
|||||||
setShowControls={setShowControls}
|
setShowControls={setShowControls}
|
||||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||||
ignoreSafeAreas={ignoreSafeAreas}
|
ignoreSafeAreas={ignoreSafeAreas}
|
||||||
|
seek={seek}
|
||||||
|
play={play}
|
||||||
|
pause={pause}
|
||||||
|
getSubtitleTracks={getSubtitleTracks}
|
||||||
|
setSubtitleTrack={(i) =>
|
||||||
|
setSelectedTextTrack({
|
||||||
|
type: SelectedTrackType.INDEX,
|
||||||
|
value: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -334,6 +353,9 @@ export function useVideoSource(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
uri: playUrl,
|
uri: playUrl,
|
||||||
|
textTracks: {
|
||||||
|
|
||||||
|
},
|
||||||
isNetwork: true,
|
isNetwork: true,
|
||||||
startPosition,
|
startPosition,
|
||||||
headers: getAuthHeaders(api),
|
headers: getAuthHeaders(api),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Controls } from "@/components/video-player/Controls";
|
||||||
import { VlcControls } from "@/components/video-player/VlcControls";
|
import { VlcControls } from "@/components/video-player/VlcControls";
|
||||||
import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar";
|
import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
import { useOrientation } from "@/hooks/useOrientation";
|
||||||
@@ -264,7 +265,7 @@ export default function page() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
{videoRef.current && (
|
{videoRef.current && (
|
||||||
<VlcControls
|
<Controls
|
||||||
mediaSource={mediaSource}
|
mediaSource={mediaSource}
|
||||||
item={playSettings.item}
|
item={playSettings.item}
|
||||||
videoRef={videoRef}
|
videoRef={videoRef}
|
||||||
@@ -290,6 +291,7 @@ export default function page() {
|
|||||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||||
setAudioTrack={videoRef.current.setAudioTrack}
|
setAudioTrack={videoRef.current.setAudioTrack}
|
||||||
stop={videoRef.current.stop}
|
stop={videoRef.current.stop}
|
||||||
|
isVlc
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const PlayButton: React.FC<Props> = ({ ...props }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === "ios") router.push("/vlc-player");
|
if (Platform.OS === "ios") router.push("/vlc-player");
|
||||||
else router.push("/play-video");
|
else router.push("/player");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,6 @@ export const PlayButton: React.FC<Props> = ({ ...props }) => {
|
|||||||
audioStreamIndex: playSettings?.audioIndex ?? 0,
|
audioStreamIndex: playSettings?.audioIndex ?? 0,
|
||||||
subtitleStreamIndex: playSettings?.subtitleIndex ?? -1,
|
subtitleStreamIndex: playSettings?.subtitleIndex ?? -1,
|
||||||
userId: user?.Id,
|
userId: user?.Id,
|
||||||
forceDirectPlay: settings?.forceDirectPlay,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data?.url) {
|
if (!data?.url) {
|
||||||
@@ -206,7 +205,8 @@ export const PlayButton: React.FC<Props> = ({ ...props }) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
router.push("/vlc-player");
|
if (Platform.OS === "ios") router.push("/vlc-player");
|
||||||
|
else router.push("/player");
|
||||||
break;
|
break;
|
||||||
case cancelButtonIndex:
|
case cancelButtonIndex:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export const SongsListItem: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log("Playing on device", data.url, item.Id);
|
console.log("Playing on device", data.url, item.Id);
|
||||||
router.push("/play-music");
|
router.push("/music-player");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
registerBackgroundFetchAsync,
|
registerBackgroundFetchAsync,
|
||||||
unregisterBackgroundFetchAsync,
|
unregisterBackgroundFetchAsync,
|
||||||
} from "@/utils/background-tasks";
|
} from "@/utils/background-tasks";
|
||||||
|
import { getStatistics } from "@/utils/optimize-server";
|
||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as BackgroundFetch from "expo-background-fetch";
|
import * as BackgroundFetch from "expo-background-fetch";
|
||||||
@@ -18,7 +19,6 @@ import * as TaskManager from "expo-task-manager";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
Linking,
|
Linking,
|
||||||
Switch,
|
Switch,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -32,8 +32,6 @@ import { Input } from "../common/Input";
|
|||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
import { MediaToggles } from "./MediaToggles";
|
import { MediaToggles } from "./MediaToggles";
|
||||||
import axios from "axios";
|
|
||||||
import { getStatistics } from "@/utils/optimize-server";
|
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -248,22 +246,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4">
|
|
||||||
<View className="flex flex-col shrink">
|
|
||||||
<Text className="font-semibold">Use external player (VLC)</Text>
|
|
||||||
<Text className="text-xs opacity-50 shrink">
|
|
||||||
Open all videos in VLC instead of the default player. This
|
|
||||||
requries VLC to be installed on the phone.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Switch
|
|
||||||
value={settings.openInVLC}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updateSettings({ openInVLC: value, forceDirectPlay: value });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
<View className="flex flex-row items-center justify-between bg-neutral-900 p-4">
|
||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
@@ -334,22 +316,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4">
|
|
||||||
<View className="flex flex-col shrink">
|
|
||||||
<Text className="font-semibold">Force direct play</Text>
|
|
||||||
<Text className="text-xs opacity-50 shrink">
|
|
||||||
This will always request direct play. This is good if you want
|
|
||||||
to try to stream movies you think the device supports.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Switch
|
|
||||||
value={settings.forceDirectPlay}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateSettings({ forceDirectPlay: value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex flex-col">
|
<View className="flex flex-col">
|
||||||
<View
|
<View
|
||||||
className={`
|
className={`
|
||||||
|
|||||||
@@ -2,16 +2,28 @@ import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
|
|||||||
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
|
||||||
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
import { useIntroSkipper } from "@/hooks/useIntroSkipper";
|
||||||
import { useTrickplay } from "@/hooks/useTrickplay";
|
import { useTrickplay } from "@/hooks/useTrickplay";
|
||||||
|
import {
|
||||||
|
TrackInfo,
|
||||||
|
VlcPlayerViewRef,
|
||||||
|
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||||
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { formatTimeString, ticksToSeconds } from "@/utils/time";
|
import {
|
||||||
|
formatTimeString,
|
||||||
|
secondsToMs,
|
||||||
|
ticksToMs,
|
||||||
|
ticksToSeconds,
|
||||||
|
} from "@/utils/time";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import {
|
||||||
|
BaseItemDto,
|
||||||
|
MediaSourceInfo,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -20,22 +32,23 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import Animated, {
|
import {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
SharedValue,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { VideoRef } from "react-native-video";
|
import { VideoRef } from "react-native-video";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
videoRef: React.MutableRefObject<VideoRef | null>;
|
videoRef: React.MutableRefObject<VlcPlayerViewRef | VideoRef | null>;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
isSeeking: SharedValue<boolean>;
|
isSeeking: SharedValue<boolean>;
|
||||||
cacheProgress: SharedValue<number>;
|
cacheProgress: SharedValue<number>;
|
||||||
@@ -47,11 +60,27 @@ interface Props {
|
|||||||
enableTrickplay?: boolean;
|
enableTrickplay?: boolean;
|
||||||
togglePlay: (ticks: number) => void;
|
togglePlay: (ticks: number) => void;
|
||||||
setShowControls: (shown: boolean) => void;
|
setShowControls: (shown: boolean) => void;
|
||||||
|
offline?: boolean;
|
||||||
|
isVideoLoaded?: boolean;
|
||||||
|
mediaSource?: MediaSourceInfo | null;
|
||||||
|
seek: (ticks: number) => void;
|
||||||
|
play: (() => Promise<void>) | (() => void);
|
||||||
|
pause: () => void;
|
||||||
|
getAudioTracks?: () => Promise<TrackInfo[] | null>;
|
||||||
|
getSubtitleTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]);
|
||||||
|
setSubtitleURL?: (url: string, customName: string) => void;
|
||||||
|
setSubtitleTrack?: (index: number) => void;
|
||||||
|
setAudioTrack?: (index: number) => void;
|
||||||
|
stop?: (() => Promise<void>) | (() => void);
|
||||||
|
isVlc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Controls: React.FC<Props> = ({
|
export const Controls: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
videoRef,
|
videoRef,
|
||||||
|
seek,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
isSeeking,
|
isSeeking,
|
||||||
@@ -62,19 +91,29 @@ export const Controls: React.FC<Props> = ({
|
|||||||
setShowControls,
|
setShowControls,
|
||||||
ignoreSafeAreas,
|
ignoreSafeAreas,
|
||||||
setIgnoreSafeAreas,
|
setIgnoreSafeAreas,
|
||||||
|
mediaSource,
|
||||||
|
isVideoLoaded,
|
||||||
|
getAudioTracks,
|
||||||
|
getSubtitleTracks,
|
||||||
|
setSubtitleURL,
|
||||||
|
setSubtitleTrack,
|
||||||
|
setAudioTrack,
|
||||||
|
stop,
|
||||||
|
offline = false,
|
||||||
enableTrickplay = true,
|
enableTrickplay = true,
|
||||||
|
isVlc = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { setPlaySettings } = usePlaySettings();
|
const { setPlaySettings, playSettings } = usePlaySettings();
|
||||||
|
const api = useAtomValue(apiAtom);
|
||||||
const windowDimensions = Dimensions.get("window");
|
const windowDimensions = Dimensions.get("window");
|
||||||
|
|
||||||
const { previousItem, nextItem } = useAdjacentItems({ item });
|
const { previousItem, nextItem } = useAdjacentItems({ item });
|
||||||
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
|
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
|
||||||
item,
|
item,
|
||||||
enableTrickplay
|
!offline && enableTrickplay
|
||||||
);
|
);
|
||||||
|
|
||||||
const [currentTime, setCurrentTime] = useState(0); // Seconds
|
const [currentTime, setCurrentTime] = useState(0); // Seconds
|
||||||
@@ -84,23 +123,20 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const max = useSharedValue(item.RunTimeTicks || 0);
|
const max = useSharedValue(item.RunTimeTicks || 0);
|
||||||
|
|
||||||
const wasPlayingRef = useRef(false);
|
const wasPlayingRef = useRef(false);
|
||||||
|
const lastProgressRef = useRef<number>(0);
|
||||||
const seek = (ticks: number) => {
|
|
||||||
videoRef.current?.seek(ticks);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { showSkipButton, skipIntro } = useIntroSkipper(
|
const { showSkipButton, skipIntro } = useIntroSkipper(
|
||||||
item.Id,
|
offline ? undefined : item.Id,
|
||||||
currentTime,
|
currentTime,
|
||||||
seek,
|
seek,
|
||||||
() => videoRef.current?.resume()
|
play
|
||||||
);
|
);
|
||||||
|
|
||||||
const { showSkipCreditButton, skipCredit } = useCreditSkipper(
|
const { showSkipCreditButton, skipCredit } = useCreditSkipper(
|
||||||
item.Id,
|
offline ? undefined : item.Id,
|
||||||
currentTime,
|
currentTime,
|
||||||
seek,
|
seek,
|
||||||
() => videoRef.current?.resume()
|
play
|
||||||
);
|
);
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
@@ -117,7 +153,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.replace("/play-video");
|
if (Platform.OS === "ios") router.replace("/vlc-player");
|
||||||
|
else router.replace("/player");
|
||||||
}, [previousItem, settings]);
|
}, [previousItem, settings]);
|
||||||
|
|
||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
@@ -134,13 +171,16 @@ export const Controls: React.FC<Props> = ({
|
|||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.replace("/play-video");
|
if (Platform.OS === "ios") router.replace("/vlc-player");
|
||||||
|
else router.replace("/player");
|
||||||
}, [nextItem, settings]);
|
}, [nextItem, settings]);
|
||||||
|
|
||||||
const updateTimes = useCallback(
|
const updateTimes = useCallback(
|
||||||
(currentProgress: number, maxValue: number) => {
|
(currentProgress: number, maxValue: number) => {
|
||||||
const current = ticksToSeconds(currentProgress);
|
const current = isVlc ? currentProgress : ticksToSeconds(currentProgress);
|
||||||
const remaining = ticksToSeconds(maxValue - currentProgress);
|
const remaining = isVlc
|
||||||
|
? maxValue - currentProgress
|
||||||
|
: ticksToSeconds(maxValue - currentProgress);
|
||||||
|
|
||||||
setCurrentTime(current);
|
setCurrentTime(current);
|
||||||
setRemainingTime(remaining);
|
setRemainingTime(remaining);
|
||||||
@@ -151,7 +191,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
goToNextItem();
|
goToNextItem();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[goToNextItem]
|
[goToNextItem, isVlc]
|
||||||
);
|
);
|
||||||
|
|
||||||
useAnimatedReaction(
|
useAnimatedReaction(
|
||||||
@@ -170,19 +210,27 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item) {
|
if (item) {
|
||||||
progress.value = item?.UserData?.PlaybackPositionTicks || 0;
|
progress.value = isVlc
|
||||||
max.value = item.RunTimeTicks || 0;
|
? 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 toggleControls = () => setShowControls(!showControls);
|
||||||
|
|
||||||
const handleSliderComplete = useCallback((value: number) => {
|
const handleSliderComplete = useCallback(
|
||||||
progress.value = value;
|
async (value: number) => {
|
||||||
isSeeking.value = false;
|
isSeeking.value = false;
|
||||||
videoRef.current?.seek(Math.max(0, Math.floor(value / 10000000)));
|
progress.value = value;
|
||||||
if (wasPlayingRef.current === true) videoRef.current?.resume();
|
|
||||||
}, []);
|
await seek(Math.max(0, Math.floor(isVlc ? value : value / 10000000)));
|
||||||
|
if (wasPlayingRef.current === true) play();
|
||||||
|
},
|
||||||
|
[isVlc]
|
||||||
|
);
|
||||||
|
|
||||||
const handleSliderChange = (value: number) => {
|
const handleSliderChange = (value: number) => {
|
||||||
calculateTrickplayUrl(value);
|
calculateTrickplayUrl(value);
|
||||||
@@ -190,49 +238,124 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
const handleSliderStart = useCallback(() => {
|
const handleSliderStart = useCallback(() => {
|
||||||
if (showControls === false) return;
|
if (showControls === false) return;
|
||||||
|
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
videoRef.current?.pause();
|
lastProgressRef.current = progress.value;
|
||||||
|
|
||||||
|
pause();
|
||||||
isSeeking.value = true;
|
isSeeking.value = true;
|
||||||
}, [showControls, isPlaying]);
|
}, [showControls, isPlaying]);
|
||||||
|
|
||||||
const handleSkipBackward = useCallback(async () => {
|
const handleSkipBackward = useCallback(async () => {
|
||||||
console.log("handleSkipBackward");
|
|
||||||
if (!settings?.rewindSkipTime) return;
|
if (!settings?.rewindSkipTime) return;
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
try {
|
try {
|
||||||
const curr = await videoRef.current?.getCurrentPosition();
|
const curr = progress.value;
|
||||||
if (curr !== undefined) {
|
if (curr !== undefined) {
|
||||||
videoRef.current?.seek(Math.max(0, curr - settings.rewindSkipTime));
|
const newTime = isVlc
|
||||||
setTimeout(() => {
|
? Math.max(0, curr - secondsToMs(settings.rewindSkipTime))
|
||||||
if (wasPlayingRef.current === true) videoRef.current?.resume();
|
: Math.max(0, curr - settings.rewindSkipTime * 10000000);
|
||||||
}, 10);
|
await seek(newTime);
|
||||||
|
if (wasPlayingRef.current === true) play();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeToLog("ERROR", "Error seeking video backwards", error);
|
writeToLog("ERROR", "Error seeking video backwards", error);
|
||||||
}
|
}
|
||||||
}, [settings, isPlaying]);
|
}, [settings, isPlaying, isVlc]);
|
||||||
|
|
||||||
const handleSkipForward = useCallback(async () => {
|
const handleSkipForward = useCallback(async () => {
|
||||||
console.log("handleSkipForward");
|
|
||||||
if (!settings?.forwardSkipTime) return;
|
if (!settings?.forwardSkipTime) return;
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
try {
|
try {
|
||||||
const curr = await videoRef.current?.getCurrentPosition();
|
const curr = progress.value;
|
||||||
if (curr !== undefined) {
|
if (curr !== undefined) {
|
||||||
videoRef.current?.seek(Math.max(0, curr + settings.forwardSkipTime));
|
const newTime = isVlc
|
||||||
setTimeout(() => {
|
? curr + secondsToMs(settings.forwardSkipTime)
|
||||||
if (wasPlayingRef.current === true) videoRef.current?.resume();
|
: curr + settings.forwardSkipTime * 10000000;
|
||||||
}, 10);
|
await seek(Math.max(0, newTime));
|
||||||
|
if (wasPlayingRef.current === true) play();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeToLog("ERROR", "Error seeking video forwards", error);
|
writeToLog("ERROR", "Error seeking video forwards", error);
|
||||||
}
|
}
|
||||||
}, [settings, isPlaying]);
|
}, [settings, isPlaying, isVlc]);
|
||||||
|
|
||||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState<
|
||||||
|
MediaStream | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const [audioTracks, setAudioTracks] = useState<TrackInfo[] | null>(null);
|
||||||
|
const [subtitleTracks, setSubtitleTracks] = useState<TrackInfo[] | null>(
|
||||||
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -245,6 +368,116 @@ export const Controls: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
{/* <VideoDebugInfo playerRef={videoRef} /> */}
|
||||||
|
|
||||||
|
{setSubtitleURL && setSubtitleTrack && setAudioTrack && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: insets.top,
|
||||||
|
left: insets.left,
|
||||||
|
zIndex: 1000,
|
||||||
|
opacity: showControls ? 1 : 0,
|
||||||
|
}}
|
||||||
|
className="p-4"
|
||||||
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<View className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
||||||
|
<Ionicons
|
||||||
|
name="ellipsis-horizontal"
|
||||||
|
size={24}
|
||||||
|
color={"white"}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
loop={true}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
alignOffset={0}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={8}
|
||||||
|
sideOffset={8}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger key="subtitle-trigger">
|
||||||
|
Subtitle
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent
|
||||||
|
alignOffset={-10}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={0}
|
||||||
|
loop={true}
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
{allSubtitleTracks.length > 0
|
||||||
|
? allSubtitleTracks?.map((sub, idx: number) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={`subtitle-item-${idx}`}
|
||||||
|
onSelect={() => {
|
||||||
|
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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemIndicator />
|
||||||
|
<DropdownMenu.ItemTitle
|
||||||
|
key={`subtitle-item-title-${idx}`}
|
||||||
|
>
|
||||||
|
{sub.name}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger key="audio-trigger">
|
||||||
|
Audio
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.SubContent
|
||||||
|
alignOffset={-10}
|
||||||
|
avoidCollisions={true}
|
||||||
|
collisionPadding={0}
|
||||||
|
loop={true}
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
{audioTracks?.length
|
||||||
|
? audioTracks?.map((a, idx: number) => (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
key={`audio-item-${idx}`}
|
||||||
|
value="off"
|
||||||
|
onValueChange={() => {
|
||||||
|
setAudioTrack(a.index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemIndicator />
|
||||||
|
<DropdownMenu.ItemTitle
|
||||||
|
key={`audio-item-title-${idx}`}
|
||||||
|
>
|
||||||
|
{a.name}
|
||||||
|
</DropdownMenu.ItemTitle>
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
@@ -331,7 +564,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
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 `}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={toggleIgnoreSafeAreas}
|
onPress={toggleIgnoreSafeAreas}
|
||||||
@@ -344,7 +577,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
|
if (stop) await stop();
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
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<Props> = ({
|
|||||||
/>
|
/>
|
||||||
<View className="flex flex-row items-center justify-between mt-0.5">
|
<View className="flex flex-row items-center justify-between mt-0.5">
|
||||||
<Text className="text-[12px] text-neutral-400">
|
<Text className="text-[12px] text-neutral-400">
|
||||||
{formatTimeString(currentTime)}
|
{formatTimeString(currentTime, isVlc ? "ms" : "s")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-[12px] text-neutral-400">
|
<Text className="text-[12px] text-neutral-400">
|
||||||
-{formatTimeString(remainingTime)}
|
-{formatTimeString(remainingTime, isVlc ? "ms" : "s")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -151,7 +151,8 @@ export const VlcControls: React.FC<Props> = ({
|
|||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.replace("/play-video");
|
if (Platform.OS === "ios") router.replace("/vlc-player");
|
||||||
|
else router.replace("/player");
|
||||||
}, [previousItem, settings]);
|
}, [previousItem, settings]);
|
||||||
|
|
||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
@@ -168,7 +169,8 @@ export const VlcControls: React.FC<Props> = ({
|
|||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.replace("/play-video");
|
if (Platform.OS === "ios") router.replace("/vlc-player");
|
||||||
|
else router.replace("/player");
|
||||||
}, [nextItem, settings]);
|
}, [nextItem, settings]);
|
||||||
|
|
||||||
const updateTimes = useCallback(
|
const updateTimes = useCallback(
|
||||||
@@ -266,10 +268,6 @@ export const VlcControls: React.FC<Props> = ({
|
|||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState<
|
|
||||||
MediaStream | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const [audioTracks, setAudioTracks] = useState<TrackInfo[] | null>(null);
|
const [audioTracks, setAudioTracks] = useState<TrackInfo[] | null>(null);
|
||||||
const [subtitleTracks, setSubtitleTracks] = useState<TrackInfo[] | null>(
|
const [subtitleTracks, setSubtitleTracks] = useState<TrackInfo[] | null>(
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
PlaybackStatePayload,
|
|
||||||
ProgressUpdatePayload,
|
|
||||||
VlcPlayerViewRef,
|
|
||||||
TrackInfo,
|
TrackInfo,
|
||||||
|
VlcPlayerViewRef,
|
||||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||||
import { useState, useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { View, TouchableOpacity, ViewProps } from "react-native";
|
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||||
import { Text } from "../common/Text";
|
|
||||||
import React from "react";
|
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
playerRef: React.RefObject<VlcPlayerViewRef>;
|
playerRef: React.RefObject<VlcPlayerViewRef>;
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ public class VlcPlayerModule: Module {
|
|||||||
return view.getVideoCropGeometry()
|
return view.getVideoCropGeometry()
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setSubtitleURL") { (view: VlcPlayerView, url: String) in
|
AsyncFunction("setSubtitleURL") {
|
||||||
view.setSubtitleURL(url)
|
(view: VlcPlayerView, url: String, name: String) in
|
||||||
|
view.setSubtitleURL(url, name: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
private var lastReportedState: VLCMediaPlayerState?
|
private var lastReportedState: VLCMediaPlayerState?
|
||||||
private var lastReportedIsPlaying: Bool?
|
private var lastReportedIsPlaying: Bool?
|
||||||
private var isMediaReady: Bool = false
|
private var isMediaReady: Bool = false
|
||||||
|
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||||
|
|
||||||
// MARK: - Initialization
|
// 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) {
|
@objc func setMuted(_ muted: Bool) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.mediaPlayer?.audio?.isMuted = muted
|
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
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self, let url = URL(string: subtitleURL) else {
|
guard let self = self, let url = URL(string: subtitleURL) else {
|
||||||
print("Error: Invalid subtitle URL")
|
print("Error: Invalid subtitle URL")
|
||||||
@@ -305,7 +298,9 @@ class VlcPlayerView: ExpoView {
|
|||||||
|
|
||||||
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||||
if let result = result {
|
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 {
|
} else {
|
||||||
print("Failed to add subtitle")
|
print("Failed to add subtitle")
|
||||||
}
|
}
|
||||||
@@ -318,10 +313,7 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let count = mediaPlayer.numberOfSubtitlesTracks
|
let count = mediaPlayer.numberOfSubtitlesTracks
|
||||||
|
print("Debug: Number of subtitle tracks: \(count)")
|
||||||
print(
|
|
||||||
"Debug: Number of subtitle tracks: \(count)"
|
|
||||||
)
|
|
||||||
|
|
||||||
guard count > 0 else {
|
guard count > 0 else {
|
||||||
return nil
|
return nil
|
||||||
@@ -333,10 +325,15 @@ class VlcPlayerView: ExpoView {
|
|||||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber]
|
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber]
|
||||||
{
|
{
|
||||||
for (index, name) in zip(indexes, names) {
|
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
|
return tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,5 +82,5 @@ export interface VlcPlayerViewRef {
|
|||||||
getChapters: () => Promise<ChapterInfo[] | null>;
|
getChapters: () => Promise<ChapterInfo[] | null>;
|
||||||
setVideoCropGeometry: (geometry: string | null) => Promise<void>;
|
setVideoCropGeometry: (geometry: string | null) => Promise<void>;
|
||||||
getVideoCropGeometry: () => Promise<string | null>;
|
getVideoCropGeometry: () => Promise<string | null>;
|
||||||
setSubtitleURL: (url: string) => Promise<void>;
|
setSubtitleURL: (url: string, name: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
|||||||
const geometry = await nativeRef.current?.getVideoCropGeometry();
|
const geometry = await nativeRef.current?.getVideoCropGeometry();
|
||||||
return geometry ?? null;
|
return geometry ?? null;
|
||||||
},
|
},
|
||||||
setSubtitleURL: async (url: string) => {
|
setSubtitleURL: async (url: string, name: string) => {
|
||||||
await nativeRef.current?.setSubtitleURL(url);
|
await nativeRef.current?.setSubtitleURL(url, name);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export type Settings = {
|
|||||||
forceLandscapeInVideoPlayer?: boolean;
|
forceLandscapeInVideoPlayer?: boolean;
|
||||||
usePopularPlugin?: boolean;
|
usePopularPlugin?: boolean;
|
||||||
deviceProfile?: "Expo" | "Native" | "Old";
|
deviceProfile?: "Expo" | "Native" | "Old";
|
||||||
forceDirectPlay?: boolean;
|
|
||||||
mediaListCollectionIds?: string[];
|
mediaListCollectionIds?: string[];
|
||||||
searchEngine: "Marlin" | "Jellyfin";
|
searchEngine: "Marlin" | "Jellyfin";
|
||||||
marlinServerUrl?: string;
|
marlinServerUrl?: string;
|
||||||
@@ -90,7 +89,6 @@ const loadSettings = async (): Promise<Settings> => {
|
|||||||
forceLandscapeInVideoPlayer: false,
|
forceLandscapeInVideoPlayer: false,
|
||||||
usePopularPlugin: false,
|
usePopularPlugin: false,
|
||||||
deviceProfile: "Expo",
|
deviceProfile: "Expo",
|
||||||
forceDirectPlay: false,
|
|
||||||
mediaListCollectionIds: [],
|
mediaListCollectionIds: [],
|
||||||
searchEngine: "Jellyfin",
|
searchEngine: "Jellyfin",
|
||||||
marlinServerUrl: "",
|
marlinServerUrl: "",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export const getStreamUrl = async ({
|
|||||||
deviceProfile = native,
|
deviceProfile = native,
|
||||||
audioStreamIndex = 0,
|
audioStreamIndex = 0,
|
||||||
subtitleStreamIndex = undefined,
|
subtitleStreamIndex = undefined,
|
||||||
forceDirectPlay = false,
|
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
}: {
|
}: {
|
||||||
api: Api | null | undefined;
|
api: Api | null | undefined;
|
||||||
@@ -31,7 +30,6 @@ export const getStreamUrl = async ({
|
|||||||
deviceProfile: any;
|
deviceProfile: any;
|
||||||
audioStreamIndex?: number;
|
audioStreamIndex?: number;
|
||||||
subtitleStreamIndex?: number;
|
subtitleStreamIndex?: number;
|
||||||
forceDirectPlay?: boolean;
|
|
||||||
height?: number;
|
height?: number;
|
||||||
mediaSourceId?: string | null;
|
mediaSourceId?: string | null;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
@@ -114,17 +112,17 @@ export const getStreamUrl = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (item.MediaType === "Video") {
|
if (item.MediaType === "Video") {
|
||||||
if (mediaSource?.SupportsDirectPlay || forceDirectPlay === true) {
|
if (mediaSource?.TranscodingUrl) {
|
||||||
return {
|
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,
|
sessionId: sessionId,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaSource?.TranscodingUrl) {
|
if (mediaSource?.SupportsDirectPlay) {
|
||||||
return {
|
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,
|
sessionId: sessionId,
|
||||||
mediaSource,
|
mediaSource,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,13 +39,23 @@ export const runtimeTicksToSeconds = (
|
|||||||
// t: ms
|
// t: ms
|
||||||
export const formatTimeString = (
|
export const formatTimeString = (
|
||||||
t: number | null | undefined,
|
t: number | null | undefined,
|
||||||
tick = false
|
unit: "s" | "ms" | "tick" = "ms"
|
||||||
): string => {
|
): string => {
|
||||||
if (t === null || t === undefined) return "0:00";
|
if (t === null || t === undefined) return "0:00";
|
||||||
|
|
||||||
let seconds = t / 1000;
|
let seconds: number;
|
||||||
if (tick) {
|
switch (unit) {
|
||||||
seconds = Math.floor(t / 10000000); // Convert ticks to seconds
|
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";
|
if (seconds < 0) return "0:00";
|
||||||
|
|||||||
Reference in New Issue
Block a user