Skip to content

Commit

Permalink
feat(player): sync playback with jellyfin server
Browse files Browse the repository at this point in the history
  • Loading branch information
prayag17 committed Mar 17, 2024
1 parent 39e4a1e commit b0a32c6
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 14 deletions.
13 changes: 7 additions & 6 deletions src/components/buttons/playButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -35,7 +35,7 @@ import {
usePlaybackStore,
} from "../../utils/store/playback";

import { SxProps } from "@mui/material";
import type { SxProps } from "@mui/material";

const PlayButton = ({
item,
Expand Down Expand Up @@ -253,6 +253,7 @@ const PlayButton = ({
queue,
0,
subtitles,
result?.Items[0].MediaSources[0].Id
);
navigate("/player");
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/layouts/homeSection/latestMediaSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CardsSkeleton />;
Expand Down
3 changes: 2 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/routes/home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const Home = () => {
return resumeItems.data;
},
enabled: !!user.data,
refetchOnMount: true
});

const resumeItemsAudio = useQuery({
Expand Down Expand Up @@ -126,6 +127,7 @@ const Home = () => {
return upNext.data;
},
enabled: !!user.data,
refetchOnMount: true
});

const [latestMediaLibs, setLatestMediaLibs] = useState([]);
Expand Down
119 changes: 114 additions & 5 deletions src/routes/player/videoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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);
Expand All @@ -940,6 +962,8 @@ const VideoPlayer = () => {
const [selectedSubtitle, setSelectedSubtitle] = useState(
mediaSource.subtitleTrack,
);
const [volume, setVolume] = useState(1);
const [muted, setMuted]= useState(false)

useEffect(() => setBackdrop("", ""), []);

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

Expand Down Expand Up @@ -1232,6 +1305,41 @@ const VideoPlayer = () => {
</Typography>
</div>
<div className="video-player-osd-controls-buttons">
<motion.div style={{
width: "13em",
padding: "0.5em 1.5em",
paddingLeft: "0.8em",
gap:"0.4em",
background: "black",
borderRadius: "100px",
display: 'grid',
justifyContent: "center",
alignItems: "center",
gridTemplateColumns: "2em 1fr",
opacity:0,
}}
animate={{
opacity: showVolumeControl ? 1:0
}}
whileHover={{
opacity:1
}}
onMouseLeave={()=> setShowVolumeControl(false)}
>
<Typography textAlign="center">{muted ? 0 : Math.round(volume*100)}</Typography>
<Slider step={0.01} max={1} size="small" value={muted ? 0 : volume} onChange={(e, newVal) => {
setVolume(newVal);
if (newVal === 0) setMuted(true)
else setMuted(false)
}} />
</motion.div>
<IconButton onClick={() => setMuted((state) => !state)} onMouseMoveCapture={() => {
setShowVolumeControl(true)
}}>
<span className="material-symbols-rounded">{muted ? "volume_off" :
volume < 0.4 ? "volume_down" :
"volume_up"}</span>
</IconButton>
<IconButton onClick={() => setShowSubtitles((state) => !state)}>
<span className={"material-symbols-rounded"}>
{showSubtitles
Expand Down Expand Up @@ -1278,6 +1386,7 @@ const VideoPlayer = () => {
// onReady={(playerRef) => {
// playerRef.seekTo(ticksToSec(startPosition));
// }}
volume={muted ? 0 : volume}
onReady={handleReady}
onBuffer={() => setLoading(true)}
onBufferEnd={() => setLoading(false)}
Expand Down
6 changes: 5 additions & 1 deletion src/utils/store/playback.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -112,6 +112,7 @@ type PlaybackStore = {
subtitleTrack: number;
container: string;
availableSubtitleTracks: MediaStream[];
id: string | undefined;
};
enableSubtitle: boolean;
hlsStream: string;
Expand All @@ -130,6 +131,7 @@ export const usePlaybackStore = create<PlaybackStore>(() => ({
subtitleTrack: 0,
container: "",
availableSubtitleTracks: [],
id: undefined
},
enableSubtitle: true,
hlsStream: "",
Expand All @@ -155,6 +157,7 @@ export const playItem = (
queue,
queueItemIndex,
availableSubtitleTracks,
mediaSourceId
) => {
usePlaybackStore.setState({
itemName,
Expand All @@ -165,6 +168,7 @@ export const playItem = (
subtitleTrack,
container,
availableSubtitleTracks,
id: mediaSourceId,
},
enableSubtitle,
hlsStream,
Expand Down

0 comments on commit b0a32c6

Please sign in to comment.