This commit is contained in:
2024-03-13 18:28:13 +05:00
parent 8f43e95057
commit 63fea5da4b
23 changed files with 5811 additions and 942 deletions
+324 -349
View File
@@ -2,97 +2,123 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import "./StreamPage2.css";
import ky from "ky";
import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper";
import { useParams } from "react-router-dom";
import { FormEvent, useEffect, useRef, useState } from "react";
import { Transition } from "react-transition-group";
import Button from "../components/ui/Button";
import CloseIcon from "../components/icons/CloseIcon";
// import CloseIcon from "../components/icons/CloseIcon";
import HandOffIcon from "../components/icons/HandOffIcon";
import MicroOffIcon from "../components/icons/MicroOffIcon";
import PersonsIcon from "../components/icons/PersonsIcon";
import MicroOnIcon from "../components/icons/MicroOnIcon";
import MoreIcon from "../components/icons/MoreIcon";
// import MoreIcon from "../components/icons/MoreIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
import WindowIcon from "../components/icons/WindowIcon";
import ChatIcon from "../components/icons/ChatIcon";
import ShareIcon from "../components/icons/ShareIcon";
import GearIcon from "../components/icons/GearIcon";
import { useFullscreen } from "ahooks";
import AttachIcon from "../components/icons/AttachIcon";
import DetachIcon from "../components/icons/DetachIcon";
import Draggable from "react-draggable";
import api from "../utils/api";
import { Socket, io } from "socket.io-client";
import { v4 as uuidv4 } from "uuid";
import ChatNew from "../components/ChatNew";
interface User {
id: string;
username: string;
name: string;
}
function StreamPage2() {
const params = useParams();
const [socket, setSocket] = useState<Socket>();
const [wsUrl, setWsUrl] = useState<string>();
const [username, setUsername] = useState<string>("");
const [isEnded, setIsEnded] = useState<boolean>();
const [name, setName] = useState<string>("");
const [userId] = useState(uuidv4());
const [step, setStep] = useState<number>(1);
const usernameRef = useRef<HTMLInputElement>(null!);
const [users, setUsers] = useState<User[]>([
{
id: "1",
username: "User 1",
},
{
id: "2",
username: "User 2",
},
{
id: "3",
username: "User 3",
},
{
id: "4",
username: "User 3",
},
{
id: "1",
username: "User 1",
},
{
id: "2",
username: "User 2",
},
]);
const nameRef = useRef<HTMLInputElement>(null!);
const [users] = useState<User[]>([]);
const ref = useRef(null);
const [isFullscreen, { toggleFullscreen }] = useFullscreen(ref);
const [isShowUsers, setIsShowUsers] = useState<boolean>(false);
const [isShowChat, setIsShowChat] = useState<boolean>(false);
const [isMicEnabled, setIsMicEnabled] = useState<boolean>(false);
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
const ref = useRef(null);
const [isFullscreen, { toggleFullscreen }] = useFullscreen(ref);
const [isUsersDetached, setIsUsersDetached] = useState<boolean>(false);
const [isChatDetached, setIsChatDetached] = useState<boolean>(false);
async function getWsUrl() {
const activeSession: any = await ky
.get(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`)
useEffect(() => {
console.log(isShowChat);
}, [isShowChat]);
async function getActiveSession() {
const activeSession: any = await api
.get(`activeSessions/${params.id}`)
.json();
return activeSession;
}
async function checkSessionStatus() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
setTimeout(async () => {
await checkSessionStatus();
}, 1000);
}
async function getWsUrl() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
setIsEnded(false);
setWsUrl(
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.server}/${activeSession.cirrusPort}/`
);
checkSessionStatus();
}
function handleSetUsername(e: FormEvent) {
function handleSetName(e: FormEvent) {
e.preventDefault();
if (!username) {
usernameRef.current.focus();
if (!name) {
nameRef.current.focus();
return;
}
setSocket(
io(import.meta.env.VITE_API_URL, {
auth: {
roomId: params.id,
user: { id: userId, name: name, isAdmin: false },
},
})
);
setStep(2);
}
function setUsernameGuest() {
setUsername("Гость");
function setNameGuest() {
setName("Гость");
setSocket(
io(import.meta.env.VITE_API_URL, {
auth: {
roomId: params.id,
user: { id: userId, name: "Гость" },
},
})
);
setStep(2);
}
@@ -105,138 +131,110 @@ function StreamPage2() {
}
useEffect(() => {
if (!username) return;
if (!name) return;
setUsername(() => username.trim());
}, [username]);
useEffect(() => {
if (isChatDetached || isUsersDetached) {
document.body.classList.add("overflow-hidden");
} else {
document.body.classList.remove("overflow-hidden");
}
}, [isUsersDetached, isChatDetached]);
setName(() => name.trim());
}, [name]);
useEffect(() => {
getWsUrl();
}, []);
return (
<div ref={ref} className="">
<div className="h-screen flex flex-col">
<div className="h-12 bg-white px-6 py-3 flex items-center justify-between">
<div id="left" className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{username && username[0].toUpperCase()}
if (isEnded !== undefined) {
if (!isEnded) {
return (
<div ref={ref} className="h-screen flex flex-col">
<div className="h-12 bg-white px-6 py-3 flex items-center justify-between">
<div id="left" className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{name && name[0].toUpperCase()}
</div>
<p className="text-xs">{name}</p>
</div>
<p className="text-xs">{username}</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" icon={<HandOffIcon />} onlyIcon />
<Button
variant="secondary"
icon={isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={() => setIsMicEnabled((prev) => !prev)}
/>
</div>
<div id="divider" className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex gap-6">
{users.map(
(user, index) =>
index < 3 && (
<div key={user.id} className="flex items-center gap-2">
<div className="h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{user.username && user.username[0].toUpperCase()}
</div>
<p className="text-xs">{user.username}</p>
</div>
)
)}
{users.length > 3 && (
<div className="flex gap-2">
<Button variant="secondary" icon={<HandOffIcon />} onlyIcon />
<Button
variant="secondary"
icon={<PersonsIcon />}
icon={isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={() => setIsShowUsers((prev) => !prev)}
onClick={() => setIsMicEnabled((prev) => !prev)}
/>
</div>
<div id="divider" className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex gap-6">
{users.map(
(user, index) =>
index < 3 && (
<div key={user.id} className="flex items-center gap-2">
<div className="h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{user.name && user.name[0].toUpperCase()}
</div>
<p className="text-xs">{user.name}</p>
</div>
)
)}
{users.length > 3 && (
<Button
variant="secondary"
icon={<PersonsIcon />}
onlyIcon
onClick={() => setIsShowUsers((prev) => !prev)}
/>
)}
</div>
</div>
<div id="right" className="flex items-center gap-4">
<div>
<Button
variant="secondary"
icon={<ChatIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
/>
</div>
<div id="divider" className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex gap-2">
<Button
variant="secondary"
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
onlyIcon
onClick={toggleFullscreen}
/>
<Button variant="secondary" icon={<ShareIcon />} onlyIcon />
<Button variant="secondary" icon={<GearIcon />} onlyIcon />
</div>
</div>
</div>
<div className="flex-1 flex">
<div className="w-full">
{wsUrl && (
<PixelStreamingWrapper
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: wsUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
onVideoInitialized={() => setIsVideoInitialized(true)}
/>
)}
</div>
</div>
<div id="right" className="flex items-center gap-4">
<div>
<Button
variant="secondary"
icon={<ChatIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
/>
</div>
<div id="divider" className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex gap-2">
<Button
variant="secondary"
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
onlyIcon
onClick={toggleFullscreen}
/>
<Button variant="secondary" icon={<ShareIcon />} onlyIcon />
<Button variant="secondary" icon={<GearIcon />} onlyIcon />
</div>
</div>
</div>
<div className="flex-1 flex">
<div className="w-full">
{wsUrl && (
<PixelStreamingWrapper
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: wsUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
onVideoInitialized={() => setIsVideoInitialized(true)}
/>
)}
</div>
<div className="flex flex-col bg-white max-h-[calc(100vh-48px)]">
<Draggable
position={!isUsersDetached ? { x: 0, y: 0 } : undefined}
disabled={!isUsersDetached}
>
<div
id="modal-users"
className={`max-h-[calc(100vh-48px)] ${
isUsersDetached ? "absolute right-0 p-4" : ""
}`}
>
<div className="flex flex-col bg-white max-h-[calc(100vh-48px)]">
{/* <div id="modal-users" className="max-h-[calc(100vh-48px)]">
<div
className={`${
isUsersDetached ? "rounded-lg" : "h-full"
} bg-white flex flex-col ${
className={`bg-white flex flex-col h-full ${
isShowUsers ? "w-[296px]" : "w-0 hidden"
}`}
>
<div
className={`flex items-center justify-between p-2 pl-4 ${
isUsersDetached ? "border-b" : ""
}`}
>
<div className="flex items-center justify-between p-2 pl-4">
<p className="font-semibold">Участники</p>
<div className="flex">
<Button
variant="tertiary"
icon={isUsersDetached ? <AttachIcon /> : <DetachIcon />}
onlyIcon
onClick={() => setIsUsersDetached((prev) => !prev)}
/>
<Button
variant="tertiary"
icon={<CloseIcon />}
@@ -246,15 +244,15 @@ function StreamPage2() {
</div>
</div>
<div className="flex flex-col gap-2 p-4">
{users.map((user) => (
<div className="flex justify-between">
{users.map((user, index) => (
<div key={index} className="flex justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<p className="font-medium text-[10px] select-none">
{user.username[0].toUpperCase()}
{user.name[0].toUpperCase()}
</p>
</div>
<p className="text-sm">{user.username}</p>
<p className="text-sm">{user.name}</p>
</div>
<div className="">
<Button
@@ -267,202 +265,179 @@ function StreamPage2() {
))}
</div>
</div>
</div>
</Draggable>
</div> */}
<Draggable
position={!isChatDetached ? { x: 0, y: 0 } : undefined}
disabled={!isChatDetached}
>
<div
id="modal-chat"
className={`max-h-[calc(100vh-48px)] ${
isChatDetached
? "absolute right-[346px] p-4 h-fit"
: "h-full"
}`}
>
<div
className={`${
isChatDetached ? "rounded-lg" : "h-full"
} bg-white flex flex-col ${
isShowChat ? "w-[296px]" : "w-0 hidden"
}`}
>
<div
className={`flex items-center justify-between p-2 pl-4 ${
isChatDetached ? "border-b" : ""
}`}
>
<p className="font-semibold">Чат</p>
<div className="flex">
<Button
variant="tertiary"
icon={isChatDetached ? <AttachIcon /> : <DetachIcon />}
onlyIcon
onClick={() => setIsChatDetached((prev) => !prev)}
/>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={() => setIsShowChat(false)}
/>
</div>
</div>
<div className="flex flex-col gap-2 p-4">//</div>
</div>
</div>
</Draggable>
<ChatNew
isShow={isShowChat}
userId={userId}
name={name}
socket={socket!}
onClose={() => setIsShowChat(false)}
/>
</div>
</div>
</div>
</div>
<Transition
in={step !== 3 || !isVideoInitialized}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<div
className={`absolute top-0 left-0 w-full h-full flex items-center justify-center bg-[#14161F] transition-all ${state} ${
step !== 3 || !isVideoInitialized
? "bg-opacity-50 backdrop-blur-2xl"
: "bg-opacity-0 backdrop-blur-none"
} `}
<Transition
in={step !== 3 || !isVideoInitialized}
timeout={300}
mountOnEnter
unmountOnExit
>
<Transition
in={step === 1}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<form
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
onSubmit={handleSetUsername}
{(state) => (
<div
className={`absolute top-0 left-0 w-full h-full flex items-center justify-center bg-[#14161F] transition-all ${state} ${
step !== 3 || !isVideoInitialized
? "bg-opacity-50 backdrop-blur-2xl"
: "bg-opacity-0 backdrop-blur-none"
} `}
>
<Transition
in={step === 1}
timeout={300}
mountOnEnter
unmountOnExit
>
<div className="mb-6">
<p className="text-2xl font-semibold">Здравствуйте!</p>
</div>
<div className="mb-6 flex flex-col gap-2">
<p className="font-semibold">Представьтесь, пожалуйста</p>
<p className="text-[#77828C] text-sm">
Так мы будем знать, как к вам обратиться
</p>
</div>
<div className="mb-10">
<div className="flex flex-col gap-1">
<label className="text-[#77828C] text-xs">Имя</label>
<input
ref={usernameRef}
type="text"
autoComplete="off"
autoFocus
className="rounded-lg px-2 py-2.5 bg-white border border-[#DAE0E5] hover:border-[#49A1F5] focus:border-[#49A1F5] outline-none transition-colors text-sm"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
</div>
<div className="flex gap-2">
<Button
type="button"
variant="secondary"
fullWidth
large
onClick={setUsernameGuest}
{(state) => (
<form
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
onSubmit={handleSetName}
>
Не указывать
</Button>
<Button
type="submit"
fullWidth
large
onClick={void handleSetUsername}
>
Продолжить
</Button>
</div>
</form>
)}
</Transition>
<div className="mb-6">
<p className="text-2xl font-semibold">Здравствуйте!</p>
</div>
<div className="mb-6 flex flex-col gap-2">
<p className="font-semibold">
Представьтесь, пожалуйста
</p>
<p className="text-[#77828C] text-sm">
Так мы будем знать, как к вам обратиться
</p>
</div>
<div className="mb-10">
<div className="flex flex-col gap-1">
<label className="text-[#77828C] text-xs">Имя</label>
<input
ref={nameRef}
type="text"
autoComplete="off"
autoFocus
className="rounded-lg px-2 py-2.5 bg-white border border-[#DAE0E5] hover:border-[#49A1F5] focus:border-[#49A1F5] outline-none transition-colors text-sm"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
<div className="flex gap-2">
<Button
type="button"
variant="secondary"
fullWidth
large
onClick={setNameGuest}
>
Не указывать
</Button>
<Button
type="submit"
fullWidth
large
onClick={void handleSetName}
>
Продолжить
</Button>
</div>
</form>
)}
</Transition>
<Transition
in={step === 2}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<div
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
<Transition
in={step === 2}
timeout={300}
mountOnEnter
unmountOnExit
>
<div className="mb-6">
<p className="text-2xl font-semibold">
Хотите принять участие в обсуждении?
</p>
</div>
<div className="mb-6 flex gap-6">
<div className="border border-[#DAE0E5] text-[#49A1F5] p-2 rounded-full">
<MicroOnIcon />
</div>
<p className="text-sm">Разрешите использование микрофона</p>
</div>
<div className="mb-10">
<p className="text-[#77828C] text-xs">
Выключить микрофон можно в любой момент
</p>
</div>
<div className="flex gap-2">
<Button
variant="secondary"
fullWidth
large
onClick={disallowMic}
{(state) => (
<div
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
>
Запретить
</Button>
<Button fullWidth large onClick={allowMic}>
Разрешить
</Button>
</div>
</div>
)}
</Transition>
<div className="mb-6">
<p className="text-2xl font-semibold">
Хотите принять участие в обсуждении?
</p>
</div>
<div className="mb-6 flex gap-6">
<div className="border border-[#DAE0E5] text-[#49A1F5] p-2 rounded-full">
<MicroOnIcon />
</div>
<p className="text-sm">
Разрешите использование микрофона
</p>
</div>
<div className="mb-10">
<p className="text-[#77828C] text-xs">
Выключить микрофон можно в любой момент
</p>
</div>
<div className="flex gap-2">
<Button
variant="secondary"
fullWidth
large
onClick={disallowMic}
>
Запретить
</Button>
<Button fullWidth large onClick={allowMic}>
Разрешить
</Button>
</div>
</div>
)}
</Transition>
<Transition
in={step === 3 && !isVideoInitialized}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<div
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
<Transition
in={step === 3 && !isVideoInitialized}
timeout={300}
mountOnEnter
unmountOnExit
>
<div className="mb-6">
<p className="text-2xl font-semibold">
Пожалуйста, подождите
</p>
</div>
<div className="flex items-center gap-2">
<img
src="/icons/LoaderPrimary.png"
alt=""
className="w-8 h-8 animate-spin"
/>
<p className="font-semibold text-[#49A1F5]">Подключение</p>
</div>
</div>
)}
</Transition>
</div>
)}
</Transition>
</div>
);
{(state) => (
<div
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
>
<div className="mb-6">
<p className="text-2xl font-semibold">
Пожалуйста, подождите
</p>
</div>
<div className="flex items-center gap-2">
<img
src="/icons/LoaderPrimary.png"
alt=""
className="w-8 h-8 animate-spin"
/>
<p className="font-semibold text-[#49A1F5]">
Подключение
</p>
</div>
</div>
)}
</Transition>
</div>
)}
</Transition>
</div>
);
} else {
return (
<div className="h-screen flex items-center justify-center p-4">
<p className="text-white text-2xl font-gilroy">
Трансляция была завершена
</p>
</div>
);
}
}
}
export default StreamPage2;