Update API URL in .env; refactor routing in main.tsx to switch between NewSessionPage and TestPage; enhance PixelStreamingWrapper with video initialization callback; improve UI components for better interaction and responsiveness in ActionsSidebarWrapper, UserCamera, UserDevicesControls, and ControlsPopover; add session data fetching and error handling in NewSessionPage.

This commit is contained in:
2025-10-16 16:04:59 +05:00
parent 9e19a6e03f
commit 34c9b58d8f
10 changed files with 178 additions and 25 deletions
+1 -1
View File
@@ -1 +1 @@
VITE_API_URL=http://localhost:3000
VITE_API_URL=http://192.168.1.23:3000
@@ -20,7 +20,7 @@ function ActionsSidebarWrapper({
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx(
"flex 2xl:flex 2xl:gap-[0.556vw] 2xl:flex-col gap-2 max-2xl:p-2 max-2xl:rounded-[32px] absolute 2xl:top-1/2 2xl:-translate-y-1/2 2xl:right-[1.111vw] max-2xl:left-1/2 max-2xl:-translate-x-1/2 max-2xl:bottom-2 max-2xl:landscape:bg-[#00000026] max-2xl:landscape:backdrop-blur",
"flex 2xl:flex 2xl:gap-[0.556vw] 2xl:flex-col gap-2 max-2xl:p-2 max-2xl:rounded-[32px] absolute 2xl:top-1/2 2xl:-translate-y-1/2 2xl:right-[1.111vw] max-2xl:left-1/2 max-2xl:-translate-x-1/2 max-2xl:bottom-2 max-2xl:bg-[#00000026] max-2xl:backdrop-blur",
className
)}
>
@@ -10,10 +10,12 @@ import type { AllSettings } from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.7
export interface PixelStreamingWrapperProps {
initialSettings?: Partial<AllSettings>;
onVideoInitialized?: () => void;
}
export const PixelStreamingWrapper = ({
initialSettings,
onVideoInitialized,
}: PixelStreamingWrapperProps) => {
// A reference to parent div element that the Pixel Streaming library attaches into:
const videoParent = useRef<HTMLDivElement>(null);
@@ -38,6 +40,10 @@ export const PixelStreamingWrapper = ({
setClickToPlayVisible(true);
});
streaming.addEventListener("videoInitialized", () => {
onVideoInitialized?.();
});
// Save the library instance into component state so that it can be accessed later:
setPixelStreaming(streaming);
+3 -2
View File
@@ -13,6 +13,7 @@ import ChatPopup from "../popups/ChatPopup";
import ParticipantsPopup from "../popups/ParticipantsPopup";
import SharePopup from "../popups/SharePopup";
import SettingsModal from "../modals/SettingsModal";
import clsx from "clsx";
function ControlsPopover() {
const [isOpened, setIsOpened] = useState(false);
@@ -41,10 +42,10 @@ function ControlsPopover() {
const { setModal } = useModalStore();
return (
<div className="2xl:hidden order-3 relative ah-full">
<div className="2xl:hidden order-3 relative">
<FloatingActionButton
ref={buttonRef}
className="!bg-[#7B60F3]"
className={clsx(isOpened && "!bg-[#7B60F3]")}
onClick={() => setIsOpened(!isOpened)}
>
<div className="size-4 text-white">
+1 -1
View File
@@ -53,7 +53,7 @@ export default function UserCamera({
: "0.139vw solid #FFFFFF4D",
}}
className={clsx(
"aspect-square rounded-[1.667vw] bg-yellow-500 relative flex-shrink-0",
"aspect-square rounded-[1.667vw] bg-yellow-500 relative flex-shrink-0 pointer-events-auto",
isAdmin && "order-last"
)}
>
@@ -8,6 +8,7 @@ import SettingsModal from "../modals/SettingsModal";
export default function UserDevicesControls() {
const { setModal } = useModalStore();
function ToggleAudioDevice() {
console.log("Mute device");
}
@@ -22,7 +23,7 @@ export default function UserDevicesControls() {
}
return (
<div className="hidden 2xl:flex aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px]">
<div className="hidden 2xl:flex aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto">
<ControlButton
size="large"
icon={<MicrophoneFilledIcon />}
+5 -4
View File
@@ -2,16 +2,17 @@ import { createRoot } from "react-dom/client";
import "./index.css";
import HomePage from "./pages/HomePage";
import { createBrowserRouter, RouterProvider } from "react-router";
import SessionPage from "./pages/SessionPage";
// import SessionPage from "./pages/SessionPage";
import LoginPage from "./pages/LoginPage";
import RegisterPage from "./pages/RegisterPage";
// import TestPage from "./pages/TestPage";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/queryClient";
import ProtectedRoute from "./components/ProtectedRoute";
import PublicRoute from "./components/PublicRoute";
import ModalContainer from "./components/ModalContainer";
import PopupContainer from "./components/PopupContainer";
// import NewSessionPage from "./pages/NewSessionPage";
import TestPage from "./pages/TestPage";
import NewSessionPage from "./pages/NewSessionPage";
const router = createBrowserRouter([
@@ -41,11 +42,11 @@ const router = createBrowserRouter([
},
{
path: "/test",
element: <NewSessionPage />,
element: <TestPage />,
},
{
path: "/sessions/:id",
element: <SessionPage />,
element: <NewSessionPage />,
},
]);
+117 -14
View File
@@ -1,6 +1,5 @@
import ActionsSidebarWrapper from "../components/ActionsSidebarWrapper";
import ChatFilledIcon from "../components/icons/ChatFilledIcon";
import CogFilledIcon from "../components/icons/CogFilledIcon";
import ExitFilledIcon from "../components/icons/ExitFilledIcon";
import FullscreenExitIcon from "../components/icons/FullscreenExitIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
@@ -14,13 +13,19 @@ import usePopupStore from "../store/popupStore";
import ControlsPopover from "../components/ui/ControlsPopover";
import ChatPopup from "../components/popups/ChatPopup";
import SharePopup from "../components/popups/SharePopup";
import SettingsModal from "../components/modals/SettingsModal";
import useModalStore from "../store/modalStore";
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";
function NewSessionPage() {
const { setPopup } = usePopupStore();
const { setModal } = useModalStore();
const [isFullscreen, setIsFullscreen] = useState(false);
@@ -39,8 +44,109 @@ function NewSessionPage() {
);
}, []);
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;
// Перенаправление на тестовую страницу при завершении сессии
useEffect(() => {
if (session?.status === "ended") {
const timer = setTimeout(() => {
navigate("/test");
}, 5000);
return () => clearTimeout(timer);
}
}, [session?.status, navigate]);
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="relative w-screen h-screen bg-[#DADADA] order-3 overflow-hidden">
<div className="relative w-screen h-screen bg-black order-3 overflow-hidden flex justify-center items-center">
{session.status === "started" &&
session.mode === "stream" &&
session.server?.localIp &&
session.playerPort && (
<div className="aspect-video w-full h-full">
<PixelStreamingWrapper
initialSettings={{
ss: `ws://${session.server.localIp}:${session.playerPort}`,
AutoPlayVideo: true,
AutoConnect: true,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
onVideoInitialized={() => {
console.log("Video initialized");
}}
/>
</div>
)}
<ActionsSidebarWrapper>
<FloatingActionButton
className="max-2xl:hidden"
@@ -61,21 +167,15 @@ function NewSessionPage() {
<FloatingActionButton
className="max-2xl:hidden"
onClick={() =>
setPopup(<SharePopup link="https://estate.stream/ahdy12jdco1" />)
setPopup(
<SharePopup link={`${window.location.origin}/sessions/${id}`} />
)
}
>
<div className="size-[1.111vw] text-white">
<ShareFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:hidden"
onClick={() => setModal(<SettingsModal />)}
>
<div className="size-[1.111vw] text-white">
<CogFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="2xl:hidden">
<div className="size-4 text-white">
<MicrophoneFilledIcon />
@@ -101,6 +201,9 @@ function NewSessionPage() {
</FloatingActionButton>
<ControlsPopover />
</ActionsSidebarWrapper>
<div className="absolute 2xl:!bottom-[1.111vw] 2xl:!right-[1.111vw] max-2xl:top-2 max-2xl:right-2 pointer-events-none">
<SessionUsersPanel />
</div>
</div>
);
}
+3 -1
View File
@@ -164,7 +164,9 @@ function SessionPage() {
HoveringMouse: true,
WaitForStreamer: true,
}}
// onVideoInitialized={() => setIsVideoInitialized(true)}
onVideoInitialized={() => {
console.log("Video initialized");
}}
/>
</div>
)}
+39
View File
@@ -0,0 +1,39 @@
export interface Session {
id: string;
appId: string;
userId: string | null;
mode: "stream" | "local";
status: "starting" | "started" | "ending" | "ended";
tier: "demo" | "prod" | null;
serverId: string | null;
appPid: number | null;
cirrusPid: number | null;
streamerPort: number | null;
playerPort: number | null;
sfuPort: number | null;
startAt: string;
endAt: string | null;
createdAt: string;
updatedAt: string;
app?: {
id: string;
name: string;
title: string;
gpuLimitMb: number | null;
psVersion: number | null;
};
server?: {
id: string;
localIp: string;
hostname: string;
type: "stream" | "local";
tier: "demo" | "prod" | null;
location: "ru1" | "uae1" | null;
} | null;
user?: {
id: string;
email: string;
role: string;
displayName: string;
} | null;
}