feat: select media source

This commit is contained in:
Fredrik Burmester
2024-08-27 08:26:27 +02:00
parent 2565bf7353
commit 91ed109a04
6 changed files with 139 additions and 38 deletions

View File

@@ -2,27 +2,29 @@ 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 {
BaseItemDto,
MediaSourceInfo,
} 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;
source: MediaSourceInfo;
onChange: (value: number) => void;
selected: number;
}
export const AudioTrackSelector: React.FC<Props> = ({
item,
source,
onChange,
selected,
...props
}) => {
const audioStreams = useMemo(
() =>
item.MediaSources?.[0].MediaStreams?.filter((x) => x.Type === "Audio"),
[item]
() => source.MediaStreams?.filter((x) => x.Type === "Audio"),
[source]
);
const selectedAudioSteam = useMemo(
@@ -31,7 +33,7 @@ export const AudioTrackSelector: React.FC<Props> = ({
);
useEffect(() => {
const index = item.MediaSources?.[0].DefaultAudioStreamIndex;
const index = source.DefaultAudioStreamIndex;
if (index !== undefined && index !== null) onChange(index);
}, []);
@@ -44,7 +46,7 @@ export const AudioTrackSelector: React.FC<Props> = ({
<View className="flex flex-row">
<TouchableOpacity className="bg-neutral-900 max-w-32 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text className="">
{tc(selectedAudioSteam?.DisplayTitle, 13)}
{tc(selectedAudioSteam?.DisplayTitle, 7)}
</Text>
</TouchableOpacity>
</View>

View File

@@ -58,7 +58,7 @@ export const BitrateSelector: React.FC<Props> = ({
<Text className="opacity-50 mb-1 text-xs">Bitrate</Text>
<View className="flex flex-row">
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>
<Text className="">
{BITRATES.find((b) => b.value === selected.value)?.key}
</Text>
</TouchableOpacity>

View File

@@ -32,6 +32,8 @@ import React, { useEffect, useMemo, useState } from "react";
import { View } from "react-native";
import { useCastDevice } from "react-native-google-cast";
import { ItemHeader } from "./ItemHeader";
import { MediaSourceSelector } from "./MediaSourceSelector";
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
const [api] = useAtom(apiAtom);
@@ -40,6 +42,8 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
const [settings] = useSettings();
const castDevice = useCastDevice();
const [selectedMediaSource, setSelectedMediaSource] =
useState<MediaSourceInfo | null>(null);
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
const [selectedSubtitleStream, setSelectedSubtitleStream] =
useState<number>(0);
@@ -85,6 +89,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
item?.Id,
maxBitrate,
castDevice,
selectedMediaSource,
selectedAudioStream,
selectedSubtitleStream,
settings,
@@ -114,9 +119,10 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
subtitleStreamIndex: selectedSubtitleStream,
forceDirectPlay: settings?.forceDirectPlay,
height: maxBitrate.height,
mediaSourceId: selectedMediaSource?.Id,
});
console.log("Transcode URL: ", url);
console.info("Stream URL:", url);
return url;
},
@@ -194,19 +200,24 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
onChange={(val) => setMaxBitrate(val)}
selected={maxBitrate}
/>
{item && (
<AudioTrackSelector
item={item}
onChange={setSelectedAudioStream}
selected={selectedAudioStream}
/>
)}
{item && (
<SubtitleTrackSelector
item={item}
onChange={setSelectedSubtitleStream}
selected={selectedSubtitleStream}
/>
<MediaSourceSelector
item={item}
onChange={setSelectedMediaSource}
selected={selectedMediaSource}
/>
{selectedMediaSource && (
<View className="flex flex-row items-center space-x-2">
<AudioTrackSelector
source={selectedMediaSource}
onChange={setSelectedAudioStream}
selected={selectedAudioStream}
/>
<SubtitleTrackSelector
source={selectedMediaSource}
onChange={setSelectedSubtitleStream}
selected={selectedSubtitleStream}
/>
</View>
)}
</View>
) : (
@@ -219,7 +230,9 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
<PlayButton item={item} url={playbackUrl} className="grow mb-2" />
</View>
<SeasonEpisodesCarousel item={item} loading={loading} />
{item?.Type === "Episode" && (
<SeasonEpisodesCarousel item={item} loading={loading} />
)}
<OverviewText text={item?.Overview} className="px-4 mb-4" />

View File

@@ -0,0 +1,81 @@
import { tc } from "@/utils/textTools";
import {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import { useEffect, useMemo } from "react";
import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "./common/Text";
interface Props extends React.ComponentProps<typeof View> {
item: BaseItemDto;
onChange: (value: MediaSourceInfo) => void;
selected: MediaSourceInfo | null;
}
export const MediaSourceSelector: React.FC<Props> = ({
item,
onChange,
selected,
...props
}) => {
const mediaSources = useMemo(() => {
return item.MediaSources;
}, [item]);
const selectedMediaSource = useMemo(
() =>
mediaSources
?.find((x) => x.Id === selected?.Id)
?.MediaStreams?.find((x) => x.Type === "Video")?.DisplayTitle || "",
[mediaSources, selected]
);
useEffect(() => {
if (mediaSources?.length) onChange(mediaSources[0]);
}, []);
return (
<View className="flex flex-row items-center justify-between" {...props}>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<View className="flex flex-col">
<Text className="opacity-50 mb-1 text-xs">Video streams</Text>
<View className="flex flex-row">
<TouchableOpacity className="bg-neutral-900 max-w-32 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text className="">{tc(selectedMediaSource, 7)}</Text>
</TouchableOpacity>
</View>
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={true}
side="bottom"
align="start"
alignOffset={0}
avoidCollisions={true}
collisionPadding={8}
sideOffset={8}
>
<DropdownMenu.Label>Video streams</DropdownMenu.Label>
{mediaSources?.map((source, idx: number) => (
<DropdownMenu.Item
key={idx.toString()}
onSelect={() => {
onChange(source);
}}
>
<DropdownMenu.ItemTitle>
{
source.MediaStreams?.find((s) => s.Type === "Video")
?.DisplayTitle
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
);
};

View File

@@ -2,29 +2,29 @@ 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 {
BaseItemDto,
MediaSourceInfo,
} 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;
source: MediaSourceInfo;
onChange: (value: number) => void;
selected: number;
}
export const SubtitleTrackSelector: React.FC<Props> = ({
item,
source,
onChange,
selected,
...props
}) => {
const subtitleStreams = useMemo(
() =>
item.MediaSources?.[0].MediaStreams?.filter(
(x) => x.Type === "Subtitle"
) ?? [],
[item]
() => source.MediaStreams?.filter((x) => x.Type === "Subtitle") ?? [],
[source]
);
const selectedSubtitleSteam = useMemo(
@@ -33,7 +33,7 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
);
useEffect(() => {
const index = item.MediaSources?.[0].DefaultSubtitleStreamIndex;
const index = source.DefaultSubtitleStreamIndex;
if (index !== undefined && index !== null) {
onChange(index);
} else {
@@ -53,7 +53,7 @@ export const SubtitleTrackSelector: React.FC<Props> = ({
<TouchableOpacity className="bg-neutral-900 max-w-32 h-10 rounded-xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text className="">
{selectedSubtitleSteam
? tc(selectedSubtitleSteam?.DisplayTitle, 13)
? tc(selectedSubtitleSteam?.DisplayTitle, 7)
: "None"}
</Text>
</TouchableOpacity>

View File

@@ -18,6 +18,7 @@ export const getStreamUrl = async ({
subtitleStreamIndex = 0,
forceDirectPlay = false,
height,
mediaSourceId,
}: {
api: Api | null | undefined;
item: BaseItemDto | null | undefined;
@@ -30,8 +31,10 @@ export const getStreamUrl = async ({
subtitleStreamIndex?: number;
forceDirectPlay?: boolean;
height?: number;
mediaSourceId?: string | null;
}) => {
if (!api || !userId || !item?.Id) {
if (!api || !userId || !item?.Id || !mediaSourceId) {
console.error("Missing required parameters");
return null;
}
@@ -46,7 +49,7 @@ export const getStreamUrl = async ({
StartTimeTicks: startTimeTicks,
EnableTranscoding: maxStreamingBitrate ? true : undefined,
AutoOpenLiveStream: true,
MediaSourceId: itemId,
MediaSourceId: mediaSourceId,
AllowVideoStreamCopy: maxStreamingBitrate ? false : true,
AudioStreamIndex: audioStreamIndex,
SubtitleStreamIndex: subtitleStreamIndex,
@@ -62,7 +65,9 @@ export const getStreamUrl = async ({
}
);
const mediaSource = response.data.MediaSources?.[0] as MediaSourceInfo;
const mediaSource: MediaSourceInfo = response.data.MediaSources.find(
(source: MediaSourceInfo) => source.Id === mediaSourceId
);
if (!mediaSource) {
throw new Error("No media source");
@@ -75,7 +80,7 @@ export const getStreamUrl = async ({
if (mediaSource.SupportsDirectPlay || forceDirectPlay === true) {
if (item.MediaType === "Video") {
console.log("Using direct stream for video!");
return `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData.PlaySessionId}&mediaSourceId=${itemId}&static=true`;
return `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData.PlaySessionId}&mediaSourceId=${mediaSource.Id}&static=true`;
} else if (item.MediaType === "Audio") {
console.log("Using direct stream for audio!");
const searchParams = new URLSearchParams({