From b0a32c67cc0fea0219bec86ad9e1519dff5b0b02 Mon Sep 17 00:00:00 2001 From: Prayag Prajapati Date: Sun, 17 Mar 2024 15:35:46 +0530 Subject: [PATCH] feat(player): sync playback with jellyfin server --- src/components/buttons/playButton.tsx | 13 +- .../homeSection/latestMediaSection.jsx | 2 +- src/main.tsx | 3 +- src/routes/home/index.jsx | 2 + src/routes/player/videoPlayer.tsx | 119 +++++++++++++++++- src/utils/store/playback.ts | 6 +- 6 files changed, 131 insertions(+), 14 deletions(-) diff --git a/src/components/buttons/playButton.tsx b/src/components/buttons/playButton.tsx index d76cb52d..03f5fbe4 100644 --- a/src/components/buttons/playButton.tsx +++ b/src/components/buttons/playButton.tsx @@ -2,20 +2,20 @@ import PropTypes from "prop-types"; import React from "react"; import Button, { - ButtonProps, - ButtonPropsSizeOverrides, + type ButtonProps, + type ButtonPropsSizeOverrides, } from "@mui/material/Button"; import Fab from "@mui/material/Fab"; import LinearProgress from "@mui/material/LinearProgress"; import Typography from "@mui/material/Typography"; import { - BaseItemDto, - BaseItemDtoQueryResult, + type BaseItemDto, + type BaseItemDtoQueryResult, BaseItemKind, ItemFields, SortOrder, - UserItemDataDto, + type UserItemDataDto, } from "@jellyfin/sdk/lib/generated-client"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api"; import { getPlaylistsApi } from "@jellyfin/sdk/lib/utils/api/playlists-api"; @@ -35,7 +35,7 @@ import { usePlaybackStore, } from "../../utils/store/playback"; -import { SxProps } from "@mui/material"; +import type { SxProps } from "@mui/material"; const PlayButton = ({ item, @@ -253,6 +253,7 @@ const PlayButton = ({ queue, 0, subtitles, + result?.Items[0].MediaSources[0].Id ); navigate("/player"); } diff --git a/src/components/layouts/homeSection/latestMediaSection.jsx b/src/components/layouts/homeSection/latestMediaSection.jsx index 838800d0..39f7078f 100644 --- a/src/components/layouts/homeSection/latestMediaSection.jsx +++ b/src/components/layouts/homeSection/latestMediaSection.jsx @@ -35,7 +35,7 @@ export const LatestMediaSection = ({ latestMediaLib }) => { queryKey: ["homeSection, latestMedia", latestMediaLib], queryFn: () => fetchLatestMedia(latestMediaLib[0]), enabled: !!user.data, - networkMode: "always", + refetchOnMount: true }); if (data.isPending) { return ; diff --git a/src/main.tsx b/src/main.tsx index c2ecd7fc..4a0a05b6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,7 +10,8 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { networkMode: "always", - staleTime: 2 * 60 * 1000, // 2 minutes + staleTime: 1 * 60 * 1000, // 1 minute, + refetchInterval: 10 * 60 * 1000, // 10 minutes, }, mutations: { networkMode: "always", diff --git a/src/routes/home/index.jsx b/src/routes/home/index.jsx index ad79733c..2b140474 100644 --- a/src/routes/home/index.jsx +++ b/src/routes/home/index.jsx @@ -91,6 +91,7 @@ const Home = () => { return resumeItems.data; }, enabled: !!user.data, + refetchOnMount: true }); const resumeItemsAudio = useQuery({ @@ -126,6 +127,7 @@ const Home = () => { return upNext.data; }, enabled: !!user.data, + refetchOnMount: true }); const [latestMediaLibs, setLatestMediaLibs] = useState([]); diff --git a/src/routes/player/videoPlayer.tsx b/src/routes/player/videoPlayer.tsx index ea698377..30ec2799 100644 --- a/src/routes/player/videoPlayer.tsx +++ b/src/routes/player/videoPlayer.tsx @@ -30,7 +30,7 @@ import { endsAt } from "../../utils/date/time"; import { useQuery } from "@tanstack/react-query"; -import { ItemFields, LocationType } from "@jellyfin/sdk/lib/generated-client"; +import { ItemFields, LocationType, PlayMethod, RepeatMode } from "@jellyfin/sdk/lib/generated-client"; import { AnimatePresence, motion } from "framer-motion"; import useDebounce from "../../utils/hooks/useDebounce"; @@ -42,6 +42,7 @@ import JASSUB from "jassub"; import workerUrl from "jassub/dist/jassub-worker.js?url"; import wasmUrl from "jassub/dist/jassub-worker.wasm?url"; +import { getUserApi } from "@jellyfin/sdk/lib/utils/api/user-api"; import subtitleFont from "./Noto-Sans-Indosphere.ttf"; // export const VideoPlayer = () => { @@ -925,9 +926,30 @@ const VideoPlayer = () => { state.enableSubtitle, ]); + const user = useQuery({ + queryKey: ['user'], + queryFn: async () => { + const result = await getUserApi(api).getCurrentUser(); + return result.data; + } + }) + + const mediaInfo = useQuery({ + queryKey: ['videoPlayer', 'mediaInfo'], + queryFn: async () => { + const result = await getMediaInfoApi(api).getPlaybackInfo({ + itemId: item?.Id, + userId: user.data?.Id + }); + return result.data + }, + enabled: user.isSuccess + }) + const [loading, setLoading] = useState(true); const [settingsMenu, setSettingsMenu] = useState(null); const settingsMenuOpen = Boolean(settingsMenu); + const [showVolumeControl, setShowVolumeControl] = useState(false); // Control States const [isReady, setIsReady] = useState(false); @@ -940,6 +962,8 @@ const VideoPlayer = () => { const [selectedSubtitle, setSelectedSubtitle] = useState( mediaSource.subtitleTrack, ); + const [volume, setVolume] = useState(1); + const [muted, setMuted]= useState(false) useEffect(() => setBackdrop("", ""), []); @@ -957,25 +981,74 @@ const VideoPlayer = () => { wasmUrl, availableFonts: { "noto sans": uint8 }, fallbackFont: "Noto Sans", - debug:true }); - window.subInst = subtitleRendererRaw; setSubtitleRenderer(subtitleRendererRaw); player.current.seekTo(ticksToSec(startPosition), "seconds"); setIsReady(true); + // Report Jellyfin server: Playback has begin + await getPlaystateApi(api).reportPlaybackStart({ + playbackStartInfo: { + AudioStreamIndex: mediaSource.audioTrack, + CanSeek: true, + IsMuted: false, + IsPaused: false, + Item: item, + ItemId: item?.Id, + MediaSourceId: mediaSource.id, + PlayMethod: PlayMethod.DirectPlay, + PlaySessionId: mediaInfo.data?.PlaySessionId, + PlaybackStartTimeTicks: startPosition, + PositionTicks: startPosition, + RepeatMode: RepeatMode.RepeatNone, + VolumeLevel: volume, + } + }) + + } }; `` - const handleProgress = (event) => { + const handleProgress = async (event) => { setProgress(secToTicks(event.playedSeconds)); + + // Report Jellyfin server: Playback progress + await getPlaystateApi(api).reportPlaybackProgress({ + playbackProgressInfo: { + AudioStreamIndex: mediaSource.audioTrack, + CanSeek: true, + IsMuted: muted, + IsPaused: !playing, + ItemId: item?.Id, + MediaSourceId: mediaSource.id, + PlayMethod: PlayMethod.DirectPlay, + PlaySessionId: mediaInfo.data?.PlaySessionId, + PlaybackStartTimeTicks: startPosition, + PositionTicks: progress, + RepeatMode: RepeatMode.RepeatNone, + VolumeLevel: volume * 100, + } + }) }; - const handleExitPlayer = () => { + const handleExitPlayer = async () => { appWindow.setFullscreen(false); subtitleRenderer.destroy(); navigate(-1); + // Report Jellyfin server: Playback has ended/stopped + await getPlaystateApi(api).reportPlaybackStopped({ + playbackStopInfo: { + Failed: false, + Item: item, + ItemId: item?.Id, + MediaSourceId: mediaSource.id, + PlayMethod: PlayMethod.DirectPlay, + PlaySessionId: mediaInfo.data?.PlaySessionId, + PlaybackStartTimeTicks: startPosition, + PositionTicks: progress, + } + }) usePlaybackStore.setState(usePlaybackStore.getInitialState()); }; @@ -1232,6 +1305,41 @@ const VideoPlayer = () => {
+ setShowVolumeControl(false)} + > + {muted ? 0 : Math.round(volume*100)} + { + setVolume(newVal); + if (newVal === 0) setMuted(true) + else setMuted(false) + }} /> + + setMuted((state) => !state)} onMouseMoveCapture={() => { + setShowVolumeControl(true) + }}> + {muted ? "volume_off" : + volume < 0.4 ? "volume_down" : + "volume_up"} + setShowSubtitles((state) => !state)}> {showSubtitles @@ -1278,6 +1386,7 @@ const VideoPlayer = () => { // onReady={(playerRef) => { // playerRef.seekTo(ticksToSec(startPosition)); // }} + volume={muted ? 0 : volume} onReady={handleReady} onBuffer={() => setLoading(true)} onBufferEnd={() => setLoading(false)} diff --git a/src/utils/store/playback.ts b/src/utils/store/playback.ts index 1255a8ef..1d699382 100644 --- a/src/utils/store/playback.ts +++ b/src/utils/store/playback.ts @@ -1,4 +1,4 @@ -import { BaseItemDto, MediaStream } from "@jellyfin/sdk/lib/generated-client"; +import type { BaseItemDto, MediaStream } from "@jellyfin/sdk/lib/generated-client"; import { create } from "zustand"; import { setQueue, setTrackIndex } from "./queue"; @@ -112,6 +112,7 @@ type PlaybackStore = { subtitleTrack: number; container: string; availableSubtitleTracks: MediaStream[]; + id: string | undefined; }; enableSubtitle: boolean; hlsStream: string; @@ -130,6 +131,7 @@ export const usePlaybackStore = create(() => ({ subtitleTrack: 0, container: "", availableSubtitleTracks: [], + id: undefined }, enableSubtitle: true, hlsStream: "", @@ -155,6 +157,7 @@ export const playItem = ( queue, queueItemIndex, availableSubtitleTracks, + mediaSourceId ) => { usePlaybackStore.setState({ itemName, @@ -165,6 +168,7 @@ export const playItem = ( subtitleTrack, container, availableSubtitleTracks, + id: mediaSourceId, }, enableSubtitle, hlsStream,