337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
/* eslint-disable no-irregular-whitespace */
|
||
/* eslint-disable react-hooks/exhaustive-deps */
|
||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
import { useParams } from "react-router-dom";
|
||
import { FullScreen, useFullScreenHandle } from "react-full-screen";
|
||
import { useEffect, useState } from "react";
|
||
import ky from "ky";
|
||
import { Socket, io } from "socket.io-client";
|
||
import userAgentParser from "ua-parser-js";
|
||
import { Player } from "./components/Player";
|
||
import { Trans, useTranslation } from "react-i18next";
|
||
import ShareIcon from "./components/icons/ShareIcon";
|
||
import ModalContainer from "./components/ModalContainer";
|
||
import useModalStore from "./stores/useModalStore";
|
||
import ShareModal from "./components/modals/ShareModal";
|
||
import { Transition } from "react-transition-group";
|
||
import FullscreenIcon from "./components/icons/FullscreenIcon";
|
||
import WindowedModeIcon from "./components/icons/WindowedModeIcon";
|
||
import QRIcon from "./components/icons/QRIcon";
|
||
import QRCodeModal from "./components/modals/QRCodeModal";
|
||
import PersonsIcon from "./components/icons/PersonsIcon";
|
||
import UsersManagementModal from "./components/modals/UsersManagementModal";
|
||
import useStreamUserStore from "./stores/useStreamUserStore";
|
||
import { ToastContainer, toast } from "react-toastify";
|
||
import "react-toastify/dist/ReactToastify.css";
|
||
// import UserIcon from "./components/icons/UserIcon";
|
||
// import HandOnIcon from "./components/icons/HandOnIcon";
|
||
import AlertIcon from "./components/icons/AlertIcon";
|
||
import useSocketStore from "./stores/useSocketStore";
|
||
import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
|
||
import ToggleMic from "./components/ToggleMic";
|
||
|
||
function StreamPage() {
|
||
const { t } = useTranslation();
|
||
const handleFullScreen = useFullScreenHandle();
|
||
const [streamUrl, setStreamUrl] = useState<string>("");
|
||
const [isStreamEnded, setIsStreamEnded] = useState<boolean>(false);
|
||
const [isStreamLoaded, setStreamLoaded] = useState<boolean>(false);
|
||
const [me, setMe] = useState<any>({});
|
||
const [modal, setModal] = useModalStore((state) => [
|
||
state.modal,
|
||
state.setModal,
|
||
]);
|
||
const params = useParams();
|
||
const roomId = params.id;
|
||
const serverUrl = "wss://livekit.stream.graff.tech";
|
||
const [token, setToken] = useState<string>();
|
||
const [socket, setSocket] = useSocketStore((state) => [
|
||
state.socket,
|
||
state.setSocket,
|
||
]);
|
||
const [users, setUsers] = useStreamUserStore((state) => [
|
||
state.users,
|
||
state.setUsers,
|
||
]);
|
||
|
||
async function getToken() {
|
||
if (!socket) return;
|
||
|
||
const { token }: any = await ky
|
||
.get(
|
||
`https://coord.graff.tech/getToken?roomName=${roomId}&participantName=${socket.id}`
|
||
)
|
||
.json();
|
||
|
||
setToken(token);
|
||
}
|
||
|
||
async function connect() {
|
||
const activeSession: any = await ky
|
||
.get(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`)
|
||
.json();
|
||
|
||
if (activeSession) {
|
||
setStreamUrl(
|
||
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.server}/${activeSession.cirrusPort}/`
|
||
);
|
||
|
||
setStreamLoaded(true);
|
||
} else {
|
||
setIsStreamEnded(true);
|
||
}
|
||
}
|
||
|
||
function update(socketId: string, data: { [key: string]: any }) {
|
||
if (!socket) return;
|
||
|
||
socket.emit("update", socketId, data);
|
||
console.log("Users: ", users);
|
||
}
|
||
|
||
function kick(socketId: string) {
|
||
if (!socket) return;
|
||
|
||
socket.emit("kick", socketId);
|
||
console.log("User kick: ", socketId);
|
||
}
|
||
|
||
function toastWarn(text: string) {
|
||
toast.warn(text, {
|
||
position: "top-center",
|
||
autoClose: 2000,
|
||
hideProgressBar: true,
|
||
closeOnClick: true,
|
||
pauseOnHover: true,
|
||
draggable: true,
|
||
theme: "dark",
|
||
icon: <AlertIcon />,
|
||
closeButton: false,
|
||
});
|
||
}
|
||
|
||
// function toastUser(text: string) {
|
||
// toast.info(text, {
|
||
// position: "top-center",
|
||
// autoClose: 2000,
|
||
// hideProgressBar: true,
|
||
// closeOnClick: true,
|
||
// pauseOnHover: true,
|
||
// draggable: true,
|
||
// theme: "dark",
|
||
// icon: <UserIcon />,
|
||
// closeButton: false,
|
||
// });
|
||
// }
|
||
|
||
// function toastHandOn(text: string) {
|
||
// toast.info(text, {
|
||
// position: "top-center",
|
||
// autoClose: 2000,
|
||
// hideProgressBar: true,
|
||
// closeOnClick: true,
|
||
// pauseOnHover: true,
|
||
// draggable: true,
|
||
// theme: "dark",
|
||
// icon: <HandOnIcon />,
|
||
// closeButton: false,
|
||
// });
|
||
// }
|
||
|
||
useEffect(() => {
|
||
connect();
|
||
|
||
const socket: Socket = io(import.meta.env.VITE_COORD_URL, {
|
||
query: {
|
||
roomId,
|
||
},
|
||
});
|
||
|
||
socket.on("connect", () => {
|
||
console.log("Socket: ", socket.id);
|
||
setSocket(socket);
|
||
});
|
||
|
||
socket.on("join", (socketId, sockets) => {
|
||
console.log("User connected: ", socketId, sockets);
|
||
setUsers(sockets);
|
||
});
|
||
|
||
socket.on("update", (socketId, data, sockets) => {
|
||
console.log("Update users: ", socketId, data, sockets);
|
||
setUsers(sockets);
|
||
});
|
||
|
||
socket.on("leave", (socketId, sockets) => {
|
||
console.log("User disconnected: ", socketId);
|
||
setUsers(sockets);
|
||
});
|
||
|
||
socket.on("kick", () => {
|
||
window.close();
|
||
});
|
||
|
||
socket.on("leave", (socketId) => {
|
||
setUsers(
|
||
useStreamUserStore
|
||
.getState()
|
||
.users.filter((user: any) => user.id !== socketId)
|
||
);
|
||
});
|
||
|
||
return () => {
|
||
socket.close();
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!socket) return;
|
||
|
||
getToken();
|
||
}, [socket]);
|
||
|
||
useEffect(() => {
|
||
if (socket) {
|
||
setMe(users.find((user: any) => user.id === socket.id));
|
||
}
|
||
}, [users]);
|
||
|
||
useEffect(() => {
|
||
if (me && me.allowControl && !me.admin) {
|
||
// toastHandOn(t("notification.controlReceived"));
|
||
}
|
||
}, [me]);
|
||
|
||
return (
|
||
<FullScreen handle={handleFullScreen} className="h-screen text-[#F2F2F2]">
|
||
{!isStreamEnded ? (
|
||
isStreamLoaded ? (
|
||
<>{streamUrl && <Player ss={streamUrl} />}</>
|
||
) : (
|
||
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center">
|
||
<Trans i18nKey="streamWaiting">Ожидание потока</Trans>
|
||
</div>
|
||
)
|
||
) : (
|
||
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center">
|
||
<p className="text-2xl font-gilroy">
|
||
<Trans i18nKey="streamEnded">Трансляция была завершена</Trans>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{me && !me.allowControl && (
|
||
<>
|
||
{new userAgentParser(me.ua).getDevice().type === "mobile" ? (
|
||
<div
|
||
className="absolute top-0 left-0 w-full h-full"
|
||
onTouchStart={() => toastWarn(t("notification.getAccess"))}
|
||
></div>
|
||
) : (
|
||
<div
|
||
className="absolute top-0 left-0 w-full h-full"
|
||
onMouseDown={() => toastWarn(t("notification.getAccess"))}
|
||
></div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
<div className="absolute top-0 left-0 min-h-screen flex flex-col justify-center transition-all">
|
||
<div className="flex flex-col gap-4 lg:p-4 p-2">
|
||
{new userAgentParser().getDevice().model !== "iPhone" && (
|
||
<>
|
||
{!handleFullScreen.active ? (
|
||
<button
|
||
onClick={() => handleFullScreen.enter()}
|
||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||
>
|
||
<FullscreenIcon />
|
||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||
<Trans i18nKey={"fullscreenMode"}>
|
||
Полноэкранный режим
|
||
</Trans>
|
||
</span>
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={() => handleFullScreen.exit()}
|
||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||
>
|
||
<WindowedModeIcon />
|
||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||
<Trans i18nKey={"windowedMode"}>Оконный режим</Trans>
|
||
</span>
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => setModal(<QRCodeModal />)}
|
||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||
>
|
||
<QRIcon />
|
||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||
<Trans i18nKey={"inviteByQRCode"}>Пригласить по QR коду</Trans>
|
||
</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setModal(<ShareModal />)}
|
||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||
>
|
||
<ShareIcon />
|
||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||
<Trans i18nKey={"inviteByLink"}>Пригласить по ссылке</Trans>
|
||
</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() =>
|
||
setModal(
|
||
<UsersManagementModal
|
||
me={me}
|
||
handleUpdate={update}
|
||
handleKick={kick}
|
||
/>
|
||
)
|
||
}
|
||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||
>
|
||
<PersonsIcon />
|
||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||
<Trans i18nKey={"members"}>Участники</Trans>
|
||
</span>
|
||
<div className="absolute px-2 py-1 text-xs flex flex-col justify-center items-center rounded-full -top-2 -right-2 bg-[#131313] -z-10">
|
||
{users.length}
|
||
</div>
|
||
</button>
|
||
|
||
<LiveKitRoom
|
||
video={false}
|
||
audio={true}
|
||
token={token}
|
||
serverUrl={serverUrl}
|
||
>
|
||
<RoomAudioRenderer />
|
||
<ToggleMic socket={socket} handleUpdate={update} />
|
||
</LiveKitRoom>
|
||
</div>
|
||
</div>
|
||
|
||
<Transition
|
||
in={modal ? true : false}
|
||
timeout={200}
|
||
mountOnEnter
|
||
unmountOnExit
|
||
>
|
||
{(state) => <ModalContainer className={state} />}
|
||
</Transition>
|
||
|
||
<ToastContainer />
|
||
</FullScreen>
|
||
);
|
||
}
|
||
|
||
export default StreamPage;
|