diff --git a/app/(auth)/player/_layout.tsx b/app/(auth)/player/_layout.tsx
index dd9d2879..96d08058 100644
--- a/app/(auth)/player/_layout.tsx
+++ b/app/(auth)/player/_layout.tsx
@@ -8,7 +8,16 @@ export default function Layout() {
+ (null);
@@ -141,7 +140,7 @@ export default function page() {
maxStreamingBitrate: bitrateValue,
mediaSourceId: mediaSourceId,
subtitleStreamIndex: subtitleIndex,
- deviceProfile: !bitrateValue ? native : transcoding,
+ deviceProfile: native,
});
if (!res) return null;
diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx
new file mode 100644
index 00000000..d568170d
--- /dev/null
+++ b/app/(auth)/player/transcoding-player.tsx
@@ -0,0 +1,546 @@
+import { Text } from "@/components/common/Text";
+import { Loader } from "@/components/Loader";
+import { useOrientation } from "@/hooks/useOrientation";
+import { useOrientationSettings } from "@/hooks/useOrientationSettings";
+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 { 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 * as Haptics from "expo-haptics";
+import { useFocusEffect, useLocalSearchParams } from "expo-router";
+import { useAtomValue } from "jotai";
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { Pressable, useWindowDimensions, View } from "react-native";
+import { useSharedValue } from "react-native-reanimated";
+import Video, {
+ OnProgressData,
+ SelectedTrack,
+ SelectedTrackType,
+ VideoRef,
+} from "react-native-video";
+import { Controls } from "@/components/video-player/controls/Controls";
+import transcoding from "@/utils/profiles/transcoding";
+
+const Player = () => {
+ const api = useAtomValue(apiAtom);
+ const user = useAtomValue(userAtom);
+ const [settings] = useSettings();
+ const videoRef = useRef(null);
+
+ const firstTime = useRef(true);
+ const dimensions = useWindowDimensions();
+
+ 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 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,
+ });
+
+ const {
+ data: stream,
+ isLoading: isLoadingStreamUrl,
+ isError: isErrorStreamUrl,
+ } = useQuery({
+ queryKey: [
+ "stream-url",
+ itemId,
+ audioIndex,
+ subtitleIndex,
+ bitrateValue,
+ user,
+ mediaSourceId,
+ ],
+ 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 (ticks: number) => {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ 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(ticks),
+ 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(ticks),
+ 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 stop = useCallback(() => {
+ setIsPlaybackStopped(true);
+ videoRef.current?.pause();
+ reportPlaybackStopped();
+ }, [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,
+ });
+ };
+
+ 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,
+ ]
+ );
+
+ useFocusEffect(
+ useCallback(() => {
+ play();
+
+ return () => {
+ stop();
+ };
+ }, [play, stop])
+ );
+
+ useOrientation();
+ useOrientationSettings();
+
+ useWebSocket({
+ isPlaying: isPlaying,
+ pauseVideo: pause,
+ playVideo: play,
+ stopPlayback: stop,
+ });
+
+ 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);
+
+
+ // Set intial Subtitle Track.
+ // We will only select external tracks if they are are text based. Else it should be burned in already.
+ const textSubs =
+ stream?.mediaSource.MediaStreams?.filter((sub) => sub.Type === "Subtitle" && sub.IsTextSubtitleStream) || [];
+ const chosenSubtitleTrack = textSubs.find(
+ (sub) => sub.Index === subtitleIndex
+ );
+ useEffect(() => {
+ if (chosenSubtitleTrack && selectedTextTrack === undefined) {
+ setSelectedTextTrack({
+ type: SelectedTrackType.INDEX,
+ value: textSubs.indexOf(chosenSubtitleTrack),
+ });
+ }
+ }, [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,
+ }));
+ };
+
+ if (isLoadingItem || isLoadingStreamUrl)
+ return (
+
+
+
+ );
+
+ if (isErrorItem || isErrorStreamUrl)
+ return (
+
+ Error
+
+ );
+
+ return (
+
+ {
+ setShowControls(!showControls);
+ }}
+ style={{
+ flex: 1,
+ height: "100%",
+ width: "100%",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ zIndex: 0,
+ }}
+ >
+ {videoSource ? (
+
+
+ {item && (
+
+ setSelectedTextTrack({
+ type: SelectedTrackType.INDEX,
+ value: i,
+ })
+ }
+ getAudioTracks={getAudioTracks}
+ setAudioTrack={(i) => {
+ console.log("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: {
+ artist: item?.AlbumArtist ?? undefined,
+ 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 2633afde..d41e59fc 100644
--- a/components/PlayButton.tsx
+++ b/components/PlayButton.tsx
@@ -65,9 +65,18 @@ export const PlayButton: React.FC = ({
const colorChangeProgress = useSharedValue(0);
const [settings] = useSettings();
+
+
+ // console.log(bitrateValue);
const goToPlayer = useCallback(
- (q: string) => {
- router.push(`/player/player?${q}`);
+ (q: string, bitrateValue: number | undefined) => {
+ if (!bitrateValue) {
+ // @ts-expect-error
+ router.push(`/player/direct-player?${q}`);
+ return;
+ }
+ // @ts-expect-error
+ router.push(`/player/transcoding-player?${q}`);
},
[router]
);
@@ -86,7 +95,7 @@ export const PlayButton: React.FC = ({
const queryString = queryParams.toString();
if (!client) {
- goToPlayer(queryString);
+ goToPlayer(queryString, selectedOptions.bitrate?.value);
return;
}
@@ -207,7 +216,7 @@ export const PlayButton: React.FC = ({
});
break;
case 1:
- goToPlayer(queryString);
+ goToPlayer(queryString, selectedOptions.bitrate?.value);
break;
case cancelButtonIndex:
break;
diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx
index f6e47c3d..c0388c51 100644
--- a/components/video-player/controls/Controls.tsx
+++ b/components/video-player/controls/Controls.tsx
@@ -145,8 +145,13 @@ export const Controls: React.FC = ({
bitrateValue: bitrate.toString(),
}).toString();
+ if (!bitrate.value) {
+ // @ts-expect-error
+ router.replace(`player/direct-player?${queryParams}`);
+ return;
+ }
// @ts-expect-error
- router.replace(`player/player?${queryParams}`);
+ router.replace(`player/transcoding-player?${queryParams}`);
}, [previousItem, settings]);
const goToNextItem = useCallback(() => {
@@ -163,8 +168,13 @@ export const Controls: React.FC = ({
bitrateValue: bitrate.toString(),
}).toString();
+ if (!bitrate.value) {
+ // @ts-expect-error
+ router.replace(`player/direct-player?${queryParams}`);
+ return;
+ }
// @ts-expect-error
- router.replace(`player/player?${queryParams}`);
+ router.replace(`player/transcoding-player?${queryParams}`);
}, [nextItem, settings]);
const updateTimes = useCallback(
diff --git a/components/video-player/controls/DropdownView.tsx b/components/video-player/controls/DropdownView.tsx
index 44303e91..fba7cef2 100644
--- a/components/video-player/controls/DropdownView.tsx
+++ b/components/video-player/controls/DropdownView.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo } from "react";
+import React, { useCallback, useMemo, useState } from "react";
import { View, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import * as DropdownMenu from "zeego/dropdown-menu";
@@ -12,6 +12,8 @@ import {
import { useAtomValue } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useLocalSearchParams, useRouter } from "expo-router";
+import { parse } from "@babel/core";
+import { set } from "lodash";
interface DropdownViewProps {
showControls: boolean;
@@ -61,19 +63,7 @@ const DropdownView: React.FC = ({ showControls }) => {
)[];
}, [item, isVideoLoaded, subtitleTracks, mediaSource?.MediaStreams]);
- // const audioForTranscodingStream = mediaSource?.MediaStreams?.filter(
- // (x) => x.Type === "Audio"
- // ).map((x) => ({
- // name: x.DisplayTitle!,
- // index: x.Index!,
- // }));
-
- // Only used for transcoding streams.
- const {
- subtitleIndex: subtitleIndexStr,
- audioIndex,
- bitrateValue,
- } = useLocalSearchParams<{
+ const { subtitleIndex, audioIndex, bitrateValue } = useLocalSearchParams<{
itemId: string;
audioIndex: string;
subtitleIndex: string;
@@ -84,13 +74,41 @@ const DropdownView: React.FC = ({ showControls }) => {
// Either its on a text subtitle or its on not on any subtitle therefore it should show all the embedded HLS subtitles.
const isOnTextSubtitle =
mediaSource?.MediaStreams?.find(
- (x) => x.Index === parseInt(subtitleIndexStr) && x.IsTextSubtitleStream
- ) || subtitleIndexStr === "-1";
+ (x) => x.Index === parseInt(subtitleIndex) && x.IsTextSubtitleStream
+ ) || subtitleIndex === "-1";
- // TODO: Add support for text sorting subtitles renaming.
+ const allSubs =
+ mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [];
+ const textBasedSubs = allSubs.filter((x) => x.IsTextSubtitleStream);
+
+ // This is used in the case where it is transcoding stream.
+ const chosenSubtitle = textBasedSubs.find(
+ (x) => x.Index === parseInt(subtitleIndex)
+ );
+
+ const intialSubtitleIndex =
+ !bitrateValue || !isOnTextSubtitle
+ ? parseInt(subtitleIndex)
+ : chosenSubtitle && isOnTextSubtitle
+ ? textBasedSubs.indexOf(chosenSubtitle)
+ : -1;
+
+ const [selectedSubtitleIndex, setSelectedSubtitleIndex] =
+ useState(intialSubtitleIndex);
+ const [selectedAudioIndex, setSelectedAudioIndex] = useState(
+ parseInt(audioIndex)
+ );
+
+ // TODO: Need to account for the fact when user is on text-based subtitle at start.
+ // Then the user swaps to another text based subtitle.
+ // Then changes audio track.
+ // The user will have the first text based subtitle selected still but it should be the second text based subtitle.
const allSubtitleTracksForTranscodingStream = useMemo(() => {
- const allSubs =
- mediaSource?.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [];
+ const disableSubtitle = {
+ name: "Disable",
+ index: -1,
+ IsTextSubtitleStream: true,
+ } as TranscodedSubtitle;
if (isOnTextSubtitle) {
const textSubtitles =
subtitleTracks?.map((s) => ({
@@ -109,7 +127,27 @@ const DropdownView: React.FC = ({ showControls }) => {
IsTextSubtitleStream: x.IsTextSubtitleStream,
} as TranscodedSubtitle)
);
- return [...textSubtitles, ...imageSubtitles];
+
+ const textSubtitlesMap = new Map(textSubtitles.map((s) => [s.name, s]));
+
+ const imageSubtitlesMap = new Map(imageSubtitles.map((s) => [s.name, s]));
+
+ const sortedSubtitles = allSubs
+ .map((sub) => {
+ const displayTitle = sub.DisplayTitle ?? "";
+ if (textSubtitlesMap.has(displayTitle)) {
+ return textSubtitlesMap.get(displayTitle);
+ }
+ if (imageSubtitlesMap.has(displayTitle)) {
+ return imageSubtitlesMap.get(displayTitle);
+ }
+ return null;
+ })
+ .filter(
+ (subtitle): subtitle is TranscodedSubtitle => subtitle !== null
+ );
+
+ return [disableSubtitle, ...sortedSubtitles];
}
const transcodedSubtitle: TranscodedSubtitle[] = allSubs.map((x) => ({
@@ -118,10 +156,7 @@ const DropdownView: React.FC = ({ showControls }) => {
IsTextSubtitleStream: x.IsTextSubtitleStream!,
}));
- return [
- { name: 'Disable', index: -1, IsTextSubtitleStream: true } as TranscodedSubtitle,
- ...transcodedSubtitle
- ];
+ return [disableSubtitle, ...transcodedSubtitle];
}, [item, isVideoLoaded, subtitleTracks, mediaSource?.MediaStreams]);
const ChangeTranscodingSubtitle = useCallback(
@@ -135,7 +170,29 @@ const DropdownView: React.FC = ({ showControls }) => {
}).toString();
// @ts-expect-error
- router.replace(`player/player?${queryParams}`);
+ router.replace(`player/transcoding-player?${queryParams}`);
+ },
+ [mediaSource]
+ );
+
+ // Audio tracks for transcoding streams.
+ const allAudio =
+ mediaSource?.MediaStreams?.filter((x) => x.Type === "Audio").map((x) => ({
+ name: x.DisplayTitle!,
+ index: x.Index!,
+ })) || [];
+ const ChangeTranscodingAudio = useCallback(
+ (audioIndex: number) => {
+ const queryParams = new URLSearchParams({
+ itemId: item.Id ?? "", // Ensure itemId is a string
+ audioIndex: audioIndex?.toString() ?? "",
+ subtitleIndex: subtitleIndex,
+ mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string
+ bitrateValue: bitrateValue,
+ }).toString();
+
+ // @ts-expect-error
+ router.replace(`player/transcoding-player?${queryParams}`);
},
[mediaSource]
);
@@ -177,9 +234,10 @@ const DropdownView: React.FC = ({ showControls }) => {
>
{!mediaSource?.TranscodingUrl &&
allSubtitleTracksForDirectPlay?.map((sub, idx: number) => (
- {
+ value={selectedSubtitleIndex === sub.index}
+ onValueChange={() => {
if ("deliveryUrl" in sub && sub.deliveryUrl) {
setSubtitleURL &&
setSubtitleURL(
@@ -196,37 +254,38 @@ const DropdownView: React.FC = ({ showControls }) => {
setSubtitleTrack && setSubtitleTrack(sub.index);
}
+ setSelectedSubtitleIndex(sub.index);
console.log("Subtitle: ", sub);
}}
>
-
{sub.name}
-
+
))}
{mediaSource?.TranscodingUrl &&
allSubtitleTracksForTranscodingStream?.map(
(sub, idx: number) => (
- {
- if (subtitleIndexStr === sub.index.toString()) return;
-
+ onValueChange={() => {
+ if (subtitleIndex === sub?.index.toString()) return;
+ setSelectedSubtitleIndex(sub.index);
if (sub.IsTextSubtitleStream && isOnTextSubtitle) {
setSubtitleTrack && setSubtitleTrack(sub.index);
return;
}
+
ChangeTranscodingSubtitle(sub.index);
}}
>
-
{sub.name}
-
+
)
)}
@@ -242,19 +301,37 @@ const DropdownView: React.FC = ({ showControls }) => {
loop={true}
sideOffset={10}
>
- {audioTracks?.map((track, idx: number) => (
- {
- setAudioTrack && setAudioTrack(track.index);
- }}
- >
-
-
- {track.name}
-
-
- ))}
+ {!mediaSource?.TranscodingUrl &&
+ audioTracks?.map((track, idx: number) => (
+ {
+ setSelectedAudioIndex(track.index);
+ setAudioTrack && setAudioTrack(track.index);
+ }}
+ >
+
+ {track.name}
+
+
+ ))}
+ {mediaSource?.TranscodingUrl &&
+ allAudio?.map((track, idx: number) => (
+ {
+ if (audioIndex === track.index.toString()) return;
+ setSelectedAudioIndex(track.index);
+ ChangeTranscodingAudio(track.index);
+ }}
+ >
+
+ {track.name}
+
+
+ ))}
diff --git a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt
index 5c3c8079..03245207 100644
--- a/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt
+++ b/modules/vlc-player/android/src/main/java/expo/modules/vlcplayer/VlcPlayerView.kt
@@ -30,7 +30,6 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
private val onVideoLoadEnd by EventDispatcher()
private var startPosition: Int? = 0
- private var isTranscodedStream: Boolean = false
private var isMediaReady: Boolean = false
private var externalTrack: Map? = null
@@ -60,9 +59,6 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
val uri = source["uri"] as? String
- if (uri != null && uri.contains("m3u8")) {
- isTranscodedStream = true
- }
// Handle video load start event
// onVideoLoadStart?.invoke(mapOf("target" to reactTag ?: "null"))
@@ -239,16 +235,6 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
}
}
- // Only used for HLS transcoded streams
- private fun setSubtitleTrackByName(trackName: String) {
- val track = mediaPlayer?.getSpuTracks()?.firstOrNull { it.name.startsWith(trackName) }
- val trackIndex = track?.id ?: -1
- println("Track Index setting to: $trackIndex")
- if (trackIndex != -1) {
- setSubtitleTrack(trackIndex)
- }
- }
-
private fun updateVideoProgress() {
val player = mediaPlayer ?: return
@@ -257,25 +243,16 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
val durationMs = player.media?.duration?.toInt() ?: 0
if (currentTimeMs >= 0 && currentTimeMs < durationMs) {
- // Handle when VLC starts at cloest earliest segment skip to the start time, for transcoded streams.
+ // Set subtitle URL if available
if (player.isPlaying && !isMediaReady) {
isMediaReady = true
externalTrack?.let {
val name = it["name"]
val deliveryUrl = it["DeliveryUrl"] ?: ""
- if (!name.isNullOrEmpty()) {
- if (!isTranscodedStream) {
- setSubtitleURL(deliveryUrl, name)
- }
- else {
- setSubtitleTrackByName(name)
- }
+ if (!name.isNullOrEmpty() && !deliveryUrl.isNullOrEmpty()) {
+ setSubtitleURL(deliveryUrl, name)
}
}
-
- if (isTranscodedStream && startPosition != 0) {
- seekTo((startPosition ?: 0) * 1000)
- }
}
onVideoProgress(mapOf(
"currentTime" to currentTimeMs,
diff --git a/modules/vlc-player/ios/VlcPlayerView.swift b/modules/vlc-player/ios/VlcPlayerView.swift
index 35bcab12..5b2b9281 100644
--- a/modules/vlc-player/ios/VlcPlayerView.swift
+++ b/modules/vlc-player/ios/VlcPlayerView.swift
@@ -12,7 +12,6 @@ class VlcPlayerView: ExpoView {
private var lastReportedIsPlaying: Bool?
private var customSubtitles: [(internalName: String, originalName: String)] = []
private var startPosition: Int32 = 0
- private var isTranscodedStream: Bool = false
private var isMediaReady: Bool = false
private var externalTrack: [String: String]?
@@ -112,9 +111,6 @@ class VlcPlayerView: ExpoView {
initOptions.append("--start-time=\(startPosition)")
let uri = source["uri"] as? String
- if let uri = uri, uri.contains("m3u8") {
- self.isTranscodedStream = true
- }
let autoplay = source["autoplay"] as? Bool ?? false
let isNetwork = source["isNetwork"] as? Bool ?? false
@@ -587,7 +583,7 @@ extension VlcPlayerView: VLCMediaPlayerDelegate {
// Playing and not transcoding, we can let it in no HLS issue.
// We should also mark it as playing when the media is ready.
// Fix HLS issue.
- if player.isPlaying && (!self.isTranscodedStream || self.isMediaReady) {
+ if player.isPlaying && self.isMediaReady {
stateInfo["isPlaying"] = true
stateInfo["isBuffering"] = false
stateInfo["state"] = "Playing"
@@ -648,39 +644,15 @@ extension VlcPlayerView: VLCMediaPlayerDelegate {
let durationMs = player.media?.length.intValue ?? 0
if currentTimeMs >= 0 && currentTimeMs < durationMs {
- // Handle when VLC starts at cloest earliest segment skip to the start time, for transcoded streams.
if player.isPlaying && !self.isMediaReady {
self.isMediaReady = true
+ // Set external track subtitle when starting.
if let externalTrack = self.externalTrack {
if let name = externalTrack["name"] as? String, !name.isEmpty {
let deliveryUrl = externalTrack["DeliveryUrl"] as? String ?? ""
- if !self.isTranscodedStream {
- self.setSubtitleURL(deliveryUrl, name: name)
- } else {
- self.setSubtitleTrackByName(name)
- }
+ self.setSubtitleURL(deliveryUrl, name: name)
}
}
-
- // HLS bug.
- if self.isTranscodedStream {
- if self.startPosition > 0 {
- print("Seeking to start position: \(self.startPosition)")
- self.seekTo(self.startPosition * 1000)
- } else {
- var stateInfo: [String: Any] = [
- "target": self.reactTag ?? NSNull(),
- "currentTime": player.time.intValue,
- "duration": player.media?.length.intValue ?? 0,
- "error": false,
- "isPlaying": true,
- "isBuffering": false,
- "state": "Playing",
- ]
- self.onVideoStateChange?(stateInfo)
- }
- }
-
}
self.onVideoProgress?([
"currentTime": currentTimeMs,