mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: streaming live tv now works
This commit is contained in:
@@ -126,7 +126,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
});
|
||||
|
||||
const [localItem, setLocalItem] = useState(item);
|
||||
useImageColors(item);
|
||||
useImageColors({ item });
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
@@ -180,10 +180,15 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
|
||||
queryKey: ["sessionData", item?.Id],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id || !item?.Id) return null;
|
||||
const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
|
||||
itemId: item?.Id,
|
||||
userId: user?.Id,
|
||||
});
|
||||
const playbackData = await getMediaInfoApi(api!).getPlaybackInfo(
|
||||
{
|
||||
itemId: item?.Id,
|
||||
userId: user?.Id,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
return playbackData.data;
|
||||
},
|
||||
|
||||
@@ -15,17 +15,13 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { TouchableOpacityProps, View } from "react-native";
|
||||
import { getColors } from "react-native-image-colors";
|
||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||
import { useImageColors } from "@/hooks/useImageColors";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
|
||||
interface Props extends TouchableOpacityProps {
|
||||
library: BaseItemDto;
|
||||
}
|
||||
|
||||
type LibraryColor = {
|
||||
dominantColor: string;
|
||||
averageColor: string;
|
||||
secondary: string;
|
||||
};
|
||||
|
||||
type IconName = React.ComponentProps<typeof Ionicons>["name"];
|
||||
|
||||
const icons: Record<CollectionType, IconName> = {
|
||||
@@ -48,12 +44,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
||||
const [user] = useAtom(userAtom);
|
||||
const [settings] = useSettings();
|
||||
|
||||
const [imageInfo, setImageInfo] = useState<LibraryColor>({
|
||||
dominantColor: "#fff",
|
||||
averageColor: "#fff",
|
||||
secondary: "#fff",
|
||||
});
|
||||
|
||||
const url = useMemo(
|
||||
() =>
|
||||
getPrimaryImageUrl({
|
||||
@@ -63,6 +53,10 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
||||
[library]
|
||||
);
|
||||
|
||||
// If we want to use image colors for library cards
|
||||
// const [color] = useAtom(itemThemeColorAtom)
|
||||
// useImageColors({ url });
|
||||
|
||||
const { data: itemsCount } = useQuery({
|
||||
queryKey: ["library-count", library.Id],
|
||||
queryFn: async () => {
|
||||
@@ -76,40 +70,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
getColors(url, {
|
||||
fallback: "#fff",
|
||||
cache: true,
|
||||
key: url,
|
||||
})
|
||||
.then((colors) => {
|
||||
let dominantColor: string = "#fff";
|
||||
let averageColor: string = "#fff";
|
||||
let secondary: string = "#fff";
|
||||
|
||||
if (colors.platform === "android") {
|
||||
dominantColor = colors.dominant;
|
||||
averageColor = colors.average;
|
||||
secondary = colors.muted;
|
||||
} else if (colors.platform === "ios") {
|
||||
dominantColor = colors.primary;
|
||||
averageColor = colors.background;
|
||||
secondary = colors.detail;
|
||||
}
|
||||
|
||||
setImageInfo({
|
||||
dominantColor,
|
||||
averageColor,
|
||||
secondary,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error getting colors", error);
|
||||
});
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
if (!url) return null;
|
||||
|
||||
if (settings?.libraryOptions?.display === "row") {
|
||||
|
||||
@@ -19,19 +19,30 @@ import { getColors } from "react-native-image-colors";
|
||||
* @param disabled - A boolean flag to disable color extraction.
|
||||
*
|
||||
*/
|
||||
export const useImageColors = (item?: BaseItemDto | null, disabled = false) => {
|
||||
export const useImageColors = ({
|
||||
item,
|
||||
url,
|
||||
disabled,
|
||||
}: {
|
||||
item?: BaseItemDto | null;
|
||||
url?: string | null;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [, setPrimaryColor] = useAtom(itemThemeColorAtom);
|
||||
|
||||
const source = useMemo(() => {
|
||||
if (!api || !item) return;
|
||||
return getItemImage({
|
||||
item,
|
||||
api,
|
||||
variant: "Primary",
|
||||
quality: 80,
|
||||
width: 300,
|
||||
});
|
||||
if (!api) return;
|
||||
if (url) return { uri: url };
|
||||
else if (item)
|
||||
return getItemImage({
|
||||
item,
|
||||
api,
|
||||
variant: "Primary",
|
||||
quality: 80,
|
||||
width: 300,
|
||||
});
|
||||
else return;
|
||||
}, [api, item]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -128,10 +128,17 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await getMediaInfoApi(api!).getPlaybackInfo({
|
||||
itemId: state.item.Id,
|
||||
userId: user.Id,
|
||||
});
|
||||
// Support live tv
|
||||
const res =
|
||||
state.item.Type !== "Program"
|
||||
? await getMediaInfoApi(api!).getPlaybackInfo({
|
||||
itemId: state.item.Id,
|
||||
userId: user.Id,
|
||||
})
|
||||
: await getMediaInfoApi(api!).getPlaybackInfo({
|
||||
itemId: state.item.ChannelId!,
|
||||
userId: user.Id,
|
||||
});
|
||||
|
||||
await postCapabilities({
|
||||
api,
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import { getAuthHeaders } from "../jellyfin";
|
||||
import iosFmp4 from "@/utils/profiles/iosFmp4";
|
||||
import { getItemsApi, getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { isPlainObject } from "lodash";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
export const getStreamUrl = async ({
|
||||
api,
|
||||
@@ -39,27 +41,42 @@ export const getStreamUrl = async ({
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("[0] getStreamUrl ~");
|
||||
let mediaSource: MediaSourceInfo | undefined;
|
||||
let url: string | null | undefined;
|
||||
|
||||
if (item.Type === "Program") {
|
||||
const res0 = await getMediaInfoApi(api).getPlaybackInfo(
|
||||
{
|
||||
userId,
|
||||
itemId: item.ChannelId!,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
params: {
|
||||
startTimeTicks: 0,
|
||||
isPlayback: true,
|
||||
autoOpenLiveStream: true,
|
||||
maxStreamingBitrate,
|
||||
audioStreamIndex,
|
||||
},
|
||||
data: {
|
||||
deviceProfile,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mediaSourceId = res0.data.MediaSources?.[0].Id;
|
||||
const liveStreamId = res0.data.MediaSources?.[0].LiveStreamId;
|
||||
|
||||
const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl;
|
||||
|
||||
console.log("transcodeUrl", transcodeUrl);
|
||||
|
||||
if (transcodeUrl) return `${api.basePath}${transcodeUrl}`;
|
||||
}
|
||||
|
||||
const itemId = item.Id;
|
||||
|
||||
console.log("[1] getStreamUrl ~");
|
||||
const res1 = await api.axiosInstance.post(
|
||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
{
|
||||
UserId: itemId,
|
||||
StartTimeTicks: 0,
|
||||
IsPlayback: true,
|
||||
AutoOpenLiveStream: true,
|
||||
MaxStreamingBitrate: 140000000,
|
||||
},
|
||||
{
|
||||
headers: getAuthHeaders(api),
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[2] getStreamUrl ~", res1.status, res1.statusText);
|
||||
|
||||
const res2 = await api.axiosInstance.post(
|
||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
{
|
||||
@@ -83,23 +100,13 @@ export const getStreamUrl = async ({
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[3] getStreamUrl ~");
|
||||
|
||||
console.log(
|
||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
res2.status,
|
||||
res2.statusText
|
||||
);
|
||||
|
||||
const mediaSource: MediaSourceInfo = res2.data.MediaSources.find(
|
||||
mediaSource = res2.data.MediaSources.find(
|
||||
(source: MediaSourceInfo) => source.Id === mediaSourceId
|
||||
);
|
||||
|
||||
let url: string | null | undefined;
|
||||
|
||||
if (mediaSource.SupportsDirectPlay || forceDirectPlay === true) {
|
||||
if (mediaSource?.SupportsDirectPlay || forceDirectPlay === true) {
|
||||
if (item.MediaType === "Video") {
|
||||
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}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData?.PlaySessionId}&mediaSourceId=${mediaSource?.Id}&static=true&subtitleStreamIndex=${subtitleStreamIndex}&audioStreamIndex=${audioStreamIndex}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`;
|
||||
} else if (item.MediaType === "Audio") {
|
||||
const searchParams = new URLSearchParams({
|
||||
UserId: userId,
|
||||
@@ -120,18 +127,11 @@ export const getStreamUrl = async ({
|
||||
api.basePath
|
||||
}/Audio/${itemId}/universal?${searchParams.toString()}`;
|
||||
}
|
||||
} else if (mediaSource.TranscodingUrl) {
|
||||
} else if (mediaSource?.TranscodingUrl) {
|
||||
url = `${api.basePath}${mediaSource.TranscodingUrl}`;
|
||||
}
|
||||
|
||||
if (!url) throw new Error("No url");
|
||||
|
||||
console.log(
|
||||
mediaSource.VideoType,
|
||||
mediaSource.Container,
|
||||
mediaSource.TranscodingContainer,
|
||||
mediaSource.TranscodingSubProtocol
|
||||
);
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user