feat: audio and subtitle picker

This commit is contained in:
Fredrik Burmester
2024-08-12 22:24:51 +02:00
parent 65837cd303
commit cc72186a80
7 changed files with 142 additions and 22 deletions

View File

@@ -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",

View File

@@ -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<number>(-1);
const [selectedSubtitleStream, setSelectedSubtitleStream] =
useState<number>(0);
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
key: "Max",
value: undefined,
});
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(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 = () => {
<Text>{item.Overview}</Text>
</View>
<View className="flex flex-col p-4">
<View className="flex flex-row items-center space-x-4 w-full">
<View className="flex flex-row items-center space-x-2 w-full">
<BitrateSelector
onChange={(val) => setMaxBitrate(val)}
selected={maxBitrate}
@@ -250,6 +261,11 @@ const page: React.FC = () => {
onChange={setSelectedAudioStream}
selected={selectedAudioStream}
/>
<SubtitleTrackSelector
item={item}
onChange={setSelectedSubtitleStream}
selected={selectedSubtitleStream}
/>
</View>
<PlayButton item={item} chromecastReady={false} onPress={onPressPlay} />
</View>

View File

@@ -26,13 +26,15 @@ export const AudioTrackSelector: React.FC<Props> = ({
);
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 (
<View className="flex flex-row items-center justify-between" {...props}>
<DropdownMenu.Root>
@@ -58,11 +60,12 @@ export const AudioTrackSelector: React.FC<Props> = ({
sideOffset={8}
>
<DropdownMenu.Label>Audio streams</DropdownMenu.Label>
{audioStreams?.map((audio, index: number) => (
{audioStreams?.map((audio, idx: number) => (
<DropdownMenu.Item
key={index.toString()}
key={idx.toString()}
onSelect={() => {
onChange(index);
if (audio.Index !== null && audio.Index !== undefined)
onChange(audio.Index);
}}
>
<DropdownMenu.ItemTitle>

View File

@@ -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 = () => {
<ActivityIndicator size={"small"} color={"white"} />
</View>
}
subtitleStyle={{
fontSize: 20,
}}
/>
)}
</TouchableOpacity>

View File

@@ -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<typeof View> {
item: BaseItemDto;
onChange: (value: number) => void;
selected: number;
}
export const SubtitleTrackSelector: React.FC<Props> = ({
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 (
<View className="flex flex-row items-center justify-between" {...props}>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<View className="flex flex-col mb-2">
<Text className="opacity-50 mb-1 text-xs">Subtitles</Text>
<View className="flex flex-row">
<TouchableOpacity className="bg-neutral-900 max-w-32 h-12 rounded-2xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text className="">
{tc(selectedSubtitleSteam?.DisplayTitle, 13)}
</Text>
</TouchableOpacity>
</View>
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={true}
side="bottom"
align="start"
alignOffset={0}
avoidCollisions={true}
collisionPadding={8}
sideOffset={8}
>
<DropdownMenu.Label>Subtitles</DropdownMenu.Label>
{subtitleStreams?.map((subtitle, idx: number) => (
<DropdownMenu.Item
key={idx.toString()}
onSelect={() => {
if (subtitle.Index !== undefined && subtitle.Index !== null)
onChange(subtitle.Index);
}}
>
<DropdownMenu.ItemTitle>
{subtitle.DisplayTitle}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
);
};

View File

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

View File

@@ -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: {