733 lines
24 KiB
TypeScript
733 lines
24 KiB
TypeScript
/* 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, useTranslation } 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 UsersIcon from "../components/icons/UsersIcon";
|
||
import Users from "../components/Users";
|
||
import SpeedtestModal from "../components/modals/stream/SpeedtestModal";
|
||
import InternetSpeedHighIcon from "../components/icons/InternetSpeedHighIcon";
|
||
import InternetSpeedLowIcon from "../components/icons/InternetSpeedLowIcon";
|
||
import InternetSpeedMediumIcon from "../components/icons/InternetSpeedMediumIcon";
|
||
// import ChatIcon from "../components/icons/ChatIcon";
|
||
|
||
// import MoreIcon from "../components/icons/MoreIcon";
|
||
|
||
const userId = uuidv4();
|
||
|
||
function StreamPage() {
|
||
const { t } = useTranslation();
|
||
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 [isShowUsers, setIsShowUsers] = useState<boolean>(false);
|
||
const { isPortrait } = useMobileOrientation();
|
||
const { setMessages } = useChatStore();
|
||
const [activeSession, setActiveSession, activeSessionRef] =
|
||
useStateRef<any>();
|
||
const [downloadSpeed, setDownloadSpeed] = useState(0);
|
||
|
||
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() {
|
||
console.log("initPeer");
|
||
|
||
const peer = new Peer({
|
||
host: "stream.graff.tech",
|
||
config: {
|
||
iceServers: [
|
||
{
|
||
urls: "turn:185.173.176.83: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,
|
||
isVideoInitialized,
|
||
downloadSpeed,
|
||
},
|
||
},
|
||
});
|
||
|
||
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} ${t("toasts.requestPermission")}`);
|
||
});
|
||
|
||
socket.on("transfer-control", (userId) => {
|
||
if (me?.id !== userId) return;
|
||
|
||
toast.info(t("toasts.receivedPermission"));
|
||
});
|
||
|
||
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);
|
||
socket?.emit("mic-enabled", 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() {
|
||
console.log("activeSession", activeSession);
|
||
|
||
setWSUrl(
|
||
`wss://${activeSessionRef.current.location}.sess.stream.graff.tech/server/${activeSessionRef.current.localIP}:${activeSessionRef.current.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);
|
||
}
|
||
|
||
function videoInitialized() {
|
||
socket?.emit("video-initialized", 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);
|
||
}
|
||
|
||
async function init() {
|
||
const activeSession = await getActiveSession();
|
||
|
||
console.log("activeSession init", activeSession);
|
||
|
||
if (!activeSession || activeSession.status === "error") {
|
||
setIsEnded(true);
|
||
return;
|
||
}
|
||
|
||
setActiveSession(activeSession);
|
||
setIsEnded(false);
|
||
|
||
setModal(
|
||
<SpeedtestModal
|
||
onSuccess={(downloadSpeed) => {
|
||
setDownloadSpeed(Math.round(downloadSpeed));
|
||
getWSUrl();
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
useEffect(() => {
|
||
init();
|
||
// getWSUrl();
|
||
// initSocket();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
console.log("permission", permission);
|
||
|
||
if (permission === undefined) return;
|
||
|
||
initPeer();
|
||
}, [permission]);
|
||
|
||
useEffect(() => {
|
||
if (step !== 2) return;
|
||
|
||
if (!isVideoInitialized) {
|
||
setModal(<LoadingModal />);
|
||
} else {
|
||
videoInitialized();
|
||
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={true}
|
||
positionOffset={!isMobile ? { x: "-50%", y: 0 } : { x: 0, y: 0 }}
|
||
defaultClassName={`lg:fixed top-2 left-1/2 z-10 transition-opacity ${
|
||
me ? "opacity-100" : "opacity-0"
|
||
}`}
|
||
defaultClassNameDragging="cursor-grabbing"
|
||
>
|
||
<div
|
||
className={`flex flex-col px-2 bg-white max-lg:flex-col lg:h-12 lg:px-6 max-lg:py-2 lg:min-w-[744px] lg:rounded-lg`}
|
||
>
|
||
<div
|
||
className={`flex flex-1 justify-between items-center 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 gap-4 items-center max-lg:flex-col">
|
||
<div className="hidden gap-2 items-center 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> */}
|
||
{me && (
|
||
<div className="-mt-0.5">
|
||
{(me.downloadSpeed < 10 && <InternetSpeedLowIcon />) ||
|
||
(me.downloadSpeed < 20 && (
|
||
<InternetSpeedMediumIcon />
|
||
)) ||
|
||
(me.downloadSpeed >= 20 && <InternetSpeedHighIcon />)}
|
||
</div>
|
||
)}
|
||
<p className="text-xs">{name}</p>
|
||
<div className="text-[#CCCCCC]">
|
||
{isMobile ? <MobileIcon /> : <DesktopIcon />}
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2 items-center 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={t("tooltips.returnControl")} />
|
||
)}
|
||
|
||
{!me?.isAdmin && !me?.isControlAllowed && (
|
||
<Tooltip text={t("tooltips.requestControl")} />
|
||
)}
|
||
</div>
|
||
|
||
{permission && (
|
||
<>
|
||
<div className="relative group">
|
||
<Button
|
||
variant="secondary"
|
||
icon={
|
||
isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />
|
||
}
|
||
onlyIcon
|
||
onClick={toggleMic}
|
||
/>
|
||
<Tooltip
|
||
text={
|
||
isMicEnabled
|
||
? t("tooltips.turnOffMic")
|
||
: t("tooltips.turnOnMic")
|
||
}
|
||
/>
|
||
</div>
|
||
<div className="relative group">
|
||
<Button
|
||
variant="secondary"
|
||
icon={
|
||
isCameraEnabled ? (
|
||
<CameraOnIcon />
|
||
) : (
|
||
<CameraOffIcon />
|
||
)
|
||
}
|
||
onlyIcon
|
||
onClick={toggleCamera}
|
||
/>
|
||
<Tooltip
|
||
text={
|
||
isCameraEnabled
|
||
? t("tooltips.turnOffCamera")
|
||
: t("tooltips.turnOnCamera")
|
||
}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="h-4 w-px bg-[#DAE0E5] lg:block hidden"></div>
|
||
<div className="flex gap-6">
|
||
<div className="relative group">
|
||
<Button
|
||
variant="secondary"
|
||
icon={<UsersIcon />}
|
||
onlyIcon
|
||
onClick={() => (
|
||
setIsShowUsers((prev) => !prev), setIsShowChat(false)
|
||
)}
|
||
/>
|
||
<Tooltip
|
||
text={
|
||
isShowUsers
|
||
? t("tooltips.hideParticipants")
|
||
: t("tooltips.showParticipants")
|
||
}
|
||
/>
|
||
{users.filter((user) => user.id !== userId).length >
|
||
0 && (
|
||
<div className="absolute flex items-center justify-center w-4 h-4 bg-[#49A1F5] rounded-full -top-1 -right-1">
|
||
<p className="text-[10px] text-white">
|
||
{users.filter((user) => user.id !== userId).length}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{/* {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 gap-2 items-center lg:gap-4 max-lg:flex-col lg:ml-auto">
|
||
<div className="relative group">
|
||
<Button
|
||
variant="secondary"
|
||
icon={<ChatIcon />}
|
||
onlyIcon
|
||
onClick={() => (
|
||
setIsShowChat((prev) => !prev), setIsShowUsers(false)
|
||
)}
|
||
/>
|
||
<Tooltip
|
||
text={
|
||
isShowChat ? t("tooltips.hideChat") : t("tooltips.showChat")
|
||
}
|
||
/>
|
||
</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={t("tooltips.share")} />
|
||
</div>
|
||
{!isIOS && (
|
||
<div className="relative group">
|
||
<Button
|
||
variant="secondary"
|
||
icon={
|
||
isFullscreen ? <WindowIcon /> : <FullscreenIcon />
|
||
}
|
||
onlyIcon
|
||
onClick={toggleFullscreen}
|
||
/>
|
||
<Tooltip
|
||
text={
|
||
isFullscreen
|
||
? t("tooltips.windowedMode")
|
||
: t("tooltips.fullscreenMode")
|
||
}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Draggable>
|
||
<div className="flex relative 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(t("toasts.needPermission"))}
|
||
></div>
|
||
)}
|
||
|
||
<Draggable
|
||
disabled={isMobile}
|
||
defaultClassName={`cursor-grab transition-opacity ${
|
||
isMobile ? "overflow-y-auto h-dvh" : ""
|
||
} ${me ? "opacity-100" : "opacity-0"}`}
|
||
defaultClassNameDragging="cursor-grabbing"
|
||
>
|
||
<div className="absolute top-2 space-y-2 lg:left-2 max-lg:right-2">
|
||
<div
|
||
className={`relative border-2 rounded-lg ${
|
||
!permission || !isCameraEnabled ? "hidden" : ""
|
||
} ${
|
||
isMicEnabled && isSpeaking
|
||
? "border-green-500"
|
||
: "border-transparent"
|
||
}`}
|
||
>
|
||
<video
|
||
ref={localVideoRef}
|
||
className={`object-cover bg-gray-500 rounded-lg aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] -scale-x-100`}
|
||
playsInline
|
||
autoPlay
|
||
muted
|
||
></video>
|
||
<div className="absolute bottom-0 p-2">
|
||
<p className="text-xs text-white truncate lg:text-sm">
|
||
{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)} />}
|
||
{isShowUsers && (
|
||
<Users
|
||
onClose={() => setIsShowUsers(false)}
|
||
transferControl={(userId) => transferControl(userId)}
|
||
kick={(userId) => kick(userId)}
|
||
/>
|
||
)}
|
||
|
||
{isPortrait && (
|
||
<div className="flex absolute top-0 left-0 flex-col gap-2 justify-center items-center w-full h-full bg-white">
|
||
<Rotate64Icon />
|
||
<p className="font-semibold">
|
||
<Trans i18nKey={"stream.rotateDevice"}>Поверните устройство</Trans>
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<ToastContainer />
|
||
<ModalContainer2 />
|
||
</>
|
||
)}
|
||
|
||
{isEnded === true && (
|
||
<div className="flex flex-1 justify-center items-center p-8">
|
||
<p className="text-2xl text-center text-white font-gilroy">
|
||
<Trans i18nKey={"demonstrationCompleted"}>
|
||
Данная демонстрация была завершена
|
||
</Trans>
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default StreamPage;
|