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:
+1
-1
@@ -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);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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 />,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,7 +164,9 @@ function SessionPage() {
|
||||
HoveringMouse: true,
|
||||
WaitForStreamer: true,
|
||||
}}
|
||||
// onVideoInitialized={() => setIsVideoInitialized(true)}
|
||||
onVideoInitialized={() => {
|
||||
console.log("Video initialized");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user