Files
stream.graff.tech-client/src/StreamPage.tsx
T
2024-02-06 13:20:47 +05:00

423 lines
14 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, useSearchParams } from "react-router-dom";
import { FullScreen, useFullScreenHandle } from "react-full-screen";
import { useEffect, useRef, 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";
import Chat from "./components/Chat";
// import AFKTimerModal from "./components/modals/AFKTimerModal";
import { differenceInMilliseconds, format, parseISO } from "date-fns";
import HandOnIcon from "./components/icons/HandOnIcon";
import { useMobileOrientation } from "react-device-detect";
import RotateDeviceIcon from "./components/icons/RotateDeviceIcon";
import LoaderIcon from "./components/icons/LoaderIcon";
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 meRef = useRef<any>({});
meRef.current = me;
const [modal, setModal] = useModalStore((state) => [
state.modal,
state.setModal,
]);
const params = useParams();
const [searchParams] = useSearchParams();
const roomId = params.id;
const livekitServerUrl = "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,
]);
const [isShowChat, setIsShowChat] = useState<boolean>(false);
const [stopwatch, setStopwatch] = useState<string>();
const { orientation } = useMobileOrientation();
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);
}
function updateStartDate(date: Date) {
const diffMs = differenceInMilliseconds(new Date(), date);
setStopwatch(format(diffMs, "mm:ss"));
setTimeout(() => {
updateStartDate(date);
}, 1000);
}
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}/`
);
updateStartDate(parseISO(activeSession.createdAt));
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,
});
}
async function checkIsActiveSession() {
const activeSession: any = await ky
.get(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`)
.json();
console.log(activeSession);
if (!activeSession) {
setIsStreamEnded(true);
return;
}
if (!isStreamEnded) {
setTimeout(() => {
checkIsActiveSession();
}, 1000);
}
}
useEffect(() => {
connect();
const socket: Socket = io(import.meta.env.VITE_COORD_URL, {
query: {
roomId,
admin: searchParams.has("admin", "true"),
},
});
socket.on("connect", () => {
console.log("Socket: ", socket.id);
setSocket(socket);
});
socket.on("join", (socketId, sockets) => {
console.log("User connected: ", socketId, sockets);
setUsers(sockets);
toastHandOn(t("notification.newMember"));
});
socket.on("update", (socketId, data, sockets) => {
console.log("Update users: ", socketId, data, sockets);
setUsers(sockets);
});
socket.on("kick", () => {
socket.close();
window.close();
});
socket.on("leave", (socketId) => {
setUsers(
useStreamUserStore
.getState()
.users.filter((user: any) => user.id !== socketId)
);
});
socket.on("notification", (type) => {
if (type === "afk-timer") {
// setModal(<AFKTimerModal />);
}
});
checkIsActiveSession();
return () => {
socket.off("connect");
socket.off("join");
socket.off("update");
socket.off("kick");
socket.off("leave");
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 ? (
<>
{orientation === "landscape" ? (
<>{streamUrl && <Player ss={streamUrl} />}</>
) : (
<div className="absolute top-0 left-0 w-full h-full flex flex-col justify-center items-center gap-2">
<RotateDeviceIcon />
<Trans i18nKey="rotateDevice">Поверните устройство</Trans>
</div>
)}
</>
) : (
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center gap-4">
<LoaderIcon className="animate-spin w-8 h-8" />
<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>
{/* <button
onClick={() => setIsShowChat(true)}
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90 flex justify-center items-center"
>
<ChatIcon className="w-5 h-5" />
<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={"chat"}>Чат</Trans>
</span>
</button> */}
<LiveKitRoom
video={false}
audio={true}
token={token}
serverUrl={livekitServerUrl}
>
<RoomAudioRenderer />
<ToggleMic socket={socket} handleUpdate={update} />
</LiveKitRoom>
<div className="relative group outline-none w-10 h-10 bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90 flex justify-center items-center">
<p className="text-xs">{stopwatch}</p>
</div>
</div>
</div>
<Transition in={isShowChat} timeout={200} mountOnEnter unmountOnExit>
{(state) => (
<div className="absolute right-0 bottom-0">
<Chat className={state} handleClose={() => setIsShowChat(false)} />
</div>
)}
</Transition>
<Transition
in={modal ? true : false}
timeout={200}
mountOnEnter
unmountOnExit
>
{(state) => <ModalContainer className={state} />}
</Transition>
<ToastContainer />
</FullScreen>
);
}
export default StreamPage;