mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
wip
This commit is contained in:
@@ -26,9 +26,10 @@ import { Dimensions, Platform, Pressable, StatusBar, View } from "react-native";
|
||||
import { useSharedValue } from "react-native-reanimated";
|
||||
import Video, { OnProgressData, VideoRef } from "react-native-video";
|
||||
import * as NavigationBar from "expo-navigation-bar";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
|
||||
export default function page() {
|
||||
const { playSettings, playUrl } = usePlaySettings();
|
||||
const { playSettings, playUrl, playSessionId } = usePlaySettings();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const [settings] = useSettings();
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
@@ -38,6 +39,7 @@ export default function page() {
|
||||
|
||||
const screenDimensions = Dimensions.get("screen");
|
||||
|
||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
@@ -68,9 +70,10 @@ export default function page() {
|
||||
? playSettings.subtitleIndex
|
||||
: undefined,
|
||||
mediaSourceId: playSettings.mediaSource?.Id!,
|
||||
positionTicks: Math.round(ticks),
|
||||
positionTicks: Math.floor(ticks),
|
||||
isPaused: true,
|
||||
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: playSessionId ? playSessionId : undefined,
|
||||
});
|
||||
} else {
|
||||
setIsPlaying(true);
|
||||
@@ -84,9 +87,10 @@ export default function page() {
|
||||
? playSettings.subtitleIndex
|
||||
: undefined,
|
||||
mediaSourceId: playSettings.mediaSource?.Id!,
|
||||
positionTicks: Math.round(ticks),
|
||||
positionTicks: Math.floor(ticks),
|
||||
isPaused: false,
|
||||
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: playSessionId ? playSessionId : undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -105,6 +109,7 @@ export default function page() {
|
||||
}, [videoRef]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setIsPlaybackStopped(true);
|
||||
setIsPlaying(false);
|
||||
videoRef.current?.pause();
|
||||
reportPlaybackStopped();
|
||||
@@ -114,7 +119,8 @@ export default function page() {
|
||||
await getPlaystateApi(api).onPlaybackStopped({
|
||||
itemId: playSettings?.item?.Id!,
|
||||
mediaSourceId: playSettings.mediaSource?.Id!,
|
||||
positionTicks: progress.value,
|
||||
positionTicks: Math.floor(progress.value),
|
||||
playSessionId: playSessionId ? playSessionId : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -129,12 +135,14 @@ export default function page() {
|
||||
: undefined,
|
||||
mediaSourceId: playSettings.mediaSource?.Id!,
|
||||
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: playSessionId ? playSessionId : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const onProgress = useCallback(
|
||||
async (data: OnProgressData) => {
|
||||
if (isSeeking.value === true) return;
|
||||
if (isPlaybackStopped) return;
|
||||
|
||||
const ticks = data.currentTime * 10000000;
|
||||
|
||||
@@ -156,17 +164,21 @@ export default function page() {
|
||||
positionTicks: Math.round(ticks),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: playSessionId ? playSessionId : undefined,
|
||||
});
|
||||
},
|
||||
[playSettings?.item.Id, isPlaying, api]
|
||||
[playSettings?.item.Id, isPlaying, api, isPlaybackStopped]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
play();
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, []);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
play();
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, [play, stop])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const orientationSubscription =
|
||||
@@ -250,6 +262,7 @@ export default function page() {
|
||||
firstTime.current = false;
|
||||
}
|
||||
}}
|
||||
progressUpdateInterval={500}
|
||||
playWhenInactive={true}
|
||||
allowsExternalPlayback={true}
|
||||
playInBackground={true}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SongsListItem: React.FC<Props> = ({
|
||||
const client = useRemoteMediaClient();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
|
||||
const { playSettings, setPlaySettings } = usePlaySettings();
|
||||
const { setPlaySettings } = usePlaySettings();
|
||||
|
||||
const openSelect = () => {
|
||||
if (!castDevice?.deviceId) {
|
||||
@@ -85,7 +85,7 @@ export const SongsListItem: React.FC<Props> = ({
|
||||
|
||||
const sessionData = response.data;
|
||||
|
||||
const url = await getStreamUrl({
|
||||
const data = await getStreamUrl({
|
||||
api,
|
||||
userId: user.Id,
|
||||
item,
|
||||
@@ -95,8 +95,8 @@ export const SongsListItem: React.FC<Props> = ({
|
||||
mediaSourceId: item.Id,
|
||||
});
|
||||
|
||||
if (!url || !item) {
|
||||
console.warn("No url or item", url, item.Id);
|
||||
if (!data?.url || !item) {
|
||||
console.warn("No url or item", data?.url, item.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export const SongsListItem: React.FC<Props> = ({
|
||||
else {
|
||||
client.loadMedia({
|
||||
mediaInfo: {
|
||||
contentUrl: url,
|
||||
contentUrl: data.url!,
|
||||
contentType: "video/mp4",
|
||||
metadata: {
|
||||
type: item.Type === "Episode" ? "tvShow" : "movie",
|
||||
@@ -120,7 +120,7 @@ export const SongsListItem: React.FC<Props> = ({
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("Playing on device", url, item.Id);
|
||||
console.log("Playing on device", data.url, item.Id);
|
||||
setPlaySettings({
|
||||
item,
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ export const useCreditSkipper = (
|
||||
return res?.data;
|
||||
},
|
||||
enabled: !!itemId,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useCallback } from "react";
|
||||
|
||||
export const useFileOpener = () => {
|
||||
const router = useRouter();
|
||||
const { setPlaySettings, setPlayUrl, setOfflineSettings } = usePlaySettings();
|
||||
const { setPlayUrl, setOfflineSettings } = usePlaySettings();
|
||||
|
||||
const openFile = useCallback(async (item: BaseItemDto) => {
|
||||
const directory = FileSystem.documentDirectory;
|
||||
|
||||
@@ -44,6 +44,7 @@ export const useIntroSkipper = (
|
||||
return res?.data;
|
||||
},
|
||||
enabled: !!itemId,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -103,7 +103,7 @@ export const useWebSocket = ({
|
||||
console.log("Command ~ DisplayMessage");
|
||||
const title = json?.Data?.Arguments?.Header;
|
||||
const body = json?.Data?.Arguments?.Text;
|
||||
Alert.alert(title, body);
|
||||
Alert.alert("Message from server: " + title, body);
|
||||
}
|
||||
};
|
||||
}, [ws, stopPlayback, playVideo, pauseVideo, isPlaying, router]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Bitrate } from "@/components/BitrateSelector";
|
||||
import { settingsAtom } from "@/utils/atoms/settings";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlaybackStopped";
|
||||
import ios from "@/utils/profiles/ios";
|
||||
import native from "@/utils/profiles/native";
|
||||
import old from "@/utils/profiles/old";
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { getPlaystateApi, getSessionApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React, {
|
||||
createContext,
|
||||
@@ -18,7 +17,7 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { apiAtom, userAtom } from "./JellyfinProvider";
|
||||
import { apiAtom, getOrSetDeviceId, userAtom } from "./JellyfinProvider";
|
||||
|
||||
export type PlaybackType = {
|
||||
item?: BaseItemDto | null;
|
||||
@@ -31,10 +30,10 @@ export type PlaybackType = {
|
||||
type PlaySettingsContextType = {
|
||||
playSettings: PlaybackType | null;
|
||||
setPlaySettings: React.Dispatch<React.SetStateAction<PlaybackType | null>>;
|
||||
setOfflineSettings: (data: PlaybackType) => void;
|
||||
playUrl?: string | null;
|
||||
reportStopPlayback: (ticks: number) => Promise<void>;
|
||||
setPlayUrl: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
playSessionId?: string | null;
|
||||
setOfflineSettings: (data: PlaybackType) => void;
|
||||
};
|
||||
|
||||
const PlaySettingsContext = createContext<PlaySettingsContextType | undefined>(
|
||||
@@ -46,26 +45,12 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
}) => {
|
||||
const [playSettings, _setPlaySettings] = useState<PlaybackType | null>(null);
|
||||
const [playUrl, setPlayUrl] = useState<string | null>(null);
|
||||
const [playSessionId, setPlaySessionId] = useState<string | null>(null);
|
||||
|
||||
const api = useAtomValue(apiAtom);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
|
||||
const reportStopPlayback = useCallback(
|
||||
async (ticks: number) => {
|
||||
const id = playSettings?.item?.Id;
|
||||
setPlaySettings(null);
|
||||
|
||||
await reportPlaybackStopped({
|
||||
api,
|
||||
itemId: id,
|
||||
sessionId: undefined,
|
||||
positionTicks: ticks,
|
||||
});
|
||||
},
|
||||
[playSettings?.item?.Id, api]
|
||||
);
|
||||
|
||||
const setOfflineSettings = useCallback((data: PlaybackType) => {
|
||||
_setPlaySettings(data);
|
||||
}, []);
|
||||
@@ -107,8 +92,9 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
userId: user.Id,
|
||||
forceDirectPlay: false,
|
||||
sessionData: null,
|
||||
}).then((url) => {
|
||||
if (url) setPlayUrl(url);
|
||||
}).then((data) => {
|
||||
setPlayUrl(data?.url!);
|
||||
setPlaySessionId(data?.sessionId!);
|
||||
});
|
||||
|
||||
return newSettings;
|
||||
@@ -129,7 +115,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
AppStoreUrl: "https://apps.apple.com/us/app/streamyfin/id6593660679",
|
||||
DeviceProfile: deviceProfile,
|
||||
IconUrl:
|
||||
"https://github.com/fredrikburmester/streamyfin/blob/master/assets/images/adaptive_icon.png",
|
||||
"https://raw.githubusercontent.com/retardgerman/streamyfinweb/refs/heads/redesign/public/assets/images/icon_new_withoutBackground.png",
|
||||
PlayableMediaTypes: ["Audio", "Video"],
|
||||
SupportedCommands: ["Play"],
|
||||
SupportsMediaControl: true,
|
||||
@@ -139,7 +125,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
};
|
||||
|
||||
postCaps();
|
||||
}, [settings]);
|
||||
}, [settings, api]);
|
||||
|
||||
return (
|
||||
<PlaySettingsContext.Provider
|
||||
@@ -147,9 +133,9 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
playSettings,
|
||||
setPlaySettings,
|
||||
playUrl,
|
||||
reportStopPlayback,
|
||||
setPlayUrl,
|
||||
setOfflineSettings,
|
||||
playSessionId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -33,18 +33,17 @@ export const getStreamUrl = async ({
|
||||
forceDirectPlay?: boolean;
|
||||
height?: number;
|
||||
mediaSourceId?: string | null;
|
||||
}) => {
|
||||
}): Promise<{
|
||||
url: string | null | undefined;
|
||||
sessionId: string | null | undefined;
|
||||
} | null> => {
|
||||
if (!api || !userId || !item?.Id) {
|
||||
console.log("getStreamUrl: missing params", {
|
||||
api: api?.basePath,
|
||||
userId,
|
||||
item: item?.Id,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
let mediaSource: MediaSourceInfo | undefined;
|
||||
let url: string | null | undefined;
|
||||
let sessionId: string | null | undefined;
|
||||
|
||||
if (item.Type === "Program") {
|
||||
const res0 = await getMediaInfoApi(api).getPlaybackInfo(
|
||||
@@ -67,35 +66,67 @@ export const getStreamUrl = async ({
|
||||
}
|
||||
);
|
||||
const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl;
|
||||
if (transcodeUrl) return `${api.basePath}${transcodeUrl}`;
|
||||
sessionId = res0.data.PlaySessionId;
|
||||
|
||||
if (transcodeUrl) {
|
||||
return { url: `${api.basePath}${transcodeUrl}`, sessionId };
|
||||
}
|
||||
}
|
||||
|
||||
const itemId = item.Id;
|
||||
|
||||
const res2 = await api.axiosInstance.post(
|
||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
// const res2 = await api.axiosInstance.post(
|
||||
// `${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
// {
|
||||
// DeviceProfile: deviceProfile,
|
||||
// UserId: userId,
|
||||
// MaxStreamingBitrate: maxStreamingBitrate,
|
||||
// StartTimeTicks: startTimeTicks,
|
||||
// EnableTranscoding: maxStreamingBitrate ? true : undefined,
|
||||
// AutoOpenLiveStream: true,
|
||||
// MediaSourceId: mediaSourceId,
|
||||
// AllowVideoStreamCopy: maxStreamingBitrate ? false : true,
|
||||
// AudioStreamIndex: audioStreamIndex,
|
||||
// SubtitleStreamIndex: subtitleStreamIndex,
|
||||
// DeInterlace: true,
|
||||
// BreakOnNonKeyFrames: false,
|
||||
// CopyTimestamps: false,
|
||||
// EnableMpegtsM2TsMode: false,
|
||||
// },
|
||||
// {
|
||||
// headers: getAuthHeaders(api),
|
||||
// }
|
||||
// );
|
||||
|
||||
const res2 = await getMediaInfoApi(api).getPlaybackInfo(
|
||||
{
|
||||
DeviceProfile: deviceProfile,
|
||||
UserId: userId,
|
||||
MaxStreamingBitrate: maxStreamingBitrate,
|
||||
StartTimeTicks: startTimeTicks,
|
||||
EnableTranscoding: maxStreamingBitrate ? true : undefined,
|
||||
AutoOpenLiveStream: true,
|
||||
MediaSourceId: mediaSourceId,
|
||||
AllowVideoStreamCopy: maxStreamingBitrate ? false : true,
|
||||
AudioStreamIndex: audioStreamIndex,
|
||||
SubtitleStreamIndex: subtitleStreamIndex,
|
||||
DeInterlace: true,
|
||||
BreakOnNonKeyFrames: false,
|
||||
CopyTimestamps: false,
|
||||
EnableMpegtsM2TsMode: false,
|
||||
userId,
|
||||
itemId: item.Id!,
|
||||
},
|
||||
{
|
||||
headers: getAuthHeaders(api),
|
||||
method: "POST",
|
||||
data: {
|
||||
deviceProfile,
|
||||
userId,
|
||||
maxStreamingBitrate,
|
||||
startTimeTicks,
|
||||
enableTranscoding: maxStreamingBitrate ? true : undefined,
|
||||
autoOpenLiveStream: true,
|
||||
mediaSourceId,
|
||||
allowVideoStreamCopy: maxStreamingBitrate ? false : true,
|
||||
audioStreamIndex,
|
||||
subtitleStreamIndex,
|
||||
deInterlace: true,
|
||||
breakOnNonKeyFrames: false,
|
||||
copyTimestamps: false,
|
||||
enableMpegtsM2TsMode: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
mediaSource = res2.data.MediaSources.find(
|
||||
sessionId = res2.data.PlaySessionId;
|
||||
|
||||
mediaSource = res2.data.MediaSources?.find(
|
||||
(source: MediaSourceInfo) => source.Id === mediaSourceId
|
||||
);
|
||||
|
||||
@@ -136,5 +167,8 @@ export const getStreamUrl = async ({
|
||||
return null;
|
||||
}
|
||||
|
||||
return url;
|
||||
return {
|
||||
url,
|
||||
sessionId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { AxiosError } from "axios";
|
||||
import { getAuthHeaders } from "../jellyfin";
|
||||
|
||||
interface PlaybackStoppedParams {
|
||||
api: Api | null | undefined;
|
||||
sessionId: string | null | undefined;
|
||||
itemId: string | null | undefined;
|
||||
positionTicks: number | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports playback stopped event to the Jellyfin server.
|
||||
*
|
||||
* @param {PlaybackStoppedParams} params - The parameters for the report.
|
||||
* @param {Api} params.api - The Jellyfin API instance.
|
||||
* @param {string} params.sessionId - The session ID.
|
||||
* @param {string} params.itemId - The item ID.
|
||||
* @param {number} params.positionTicks - The playback position in ticks.
|
||||
*/
|
||||
export const reportPlaybackStopped = async ({
|
||||
api,
|
||||
sessionId,
|
||||
itemId,
|
||||
positionTicks,
|
||||
}: PlaybackStoppedParams): Promise<void> => {
|
||||
if (!positionTicks || positionTicks === 0) return;
|
||||
|
||||
if (!api) {
|
||||
console.error("Missing api");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionId) {
|
||||
console.error("Missing sessionId", sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!itemId) {
|
||||
console.error("Missing itemId");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${api.basePath}/PlayingItems/${itemId}`;
|
||||
const params = {
|
||||
playSessionId: sessionId,
|
||||
positionTicks: Math.round(positionTicks),
|
||||
MediaSourceId: itemId,
|
||||
IsPaused: true,
|
||||
};
|
||||
const headers = getAuthHeaders(api);
|
||||
|
||||
// Send DELETE request to report playback stopped
|
||||
await api.axiosInstance.delete(url, { params, headers });
|
||||
} catch (error) {
|
||||
// Log the error with additional context
|
||||
if (error instanceof AxiosError) {
|
||||
console.error(
|
||||
"Failed to report playback progress",
|
||||
error.message,
|
||||
error.response?.data
|
||||
);
|
||||
} else {
|
||||
console.error("Failed to report playback progress", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user