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";