diff --git a/src/components/Chat2.tsx b/src/components/Chat2.tsx index eb2f9ce..4344d17 100644 --- a/src/components/Chat2.tsx +++ b/src/components/Chat2.tsx @@ -9,6 +9,7 @@ import useStreamUserStore from "../stores/useStreamUserStore"; import { FormEvent, useEffect, useRef, useState } from "react"; import useSocketStore from "../stores/useSocketStore"; import useChatStore from "../stores/useChatStore"; +import { isMobile } from "react-device-detect"; interface Props { onClose: () => void; @@ -26,6 +27,8 @@ function Chat2({ onClose }: Props) { function sendMessage(e: FormEvent) { e.preventDefault(); + if (!messageText || messageText === " ") return; + socket?.emit("message", messageText); setMessageText(""); @@ -42,22 +45,24 @@ function Chat2({ onClose }: Props) { return (
-
-

- Чат -

-
{messages.map((message, index) => (
setMessageText(e.target.value)} + onChange={(e) => + setMessageText(e.target.value.replace(/\s+/g, " ")) + } /> */} + + +
+ ); +} + +export default Dropdown; diff --git a/src/components/User.tsx b/src/components/User.tsx index daf6501..3da4573 100644 --- a/src/components/User.tsx +++ b/src/components/User.tsx @@ -1,82 +1,86 @@ -import { useRef, useState } from "react"; -import CloseIcon from "./icons/CloseIcon"; +import { isMobile } from "react-device-detect"; +import DesktopIcon from "./icons/DesktopIcon"; import HandOnIcon from "./icons/HandOnIcon"; -import Button from "./ui/Button"; +import MobileIcon from "./icons/MobileIcon"; import MoreIcon from "./icons/MoreIcon"; -import { useOnClickOutside } from "usehooks-ts"; import Tooltip from "./Tooltip"; +import Button from "./ui/Button"; import IUser from "../types/IUser"; +import CloseIcon from "./icons/CloseIcon"; +import { useState } from "react"; +import { useClickAway } from "@uidotdev/usehooks"; -interface UserProps { +interface Props { + me: IUser; user: IUser; - onTransferControl: (userId: string) => void; - onKickUser: (userId: string) => void; - className?: string; - tooltip?: boolean; + handleTransferControl: () => void; + handleKick: () => void; } -function User({ - user, - onTransferControl, - onKickUser, - className, - tooltip = false, -}: UserProps) { - const [isShowMore, setIsShowMore] = useState(false); - const moreRef = useRef(null); +function User({ me, user, handleTransferControl, handleKick }: Props) { + const [showMore, setShowMore] = useState(false); - function handleClickOutside() { - setIsShowMore(false); - } - - useOnClickOutside(moreRef, handleClickOutside); - - function handleClickTransferControl() { - onTransferControl(user.id); - setIsShowMore(false); - } - - function handleClickKickUser() { - onKickUser(user.id); - setIsShowMore(false); - } + const ref = useClickAway(() => { + setShowMore(false); + }); return ( -
- +
+
+

{user.name[0]?.toUpperCase()}

+ {user?.isControlAllowed && ( +
+ )} +
+

{user.name}

+
+ {isMobile ? : } +
- {isShowMore && ( -
- - + Отключить микрофон + */} + + +
+ )}
)}
diff --git a/src/main.tsx b/src/main.tsx index 43cff56..f60c714 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,8 +7,7 @@ import App from "./App"; // import ErrorBoundary from "./ErrorBoundary"; import HistoryPage from "./HistoryPage"; import ScheduledPage from "./ScheduledPage"; -// import StreamPage2 from "./pages/StreamPage2"; -import StreamPage3 from "./pages/StreamPage3"; +import StreamPage from "./pages/StreamPage"; const router = createBrowserRouter([ { @@ -18,7 +17,7 @@ const router = createBrowserRouter([ }, { path: "/stream/:id", - element: , + element: , }, { path: "/history", diff --git a/src/pages/StreamPage3.tsx b/src/pages/StreamPage.tsx similarity index 86% rename from src/pages/StreamPage3.tsx rename to src/pages/StreamPage.tsx index 1ebe3e6..dd1a699 100644 --- a/src/pages/StreamPage3.tsx +++ b/src/pages/StreamPage.tsx @@ -43,13 +43,14 @@ 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 StreamPage3() { +function StreamPage() { const params = useParams(); const [searchParams] = useSearchParams(); const [WSUrl, setWSUrl] = useState(""); @@ -218,6 +219,17 @@ function StreamPage3() { 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 }]); }); @@ -289,10 +301,13 @@ function StreamPage3() { } function requestControl(userId: string) { - console.log("requestControl func", userId); 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}`) @@ -443,50 +458,22 @@ function StreamPage3() {
- {users.map((user) => { - if (user.id !== userId) { - return ( -
-
-

- {name[0]?.toUpperCase()} -

- {user?.isControlAllowed && ( -
- )} -
-

{user.name}

-
- {isMobile ? : } -
- - {me?.isAdmin && me?.isControlAllowed && ( -
- {/*
- {/*
*/} -
- )} - - ); - } - })} + {me && + users.map((user) => { + if (user.id !== userId) { + return ( + transferControl(user.id)} + handleKick={() => kick(user.id)} + /> + ); + } + })} -
+
-
-
- {!isIOS && ( +
+
- )} + {!isIOS && ( +
+
+ )} +
@@ -607,4 +597,4 @@ function StreamPage3() { ); } -export default StreamPage3; +export default StreamPage; diff --git a/src/pages/StreamPage2.css b/src/pages/StreamPage2.css deleted file mode 100644 index 991cda3..0000000 --- a/src/pages/StreamPage2.css +++ /dev/null @@ -1,33 +0,0 @@ -.entering { - opacity: 1; -} - -.entered { - opacity: 1; -} - -.exiting { - opacity: 0; -} - -.exited { - opacity: 0; -} - -:root { - --toastify-toast-offset: 52px; -} - -.Toastify__toast-body { - padding: 0; - margin: 0 4px 0 0; - align-items: normal; - font-family: "Inter", sans-serif; - font-size: 14px; - color: #111c26; -} - -.Toastify__toast-icon { - width: auto; - margin-inline-end: 8px; -} diff --git a/src/pages/StreamPage2.tsx b/src/pages/StreamPage2.tsx deleted file mode 100644 index 71fe581..0000000 --- a/src/pages/StreamPage2.tsx +++ /dev/null @@ -1,1234 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable react-hooks/rules-of-hooks */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable react-hooks/exhaustive-deps */ -import "./StreamPage2.css"; -import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper"; -import { useParams, useSearchParams } from "react-router-dom"; -import { FormEvent, useEffect, useRef } from "react"; -import { Transition } from "react-transition-group"; -import Button from "../components/ui/Button"; -// import CloseIcon from "../components/icons/CloseIcon"; -import HandOffIcon from "../components/icons/HandOffIcon"; -// import MicroOffIcon from "../components/icons/MicroOffIcon"; -import PersonsIcon from "../components/icons/PersonsIcon"; -import MicroOnIcon from "../components/icons/MicroOnIcon"; -// import MoreIcon from "../components/icons/MoreIcon"; -import FullscreenIcon from "../components/icons/FullscreenIcon"; -import WindowIcon from "../components/icons/WindowIcon"; -import ChatIcon from "../components/icons/ChatIcon"; -import ShareIcon from "../components/icons/ShareIcon"; -// import GearIcon from "../components/icons/GearIcon"; -import { useFullscreen } from "ahooks"; -import api from "../utils/api"; -import { Socket, io } from "socket.io-client"; -import { v4 as uuidv4 } from "uuid"; -import HandOnIcon from "../components/icons/HandOnIcon"; -import User from "../components/User"; -import Tooltip from "../components/Tooltip"; -import { Bounce, ToastContainer, toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import InfoBlueIcon from "../components/icons/InfoBlueIcon"; -import CloseIcon from "../components/icons/CloseIcon"; -import SendChatIcon from "../components/icons/SendChatIcon"; -import UserIcon from "../components/icons/UserIcon"; -import { isIOS, isMobile, useMobileOrientation } from "react-device-detect"; -import LinkIcon from "../components/icons/LinkIcon"; -import removeSpaces from "../utils/removeSpaces"; -import MobileIcon from "../components/icons/MobileIcon"; -import DesktopIcon from "../components/icons/DesktopIcon"; -import { format } from "date-fns"; -import IUser from "../types/IUser"; -import IMessage from "../types/IMessage"; -import InfoIcon from "../components/icons/InfoIcon"; -import Rotate64Icon from "../components/icons/Rotate64Icon"; -import { useClipboard } from "use-clipboard-copy"; -import QRCode from "react-qr-code"; -import Star12Icon from "../components/icons/Star12Icon"; -import { Trans, useTranslation } from "react-i18next"; -import Countdown from "react-countdown"; -import useStreamStore from "../stores/useStreamStore"; -import Peer from "peerjs"; -import Video from "../components/Video"; -import MicroOffIcon from "../components/icons/MicroOffIcon"; -import useIsAudioActive from "use-is-audio-active"; -import CameraOffIcon from "../components/icons/CameraOffIcon"; -import CameraOnIcon from "../components/icons/CameraOnIcon"; -import useState from "react-usestateref"; - -const renderer = ({ minutes, seconds }: any) => { - return ( - <> - {String(minutes).padStart(2, "0")}:{String(seconds).padStart(2, "0")} - - ); -}; - -interface IRemoteStream { - peerId: string; - mediaStream: MediaStream; -} - -function StreamPage2() { - const { t, i18n } = useTranslation(); - const { isPortrait } = useMobileOrientation(); - const params = useParams(); - const [searchParams] = useSearchParams(); - const [socket, setSocket] = useState(); - const [wsUrl, setWsUrl] = useState(); - const [isEnded, setIsEnded] = useState(false); - const { name, setName } = useStreamStore(); - const userId = uuidv4(); - const [step, setStep] = useState(1); - const nameRef = useRef(null!); - const [users, setUsers] = useState([]); - const fullscreenRef = useRef(null); - const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef); - const [isShowChat, setIsShowChat] = useState(false); - const [isShowUsers, setIsShowUsers] = useState(false); - const [isShowInviteModal, setIsShowInviteModal] = useState(false); - const [isVideoInitialized, setIsVideoInitialized] = useState(false); - const [messageText, setMessageText] = useState(""); - const [messages] = useState([]); - const messagesRef = useRef(null); - const messageTextRef = useRef(null); - const parentElementRef = useRef(null); - const clipboard = useClipboard(); - const link = window.location.origin + window.location.pathname; - const [anyNewMessages, setAnyNewMessages] = useState(false); - const [endAt, setEndAt] = useState(); - - const localVideoRef = useRef(null); - const [localStream, setLocalStream] = useState( - new MediaStream() - ); - const [remoteStreams, setRemoteStreams, remoteStreamsRef] = useState< - IRemoteStream[] - >([]); - const [peerId, setPeerId] = useState(""); - const [peerInstance, setPeerInstance] = useState(); - const [permission, setPermission] = useState(); - const isSpeaking = useIsAudioActive({ - source: localStream.getTracks().length ? localStream : null, - }); - const isCallInit = useRef(false); - - const [isVideoEnabled, setIsVideoEnabled] = useState(false); - const [isAudioEnabled, setIsAudioEnabled] = useState(false); - - async function getLang() { - const { countryCode, error }: { countryCode: string; error: string } = - await api.get("getCountryCode").json(); - - if (!error && countryCode !== "RU") { - i18n.changeLanguage("en"); - } - } - - function handleClickClipboard() { - clipboard.copy(); - - toast.info("Ссылка скопирована в буфер обмена", { - icon: , - position: "top-center", - autoClose: 3000, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - - async function sendMessage(e: FormEvent) { - e.preventDefault(); - - const text = removeSpaces(messageText); - - if (text) { - const name = users.find((user) => user.id === userId)?.name; - socket?.emit("message", { name, text }); - } - - setMessageText(""); - messageTextRef.current?.focus(); - } - - 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; - } - } - - 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/${activeSession.name}/${activeSession.cirrusPort}/` - ); - - checkSessionStatus(); - } - - function handleSetName(e: FormEvent) { - e.preventDefault(); - - if (!name) { - nameRef.current.focus(); - return; - } - - setStep(2); - } - - function setNameGuest() { - i18n.language === "ru" ? setName("Гость") : setName("Guest"); - - setStep(2); - } - - function toggleVideo() { - localStream.getVideoTracks().forEach((track) => { - track.enabled = !track.enabled; - - if (!permission) return; - setIsVideoEnabled(track.enabled); - }); - } - - function toggleAudio() { - localStream.getAudioTracks().forEach((track) => { - track.enabled = !track.enabled; - - if (!permission) return; - setIsAudioEnabled(track.enabled); - }); - } - - function transferControl(userId: string) { - socket?.emit("transferControl", userId); - } - - function requestControl(userId: string) { - socket?.emit("requestControl", userId); - - toast.info(`Вы запросили разрешение на управление`, { - icon: , - position: "top-center", - autoClose: 3000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - - function kickUser(userId: string) { - socket?.emit("kickUser", userId); - } - - 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(); - - 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 socket = io(import.meta.env.VITE_SOCKET_URL, { - transports: ["websocket"], - auth: { - roomId: params.id, - user: { - id: userId, - name: name, - device: isMobile ? "mobile" : "desktop", - isAdmin: searchParams.has("admin", true), - peerId, - }, - }, - }); - - 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; - } - - setUsers(users); - }); - - setSocket(socket); - setStep(3); - } - - useEffect(() => { - console.log("users", users); - }, [users]); - - function updateRemoteStreams() { - setTimeout(() => { - console.log("users", users); - - const newRemoteStreams = remoteStreamsRef.current.filter((remoteStream) => - users.some((user) => user.peerId === remoteStream.peerId) - ); - - setRemoteStreams(newRemoteStreams); - }, 500); - } - - useEffect(() => { - getLang(); - getWsUrl(); - }, []); - - useEffect(() => { - document.title = t("title"); - }, [i18n.language]); - - useEffect(() => { - if (!name) return; - - setName(name.trim()); - }, [name]); - - useEffect(() => { - if (!isShowChat || messagesRef.current?.scrollTop === undefined) return; - - messagesRef.current.scrollTop = messagesRef.current.scrollHeight; - }, [messages, isShowChat]); - - useEffect(() => { - if (!messages.length || isShowChat) return; - - setAnyNewMessages(true); - }, [messages]); - - useEffect(() => { - if (isShowChat) { - setAnyNewMessages(false); - } - }, [isShowChat]); - - // useEffect(() => { - // if (!peerId) return; - - // const socket = io(import.meta.env.VITE_SOCKET_URL, { - // auth: { - // roomId: params.id, - // user: { - // id: userId, - // name: name, - // device: isMobile ? "mobile" : "desktop", - // isAdmin: searchParams.has("admin", true), - // peerId, - // }, - // }, - // }); - - // // TODO - - // socket.on("message", (message: IMessage) => { - // setMessages((prev) => [...prev, message]); - // }); - - // socket.on("requestControl", (user: IUser) => { - // if (!usersRef.current.find((user) => user.id === userId)?.isAdmin) return; - - // toast.info(`${user.name} запрашивает разрешение на управление`, { - // icon: , - // position: "top-center", - // autoClose: 5000, - // hideProgressBar: false, - // closeOnClick: true, - // pauseOnHover: true, - // draggable: true, - // progress: undefined, - // theme: "light", - // transition: Bounce, - // }); - // }); - - // socket.on("transferControl", (user: IUser) => { - // if (user.id !== userId) return; - - // toast.info(`Вы получили разрешение на управление`, { - // icon: , - // position: "top-center", - // autoClose: 3000, - // hideProgressBar: false, - // closeOnClick: true, - // pauseOnHover: true, - // draggable: true, - // progress: undefined, - // theme: "light", - // transition: Bounce, - // }); - // }); - - // socket.on("kickUser", (socketUserId: string) => { - // if (socketUserId === userId) { - // socket.disconnect(); - // window.close(); - // window.location.reload(); - // } - // }); - - // setSocket(socket); - // }, [peerId]); - - useEffect(() => { - if (!isMobile) return; - if (isShowUsers) { - setIsShowChat(false); - } - }, [isShowUsers]); - - useEffect(() => { - if (!isMobile) return; - if (isShowChat) { - setIsShowUsers(false); - } - }, [isShowChat]); - - useEffect(() => { - if (step === 3) { - setInterval(async () => { - await checkSessionStatus(); - }, 1000); - } - }, [step]); - - useEffect(() => { - if (permission === undefined) return; - - initPeer(); - }, [permission]); - - useEffect(() => { - if (!peerId) return; - - initSocket(); - }, [peerId]); - - useEffect(() => { - if (!users.length) return; - - updateRemoteStreams(); - }, [users.length]); - - return ( - <> - {isEnded === false ? ( -
-
-
-
- -
-
-
- {name && name[0].toUpperCase()} - {users.find((user) => user.id === userId) - ?.isControlAllowed && ( -
- )} - {users.find((user) => user.id === userId)?.isAdmin && ( -
- -
- )} -
-

{name}

-
-
- {users.find((user) => user.id === userId)?.isControlAllowed ? ( - - ) : ( - - )} -
-
-
- {users - .filter((user) => user.id !== userId) - .map( - (user, index) => - index < 3 && ( -
-
- {user.name[0].toUpperCase()} - {user.isControlAllowed && ( -
- )} - {user.isAdmin && ( -
- -
- )} -
-

{user.name}

-
- {user.device === "mobile" ? ( - - ) : ( - - )} -
- {users.find((user) => user.id === userId) - ?.isAdmin && ( - - transferControl(userId) - } - onKickUser={(userId) => { - kickUser(userId); - }} - /> - )} -
- ) - )} - - {users.length > 4 && ( -
-
- - -
-
-
-
-
- -
- {!isIOS && ( -
-
-
-
-
-
-
-
-
- {wsUrl && ( - setIsVideoInitialized(true)} - /> - )} - - {!users.find((user) => user.id === userId)?.isControlAllowed && ( -
- toast.error( - `Запросите доступ на управление у администратора!`, - { - icon: , - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - } - ) - } - onMouseDown={() => - toast.error( - `Запросите доступ на управление у администратора!`, - { - icon: , - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - } - ) - } - >
- )} - -
-
-
- - {(isShowUsers || isShowChat) && ( -
- {isShowUsers && ( -
-
-

- Участники -

-
-
- {users.map((user) => ( -
-
- {user.name[0].toUpperCase()} - {user.isControlAllowed && ( -
- )} - {user.isAdmin && ( -
- -
- )} -
-

{user.name}

-
- {user.device === "mobile" ? ( - - ) : ( - - )} -
-
- {users.find((user) => user.id === userId) - ?.isAdmin && ( - - transferControl(userId) - } - onKickUser={(userId) => { - kickUser(userId); - }} - /> - )} -
-
- ))} -
-
- -
-
- )} - {isShowChat && ( -
-
-

- Чат -

-
-
- {messages.map((message, index) => ( -
user.id === userId)?.name === - message.name - ? "bg-[#C4DDF5]" - : "bg-[#F0F1F2]" - }`} - > - {users.find((user) => user.id === userId)?.name !== - message.name && ( -

- {message.name} -

- )} - -

{message.text}

-

- {format(new Date(), "HH:mm")} -

-
- ))} -
-
- - setMessageText(e.target.value)} - /> - - -
-
- )} -
- )} -
- - {isShowInviteModal && ( -
-
-
-

- Пригласить -

-
-
-
- -

- - Отсканируйте QR-код, -
- чтобы присоедениться -
к демонстрации -
-

-
-
- - -
-
-
e.preventDefault()} - className="flex gap-2" - > - - -
-
- {/*
-

Участники

-
- {users.map((user) => ( -
-
-

- {user.name[0].toUpperCase()} -

-
-

{user.name}

-
- ))} -
-
*/} -
-
-
- )} - - - {(state) => ( -
- - {(state) => ( -
-
-
-

- Здравствуйте! -

-
-
-

- - Представьтесь, пожалуйста - -

-

- - Так мы будем знать, как к вам обращаться - -

-
-
-
- - setName(e.target.value)} - /> -
-
-
-
- - -
-
- )} -
- - - {(state) => ( -
-
-
-

- - Хотите принять участие в обсуждении? - -

-
-
-
- -
-

- - Разрешите использование камеры и микрофона - -

-
-
-

- - Выключить камеру и микрофон можно в любой момент - -

-
-
-
- {/* */} - -
-
- )} -
- - - {(state) => ( -
-
-

- - Пожалуйста, подождите - -

-
-
- -

- Подключение -

-
-
- )} -
-
- )} -
- -
- - {step === 3 && isMobile && isPortrait && ( -
-
- -

- - Поверните устройство - -

-
-
- )} - - -
- ) : ( -
-

- - Данная демонстрация была завершена - -

-
- )} - - ); -} - -export default StreamPage2;