Files
stream.graff.tech-client/src/StreamPage.tsx
T
2023-09-15 18:07:20 +05:00

337 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/* 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;