Files
stream.graff.tech-client/src/pages/StreamPage.tsx
T
2024-10-30 20:57:42 +05:00

641 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef, useState } from "react";
import { PixelStreamingWrapper2 } from "../components/PixelStreamingWrapper2";
import api from "../utils/api";
import { useParams, useSearchParams } from "react-router-dom";
import useStateRef from "react-usestateref";
import Peer from "peerjs";
import useIsAudioActive from "use-is-audio-active";
import { v4 as uuidv4 } from "uuid";
import { io } from "socket.io-client";
import IRemoteStream from "../types/IRemoteStream";
import Video from "../components/Video";
import ModalContainer2 from "../components/ModalContainer2";
import IUser from "../types/IUser";
import useModalStore from "../stores/useModalStore";
import SetNameModal from "../components/modals/stream/SetNameModal";
import useStreamStore from "../stores/useStreamStore";
import Button from "../components/ui/Button";
import HandOnIcon from "../components/icons/HandOnIcon";
import HandOffIcon from "../components/icons/HandOffIcon";
import MicroOnIcon from "../components/icons/MicroOnIcon";
import MicroOffIcon from "../components/icons/MicroOffIcon";
import CameraOnIcon from "../components/icons/CameraOnIcon";
import CameraOffIcon from "../components/icons/CameraOffIcon";
import { Trans } from "react-i18next";
import { isIOS, isMobile, useMobileOrientation } from "react-device-detect";
import WindowIcon from "../components/icons/WindowIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
import ShareIcon from "../components/icons/ShareIcon";
import { useFullscreen } from "ahooks";
import InviteModal from "../components/modals/stream/InviteModal";
import Tooltip from "../components/Tooltip";
import { toast, ToastContainer } from "react-toastify";
import LoadingModal from "../components/modals/stream/LoadingModal";
import useSocketStore from "../stores/useSocketStore";
import useStreamUserStore from "../stores/useStreamUserStore";
import Chat2 from "../components/Chat2";
import Rotate64Icon from "../components/icons/Rotate64Icon";
import Draggable from "react-draggable";
import DesktopIcon from "../components/icons/DesktopIcon";
import MobileIcon from "../components/icons/MobileIcon";
import ChatIcon from "../components/icons/ChatIcon";
import useChatStore from "../stores/useChatStore";
import User from "../components/User";
// import ChatIcon from "../components/icons/ChatIcon";
// import MoreIcon from "../components/icons/MoreIcon";
const userId = uuidv4();
function StreamPage() {
const params = useParams();
const [searchParams] = useSearchParams();
const [WSUrl, setWSUrl] = useState<string>("");
const localVideoRef = useRef<HTMLVideoElement>(null);
const [localStream, setLocalStream] = useState<MediaStream>(
new MediaStream()
);
const [remoteStreams, setRemoteStreams, remoteStreamsRef] = useStateRef<
IRemoteStream[]
>([]);
const [peerId, setPeerId] = useState<string>("");
const [peerInstance, setPeerInstance] = useState<Peer>();
const [permission, setPermission] = useState<boolean>();
const isSpeaking = useIsAudioActive({
source: localStream.getTracks().length ? localStream : null,
});
const { me, users, setMe, setUsers } = useStreamUserStore();
const isCallInit = useRef<boolean>(false);
const [roomId] = useState<string>(params.id!);
const { socket, setSocket } = useSocketStore();
const { setModal } = useModalStore();
const { name } = useStreamStore();
const [isMicEnabled, setIsMicEnabled] = useState(true);
const [isCameraEnabled, setIsCameraEnabled] = useState(true);
const [isEnded, setIsEnded] = useState<boolean>();
const [, setEndAt] = useState<Date>();
const fullscreenRef = useRef(null);
const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef);
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
const [step, setStep] = useState<number>(1);
const [isShowChat, setIsShowChat] = useState<boolean>(false);
const { isPortrait } = useMobileOrientation();
const { setMessages } = useChatStore();
async function startCall(remotePeerId: string) {
if (!peerInstance) return;
console.log("startCall", remotePeerId);
const options = {
constraints: {
offerToReceiveVideo: true,
offerToReceiveAudio: true,
},
};
const call = peerInstance.call(remotePeerId, localStream, options as any);
let accept = true;
call.on("stream", (remoteStream) => {
if (!accept) return;
console.log("setRemoteStreams", remoteStream);
setRemoteStreams((prev) => [
...prev,
{ peerId: remotePeerId, mediaStream: remoteStream },
]);
accept = false;
});
}
async function getUserMedia() {
try {
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
if (!localVideoRef.current) return;
localVideoRef.current.srcObject = mediaStream;
localVideoRef.current.onloadedmetadata = () => {
localVideoRef.current?.play();
};
setLocalStream(mediaStream);
setPermission(true);
console.log("setLocalStream mediaStream", mediaStream);
} catch (error) {
setPermission(false);
console.log("ERROR: ", error);
}
}
function initPeer() {
const peer = new Peer({
host: "stream.graff.tech",
config: {
iceServers: [
{
urls: "turn:194.26.138.94:3478", // Replace with your TURN server URL
username: "username1", // Replace with your TURN username
credential: "password1", // Replace with your TURN credential
},
],
},
});
peer.on("open", (id) => {
setPeerId(id);
});
peer.on("call", (call) => {
call.answer(localStream || undefined);
let accept = true;
call.on("stream", (remoteStream) => {
if (!accept) return;
setRemoteStreams((prev) => [
...prev,
{ peerId: call.peer, mediaStream: remoteStream },
]);
accept = false;
});
});
setPeerInstance(peer);
}
function initSocket() {
const superAdmin = searchParams.has("admin", true);
console.log("superAdmin", superAdmin);
const socket = io(import.meta.env.VITE_SOCKET_URL, {
transports: ["websocket"],
auth: {
roomId,
user: {
id: userId,
name,
peerId,
superAdmin,
},
},
});
socket.on("update", async (users: IUser[]) => {
console.log("isCallInit", isCallInit.current);
if (!isCallInit.current) {
for (const user of users) {
if (userId === user.id) continue;
await startCall(user.peerId);
}
isCallInit.current = true;
}
setMe(users.find((user) => user.id === userId));
setUsers(users);
});
socket.on("request-control", (userId) => {
const user = users.find((user) => user.id === userId);
if (user?.id === me?.id || !me?.isAdmin) return;
toast.info(`${user?.name} запрашивает разрешение на управление`);
});
socket.on("transfer-control", (userId) => {
if (me?.id !== userId) return;
toast.info(`Вы получили разрешение на управление`);
});
socket.on("kick", (userId) => {
if (useStreamUserStore.getState().me?.id !== userId) return;
window.close();
socket.disconnect();
setPeerInstance(undefined);
setWSUrl("");
setUsers([]);
setRemoteStreams([]);
});
socket.on("message", ({ userId, text }) => {
setMessages([...useChatStore.getState().messages, { userId, text }]);
});
socket.on("connect", () => {
setSocket(socket);
});
}
function toggleMic() {
localStream.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
if (!permission) return;
setIsMicEnabled(track.enabled);
});
}
function toggleCamera() {
localStream.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
if (!permission) return;
setIsCameraEnabled(track.enabled);
});
}
useEffect(() => {
if (!socket) return;
console.log("socket", socket);
}, [socket?.connected]);
function updateRemoteStreams() {
setTimeout(() => {
console.log("users", users);
const newRemoteStreams = remoteStreamsRef.current.filter((remoteStream) =>
users.some((user) => user.peerId === remoteStream.peerId)
);
setRemoteStreams(newRemoteStreams);
}, 500);
}
function handleSetName() {
setStep(2);
getUserMedia();
}
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/server/${activeSession.localIP}:${activeSession.cirrusPort}`
);
setModal(<SetNameModal onAction={handleSetName} />);
checkSessionStatus();
}
function transferControl(userId: string) {
socket?.emit("transfer-control", userId);
}
function requestControl(userId: string) {
socket?.emit("request-control", userId);
}
function kick(userId: string) {
socket?.emit("kick", userId);
}
async function getActiveSession() {
const activeSession: any = await api
.get(`activeSessions/${params.id}`)
.json();
if (activeSession?.endAt) {
setEndAt(activeSession.endAt);
}
return activeSession;
}
async function checkSessionStatus() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
setTimeout(async () => {
await checkSessionStatus();
}, 1000);
}
useEffect(() => {
getWSUrl();
// initSocket();
}, []);
useEffect(() => {
if (permission === undefined) return;
initPeer();
}, [permission]);
useEffect(() => {
if (step !== 2) return;
if (!isVideoInitialized) {
setModal(<LoadingModal />);
} else {
setModal(null);
}
}, [step, isVideoInitialized]);
useEffect(() => {
if (!peerId) return;
initSocket();
}, [peerId]);
useEffect(() => {
if (!users.length) return;
updateRemoteStreams();
}, [users.length]);
useEffect(() => {
toggleCamera();
toggleMic();
}, [localStream]);
return (
<div
ref={fullscreenRef}
className="h-dvh flex lg:flex-col bg-[#111C26] overflow-hidden"
>
{isEnded === false && (
<>
<Draggable
disabled={isMobile}
positionOffset={!isMobile ? { x: "-50%", y: 0 } : { x: 0, y: 0 }}
defaultClassName={`cursor-grab lg:fixed top-2 left-1/2 z-10 transition-opacity transition-opacity ${
me ? "opacity-100" : "opacity-0"
}`}
defaultClassNameDragging="cursor-grabbing"
>
<div
className={`bg-white max-lg:flex-col lg:h-12 lg:px-6 px-2 max-lg:py-2 lg:min-w-[744px] lg:rounded-lg flex flex-col`}
>
<div
className={`flex items-center justify-between flex-1 max-lg:flex-col`}
>
<div className="lg:pr-6">
<img
src="/images/logo24.svg"
alt=""
className="hidden pointer-events-none lg:block"
/>
</div>
<div className="flex items-center gap-4">
<div className="items-center hidden gap-2 lg:flex">
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<p className="text-xs font-semibold">
{name[0]?.toUpperCase()}
</p>
{me?.isControlAllowed && (
<div className="absolute bottom-0 right-0 bg-[#49A1F5] w-2 h-2 rounded-full border border-white"></div>
)}
</div>
<p className="text-xs">{name}</p>
<div className="text-[#CCCCCC]">
{isMobile ? <MobileIcon /> : <DesktopIcon />}
</div>
</div>
<div className="flex items-center gap-2 max-lg:flex-col">
<div className="relative group">
<Button
variant="secondary"
icon={
me?.isControlAllowed ? (
<HandOnIcon />
) : (
<HandOffIcon />
)
}
onlyIcon
onClick={() =>
me!.isAdmin
? transferControl(me!.id)
: requestControl(me!.id)
}
/>
{me?.isAdmin && !me.isControlAllowed && (
<Tooltip text={"Вернуть управление"} />
)}
{!me?.isAdmin && !me?.isControlAllowed && (
<Tooltip text={"Запросить управление"} />
)}
</div>
{permission && (
<>
<div className="relative group">
<Button
variant="secondary"
icon={
isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />
}
onlyIcon
onClick={toggleMic}
/>
<Tooltip
text={
isMicEnabled
? "Выключить микрофон"
: "Включить микрофон"
}
/>
</div>
<div className="relative group">
<Button
variant="secondary"
icon={
isCameraEnabled ? (
<CameraOnIcon />
) : (
<CameraOffIcon />
)
}
onlyIcon
onClick={toggleCamera}
/>
<Tooltip
text={
isCameraEnabled
? "Выключить камеру"
: "Включить камеру"
}
/>
</div>
</>
)}
</div>
<div className="h-4 w-px bg-[#DAE0E5] lg:block hidden"></div>
<div className="hidden gap-6 lg:flex">
{me &&
users.map((user) => {
if (user.id !== userId) {
return (
<User
me={me}
user={user}
handleTransferControl={() =>
transferControl(user.id)
}
handleKick={() => kick(user.id)}
/>
);
}
})}
</div>
</div>
<div className="flex items-center gap-2 lg:gap-4 max-lg:flex-col lg:ml-auto">
<div className="relative group">
<Button
variant="secondary"
icon={<ChatIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
/>
<Tooltip
text={isShowChat ? "Скрыть чат" : "Показать чат"}
/>
</div>
<div className="w-px h-4 bg-[#DAE0E5] max-lg:hidden"></div>
<div className="flex gap-2 max-lg:flex-col lg:ml-auto">
<div className="relative group">
<Button
variant="secondary"
icon={<ShareIcon />}
onlyIcon
onClick={() => setModal(<InviteModal />)}
/>
<Tooltip text={"Поделиться"} />
</div>
{!isIOS && (
<div className="relative group">
<Button
variant="secondary"
icon={
isFullscreen ? <WindowIcon /> : <FullscreenIcon />
}
onlyIcon
onClick={toggleFullscreen}
/>
<Tooltip
text={
isFullscreen
? "Оконный режим"
: "Полноэкранный режим"
}
/>
</div>
)}
</div>
</div>
</div>
</div>
</Draggable>
<div className="relative flex flex-1">
{WSUrl && (
<PixelStreamingWrapper2
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: WSUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
onVideoInitialized={() => setIsVideoInitialized(true)}
/>
)}
{step !== 1 &&
!users.find((user) => user.id === userId)?.isControlAllowed && (
<div
className="absolute top-0 left-0 w-full h-full"
onClick={() =>
toast.warn("Необходимо запросить разрешение на управление")
}
></div>
)}
<Draggable
defaultClassName={`cursor-grab transition-opacity ${
me ? "opacity-100" : "opacity-0"
}`}
defaultClassNameDragging="cursor-grabbing"
>
<div className="absolute space-y-2 top-2 lg:left-2 max-lg:right-2">
<div className={`relative ${!permission ? "hidden" : ""}`}>
<video
ref={localVideoRef}
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500 -scale-x-100 ring-2 ${
isMicEnabled && isSpeaking
? "ring-green-500"
: "ring-transparent"
}`}
playsInline
autoPlay
muted
></video>
<div className="absolute bottom-0 p-2">
<p className="text-sm text-white">{name}</p>
</div>
</div>
{remoteStreams.map(({ peerId, mediaStream }) => (
<Video
key={peerId}
mediaStream={mediaStream}
muted={!permission}
user={users.find((user) => user.peerId === peerId)}
/>
))}
</div>
</Draggable>
</div>
{isShowChat && <Chat2 onClose={() => setIsShowChat(false)} />}
{isPortrait && (
<div className="absolute top-0 left-0 flex flex-col items-center justify-center w-full h-full gap-2 bg-white">
<Rotate64Icon />
<p className="font-semibold">Поверните устройство</p>
</div>
)}
<ModalContainer2 />
<ToastContainer />
</>
)}
{isEnded === true && (
<div className="flex items-center justify-center flex-1 p-8">
<p className="text-2xl text-center text-white font-gilroy">
<Trans i18nKey={"demonstrationCompleted"}>
Данная демонстрация была завершена
</Trans>
</p>
</div>
)}
</div>
);
}
export default StreamPage;