This commit is contained in:
Fredrik Burmester
2024-10-07 10:00:16 +02:00
parent 4b60de4d43
commit a5b4f6cc78
9 changed files with 105 additions and 138 deletions

View File

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

View File

@@ -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,
});

View File

@@ -48,6 +48,7 @@ export const useCreditSkipper = (
return res?.data;
},
enabled: !!itemId,
retry: false,
});
useEffect(() => {

View File

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

View File

@@ -44,6 +44,7 @@ export const useIntroSkipper = (
return res?.data;
},
enabled: !!itemId,
retry: false,
});
useEffect(() => {

View File

@@ -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]);

View File

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

View File

@@ -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,
};
};

View File

@@ -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);
}
}
};