Files
stream.graff.tech-new/client/src/pages/SessionPage.tsx
T

283 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ActionsSidebarWrapper from "../components/ActionsSidebarWrapper";
import ChatFilledIcon from "../components/icons/ChatFilledIcon";
import ExitFilledIcon from "../components/icons/ExitFilledIcon";
import FullscreenExitIcon from "../components/icons/FullscreenExitIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
import MicrophoneFilledIcon from "../components/icons/MicrophoneFilledIcon";
import ShareFilledIcon from "../components/icons/ShareFilledIcon";
import UsersFilledIcon from "../components/icons/UsersFilledIcon";
import VideoOffFilledIcon from "../components/icons/VideoOffFilledIcon";
import FloatingActionButton from "../components/ui/FloatingActionButton";
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
import usePopupStore from "../store/popupStore";
import ControlsPopover from "../components/ui/ControlsPopover";
import ChatPopup from "../components/popups/ChatPopup";
import SharePopup from "../components/popups/SharePopup";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "../lib/api";
import type { Session } from "../types/Session";
import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper";
import WarningIcon from "../components/icons/WarningIcon";
import Button from "../components/ui/Button";
import LoaderIcon from "../components/icons/LoaderIcon";
import SessionUsersPanel from "../components/SessionUsersPanel";
import { useWebRTC } from "../hooks/useWebRTC";
import MicrophoneOffFilledIcon from "../components/icons/MicrophoneOffFilledIcon";
import VideoFilledIcon from "../components/icons/VideoFilledIcon";
import clsx from "clsx";
function SessionPage() {
const { setPopup } = usePopupStore();
const [isFullscreen, setIsFullscreen] = useState(false);
function toggleFullscreen() {
if (document.fullscreenElement) document.exitFullscreen();
else document.documentElement.requestFullscreen();
}
useEffect(() => {
document.addEventListener("fullscreenchange", () =>
setIsFullscreen(!!document.fullscreenElement)
);
return () =>
document.removeEventListener("fullscreenchange", () =>
setIsFullscreen(!!document.fullscreenElement)
);
}, []);
const { id } = useParams();
const navigate = useNavigate();
const {
data: sessionData,
isLoading,
error,
// refetch,
} = useQuery({
queryKey: ["session", id],
queryFn: async () => {
const response = await api.get(`sessions/${id}`).json<{
session: Session;
}>();
return response;
},
refetchInterval: (query) => {
// Автоматически обновляем каждые 2 секунды, если сессия в процессе запуска
const data = query.state.data;
if (
data?.session.status === "starting" ||
data?.session.status === "ending"
) {
return 2000;
}
return false;
},
});
const session = sessionData?.session;
function handleChatOpen() {
console.log("handleChatOpen");
setPopup(<ChatPopup />);
}
function handleParticipantsOpen() {
if (session) {
setPopup(<ParticipantsPopup session={session} />);
}
}
function handleShareOpen() {
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
}
const [mode, setMode] = useState<"full" | "mini">("full");
function toggleMode() {
setMode(mode === "full" ? "mini" : "full");
}
// Не перенаправляем автоматически - пользователи могут продолжать общаться
// useEffect(() => {
// if (session?.status === "ended") {
// const timer = setTimeout(() => {
// navigate("/test");
// }, 5000);
// return () => clearTimeout(timer);
// }
// }, [session?.status, navigate]);
const { localStream, toggleAudio, isAudioMuted, toggleVideo, isVideoMuted } =
useWebRTC(session?.id, true);
if (isLoading) {
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50">
<div className="flex flex-col gap-4 items-center">
<div className="size-12 text-[#7B60F3] animate-spin">
<LoaderIcon />
</div>
<p className="text-gray-600 text-m">
Загрузка информации о сессии...
</p>
</div>
</div>
);
}
if (error || !session) {
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50">
<div className="p-8 w-full max-w-2xl bg-white rounded-lg shadow-md">
<div className="flex gap-4 items-start">
<div className="text-red-500 size-6">
<WarningIcon />
</div>
<div className="flex-1">
<h1 className="mb-2 text-red-900 title-l">Сессия не найдена</h1>
<p className="mb-6 text-gray-600 text-m">
{error instanceof Error
? error.message
: "Не удалось загрузить информацию о сессии"}
</p>
<Button variant="primary" onClick={() => navigate("/test")}>
Вернуться назад
</Button>
</div>
</div>
</div>
</div>
);
}
return (
<div
className={clsx(
mode === "full"
? "flex overflow-hidden relative order-3 w-screen h-dvh bg-black justify-center_items-center touch-none"
: "2xl:px-[5vw] grid 2xl:gap-[0.556vw] gap-2 bg-black relative w-screen h-dvh overflow-hidden"
)}
>
{/* Pixel Streaming - показывается только когда сессия активна */}
{session.status === "started" &&
session.mode === "stream" &&
session.server?.localIp &&
session.playerPort && (
<div className=" absolute w-full h-full aspect-video">
<PixelStreamingWrapper
initialSettings={{
ss: `ws://${session.server.localIp}:${session.playerPort}`,
AutoPlayVideo: true,
AutoConnect: true,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
StreamerId: "DefaultStreamer",
}}
onVideoInitialized={() => {
console.log("Video initialized");
}}
/>
</div>
)}
{/* Сообщение когда Pixel Streaming завершён, но WebRTC чат работает */}
{session.status === "ended" && (
<div className="flex flex-col gap-6 justify-center items-center w-full h-full">
<div className="flex flex-col gap-2 items-center">
<div className="text-2xl font-semibold text-white">
Сессия завершена
</div>
<div className="text-base text-gray-400">
Вы можете продолжать общаться через видеочат
</div>
</div>
<Button variant="primary" onClick={() => navigate("/test")}>
Покинуть сессию
</Button>
</div>
)}
<ActionsSidebarWrapper className="z-[99]">
<FloatingActionButton onClick={toggleMode}>
<div className="2xl:size-[1.111vw] size-4 text-white">
{mode === "mini" ? <FullscreenExitIcon /> : <FullscreenIcon />}
</div>
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:hidden"
onClick={handleChatOpen}
>
<div className="size-[1.111vw] text-white">
<ChatFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:hidden"
onClick={handleParticipantsOpen}
>
<div className="size-[1.111vw] text-white">
<UsersFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:hidden"
onClick={handleShareOpen}
>
<div className="size-[1.111vw] text-white">
<ShareFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton
className="2xl:hidden"
disabled={!localStream}
onClick={toggleAudio}
>
<div className="text-white size-4">
{!localStream || isAudioMuted ? (
<MicrophoneOffFilledIcon />
) : (
<MicrophoneFilledIcon />
)}
</div>
</FloatingActionButton>
<FloatingActionButton
className="2xl:hidden"
disabled={!localStream}
onClick={toggleVideo}
>
<div className="text-white size-4">
{!localStream || isVideoMuted ? (
<VideoOffFilledIcon />
) : (
<VideoFilledIcon />
)}
</div>
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:order-2"
onClick={toggleFullscreen}
>
<div className="2xl:size-[1.111vw] size-4 text-white">
{isFullscreen ? <FullscreenExitIcon /> : <FullscreenIcon />}
</div>
</FloatingActionButton>
<FloatingActionButton variant="critical" className="max-2xl:order-1">
<div className="2xl:size-[1.111vw] size-4 text-white">
<ExitFilledIcon />
</div>
</FloatingActionButton>
<ControlsPopover session={session} />
</ActionsSidebarWrapper>
{/* WebRTC видеочат - работает всегда, пока пользователь на странице */}
<SessionUsersPanel roomId={session.id} autoJoin={true} mode={mode} />
</div>
);
}
export default SessionPage;