diff --git a/app.json b/app.json index f38e93a5..11ecd837 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.2.1", + "version": "0.3.0", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -23,12 +23,7 @@ "bundleIdentifier": "com.fredrikburmester.streamyfin" }, "android": { - "jsEngine": "jsc", - "androidNavigationBar": { - "visible": true, - "barStyle": "dark-content", - "backgroundColor": "#000000" - }, + "jsEngine": "hermes", "adaptiveIcon": { "foregroundImage": "./assets/images/icon.png" }, @@ -37,7 +32,7 @@ "android.permission.FOREGROUND_SERVICE", "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" ], - "versionCode": 7 + "versionCode": 8 }, "web": { "bundler": "metro", diff --git a/app/(auth)/items/[id]/page.tsx b/app/(auth)/items/[id]/page.tsx index 7b8bb901..e69f0f7f 100644 --- a/app/(auth)/items/[id]/page.tsx +++ b/app/(auth)/items/[id]/page.tsx @@ -31,6 +31,7 @@ import { chromecastProfile } from "@/utils/profiles/chromecast"; import ios12 from "@/utils/profiles/ios12"; import { currentlyPlayingItemAtom } from "@/components/CurrentlyPlayingBar"; import { AudioTrackSelector } from "@/components/AudioTrackSelector"; +import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector"; const page: React.FC = () => { const local = useLocalSearchParams(); @@ -42,14 +43,14 @@ const page: React.FC = () => { const castDevice = useCastDevice(); const chromecastReady = useMemo(() => !!castDevice?.deviceId, [castDevice]); - + const [selectedAudioStream, setSelectedAudioStream] = useState(-1); + const [selectedSubtitleStream, setSelectedSubtitleStream] = + useState(0); const [maxBitrate, setMaxBitrate] = useState({ key: "Max", value: undefined, }); - const [selectedAudioStream, setSelectedAudioStream] = useState(0); - const { data: item, isLoading: l1 } = useQuery({ queryKey: ["item", id], queryFn: async () => @@ -94,7 +95,13 @@ const page: React.FC = () => { }); const { data: playbackUrl } = useQuery({ - queryKey: ["playbackUrl", item?.Id, maxBitrate, castDevice], + queryKey: [ + "playbackUrl", + item?.Id, + maxBitrate, + castDevice, + selectedAudioStream, + ], queryFn: async () => { if (!api || !user?.Id || !sessionData) return null; @@ -106,8 +113,12 @@ const page: React.FC = () => { maxStreamingBitrate: maxBitrate.value, sessionData, deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12, + audioStreamIndex: selectedAudioStream, + subtitleStreamIndex: selectedSubtitleStream, }); + console.log("Transcode URL: ", url); + return url; }, enabled: !!sessionData, @@ -240,7 +251,7 @@ const page: React.FC = () => { {item.Overview} - + setMaxBitrate(val)} selected={maxBitrate} @@ -250,6 +261,11 @@ const page: React.FC = () => { onChange={setSelectedAudioStream} selected={selectedAudioStream} /> + diff --git a/components/AudioTrackSelector.tsx b/components/AudioTrackSelector.tsx index 84c777f4..3ee90937 100644 --- a/components/AudioTrackSelector.tsx +++ b/components/AudioTrackSelector.tsx @@ -26,13 +26,15 @@ export const AudioTrackSelector: React.FC = ({ ); const selectedAudioSteam = useMemo( - () => audioStreams?.[selected], + () => audioStreams?.find((x) => x.Index === selected), [audioStreams, selected], ); useEffect(() => { - console.log(audioStreams, selected); - }, [audioStreams, selected]); + const index = item.MediaSources?.[0].DefaultAudioStreamIndex; + if (index !== undefined && index !== null) onChange(index); + }, []); + return ( @@ -58,11 +60,12 @@ export const AudioTrackSelector: React.FC = ({ sideOffset={8} > Audio streams - {audioStreams?.map((audio, index: number) => ( + {audioStreams?.map((audio, idx: number) => ( { - onChange(index); + if (audio.Index !== null && audio.Index !== undefined) + onChange(audio.Index); }} > diff --git a/components/CurrentlyPlayingBar.tsx b/components/CurrentlyPlayingBar.tsx index 42244641..26fc6c27 100644 --- a/components/CurrentlyPlayingBar.tsx +++ b/components/CurrentlyPlayingBar.tsx @@ -8,7 +8,12 @@ import { Text } from "./common/Text"; import { Ionicons } from "@expo/vector-icons"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import Video, { OnProgressData, VideoRef } from "react-native-video"; +import Video, { + OnProgressData, + SelectedTrack, + SelectedTrackType, + VideoRef, +} from "react-native-video"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { atom, useAtom } from "jotai"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; @@ -229,6 +234,9 @@ export const CurrentlyPlayingBar: React.FC = () => { } + subtitleStyle={{ + fontSize: 20, + }} /> )} diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx new file mode 100644 index 00000000..6531490e --- /dev/null +++ b/components/SubtitleTrackSelector.tsx @@ -0,0 +1,92 @@ +import { TouchableOpacity, View } from "react-native"; +import * as DropdownMenu from "zeego/dropdown-menu"; +import { Text } from "./common/Text"; +import { atom, useAtom } from "jotai"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useEffect, useMemo } from "react"; +import { MediaStream } from "@jellyfin/sdk/lib/generated-client/models"; +import { tc } from "@/utils/textTools"; + +interface Props extends React.ComponentProps { + item: BaseItemDto; + onChange: (value: number) => void; + selected: number; +} + +export const SubtitleTrackSelector: React.FC = ({ + item, + onChange, + selected, + ...props +}) => { + const subtitleStreams = useMemo( + () => + item.MediaSources?.[0].MediaStreams?.filter( + (x) => x.Type === "Subtitle", + ) ?? [], + [item], + ); + + const selectedSubtitleSteam = useMemo( + () => subtitleStreams.find((x) => x.Index === selected), + [subtitleStreams, selected], + ); + + useEffect(() => { + const index = item.MediaSources?.[0].DefaultSubtitleStreamIndex; + if (index !== undefined && index !== null) { + onChange(index); + } else { + // Get first subtitle stream + const firstSubtitle = subtitleStreams.find((x) => x.Index !== undefined); + if (firstSubtitle?.Index !== undefined) { + onChange(firstSubtitle.Index); + } + } + }, []); + + if (subtitleStreams.length === 0) return null; + + return ( + + + + + Subtitles + + + + {tc(selectedSubtitleSteam?.DisplayTitle, 13)} + + + + + + + Subtitles + {subtitleStreams?.map((subtitle, idx: number) => ( + { + if (subtitle.Index !== undefined && subtitle.Index !== null) + onChange(subtitle.Index); + }} + > + + {subtitle.DisplayTitle} + + + ))} + + + + ); +}; diff --git a/eas.json b/eas.json index da4ca32b..754a0ec1 100644 --- a/eas.json +++ b/eas.json @@ -21,13 +21,13 @@ } }, "production": { - "channel": "0.2.1", + "channel": "0.3.0", "android": { "image": "latest" } }, "production-apk": { - "channel": "0.2.1", + "channel": "0.3.0", "android": { "buildType": "apk", "image": "latest" diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index fbc6d8bb..92c70ea2 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -14,6 +14,8 @@ export const getStreamUrl = async ({ maxStreamingBitrate, sessionData, deviceProfile = ios12, + audioStreamIndex = 0, + subtitleStreamIndex = 0, }: { api: Api | null | undefined; item: BaseItemDto | null | undefined; @@ -22,6 +24,8 @@ export const getStreamUrl = async ({ maxStreamingBitrate?: number; sessionData: PlaybackInfoResponse; deviceProfile: any; + audioStreamIndex?: number; + subtitleStreamIndex?: number; }) => { if (!api || !userId || !item?.Id) { return null; @@ -40,6 +44,8 @@ export const getStreamUrl = async ({ AutoOpenLiveStream: true, MediaSourceId: itemId, AllowVideoStreamCopy: maxStreamingBitrate ? false : true, + AudioStreamIndex: audioStreamIndex, + SubtitleStreamIndex: subtitleStreamIndex, }, { headers: {