diff --git a/.env b/.env.development similarity index 65% rename from .env rename to .env.development index 4294299..8e799b2 100644 --- a/.env +++ b/.env.development @@ -1,9 +1,7 @@ # VITE_COORD_URL=http://localhost:4000 VITE_COORD_URL=https://coord.graff.tech # VITE_CRM_API_URL=http://localhost:3001 -# VITE_CRM_API_URL=http://192.168.1.170:3001 VITE_CRM_API_URL=https://crm.stream.graff.tech/api # VITE_API_URL=http://localhost:5002 VITE_API_URL=https://stream.graff.tech/api -# VITE_SOCKET_URL=http://192.168.1.171:5003 -VITE_SOCKET_URL=https://stream.graff.tech \ No newline at end of file +VITE_SOCKET_URL=http://localhost:5003 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..3b9a2a7 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +VITE_COORD_URL=https://coord.graff.tech +VITE_CRM_API_URL=https://crm.stream.graff.tech/api +VITE_API_URL=https://stream.graff.tech/api +VITE_SOCKET_URL=https://stream.graff.tech \ No newline at end of file diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 716c210..3bf36fd 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -1,7 +1,7 @@ module.exports = { apps: [ { - name: "stream.graff.tech-client", + name: "stream.graff.tech-client:5001", exec_mode: "cluster", script: "yarn", args: "preview --host", diff --git a/package.json b/package.json index 471ea67..8651344 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-timer-hook": "^3.0.7", "react-toastify": "^10.0.5", "react-transition-group": "^4.4.5", + "react-usestateref": "^1.0.9", "socket.io-client": "^4.7.4", "ua-parser-js": "^1.0.35", "use-clipboard-copy": "^0.2.0", diff --git a/src/components/ModalContainer2.tsx b/src/components/ModalContainer2.tsx new file mode 100644 index 0000000..6fd0d20 --- /dev/null +++ b/src/components/ModalContainer2.tsx @@ -0,0 +1,11 @@ +import useModalStore from "../stores/useModalStore"; + +function ModalContainer2() { + const { modal } = useModalStore(); + + if (modal) { + return
{modal}
; + } +} + +export default ModalContainer2; diff --git a/src/components/Video.tsx b/src/components/Video.tsx index 9165194..d306fb5 100644 --- a/src/components/Video.tsx +++ b/src/components/Video.tsx @@ -1,14 +1,27 @@ -import { useEffect, useRef } from "react"; +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { useEffect, useRef, useState } from "react"; import useIsAudioActive from "use-is-audio-active"; +import IUser from "../types/IUser"; +import SoundOffIcon from "./icons/SoundOffIcon"; +import SoundOnIcon from "./icons/SoundOnIcon"; interface Props { mediaStream: MediaStream | null; muted: boolean; + user?: IUser; } -function Video({ mediaStream, muted }: Props) { +function Video({ mediaStream, muted, user }: Props) { const remoteVideoRef = useRef(null); const isSpeaking = useIsAudioActive({ source: mediaStream }); + const [_muted, setMuted] = useState(muted); + + function toggleSound() { + if (!remoteVideoRef.current) return; + // remoteVideoRef.current.muted = !remoteVideoRef.current.muted; + setMuted((prev) => !prev); + } useEffect(() => { if (!remoteVideoRef.current) return; @@ -19,16 +32,30 @@ function Video({ mediaStream, muted }: Props) { }; }, [mediaStream]); + useEffect(() => { + if (!remoteVideoRef.current) return; + + console.log("remoteVideoRef.current!.muted", remoteVideoRef.current.muted); + }, [remoteVideoRef.current?.muted]); + return ( - +
+ +
+

{user?.name}

+ +
+
); } diff --git a/src/components/icons/CameraOffIcon.tsx b/src/components/icons/CameraOffIcon.tsx index 27e7670..082cba8 100644 --- a/src/components/icons/CameraOffIcon.tsx +++ b/src/components/icons/CameraOffIcon.tsx @@ -1,17 +1,26 @@ function CameraOffIcon() { return ( + ); diff --git a/src/components/icons/CameraOnIcon.tsx b/src/components/icons/CameraOnIcon.tsx index cfc8fc9..c9a2dfd 100644 --- a/src/components/icons/CameraOnIcon.tsx +++ b/src/components/icons/CameraOnIcon.tsx @@ -1,17 +1,17 @@ function CameraOnIcon() { return ( ); diff --git a/src/components/icons/DesktopIcon.tsx b/src/components/icons/DesktopIcon.tsx index 223f4ab..14b3a3f 100644 --- a/src/components/icons/DesktopIcon.tsx +++ b/src/components/icons/DesktopIcon.tsx @@ -1,20 +1,23 @@ -import { SVGProps } from "react"; -import { JSX } from "react/jsx-runtime"; - -function DesktopIcon(props: JSX.IntrinsicAttributes & SVGProps) { +function DesktopIcon() { return ( + ); diff --git a/src/components/icons/MobileIcon.tsx b/src/components/icons/MobileIcon.tsx index f00c5fb..d70c5f4 100644 --- a/src/components/icons/MobileIcon.tsx +++ b/src/components/icons/MobileIcon.tsx @@ -1,22 +1,17 @@ -import React from "react"; -import { JSX } from "react/jsx-runtime"; - -function MobileIcon( - props: JSX.IntrinsicAttributes & React.SVGProps -) { +function MobileIcon() { return ( ); diff --git a/src/components/icons/MoreIcon.tsx b/src/components/icons/MoreIcon.tsx index ec25d90..8c5a2d6 100644 --- a/src/components/icons/MoreIcon.tsx +++ b/src/components/icons/MoreIcon.tsx @@ -1,15 +1,15 @@ function MoreIcon() { return ( - - - + + + ); } diff --git a/src/components/icons/SoundOffIcon.tsx b/src/components/icons/SoundOffIcon.tsx new file mode 100644 index 0000000..d65385b --- /dev/null +++ b/src/components/icons/SoundOffIcon.tsx @@ -0,0 +1,29 @@ +function SoundOffIcon() { + return ( + + + + + ); +} + +export default SoundOffIcon; diff --git a/src/components/icons/SoundOnIcon.tsx b/src/components/icons/SoundOnIcon.tsx new file mode 100644 index 0000000..fa5488a --- /dev/null +++ b/src/components/icons/SoundOnIcon.tsx @@ -0,0 +1,28 @@ +function SoundOnIcon() { + return ( + + + + + ); +} + +export default SoundOnIcon; diff --git a/src/components/modals/stream/SetNameModal.tsx b/src/components/modals/stream/SetNameModal.tsx new file mode 100644 index 0000000..789079f --- /dev/null +++ b/src/components/modals/stream/SetNameModal.tsx @@ -0,0 +1,70 @@ +import { ChangeEvent, FormEvent } from "react"; +import Input from "../../ui/Input"; +import useStreamStore from "../../../stores/useStreamStore"; +import Button from "../../ui/Button"; +import useModalStore from "../../../stores/useModalStore"; + +interface Props { + onAction: () => void; +} + +function SetNameModal({ onAction }: Props) { + const { name, setName } = useStreamStore(); + const { setModal } = useModalStore(); + + function handleChangeName(e: ChangeEvent) { + setName(e.target.value); + } + + function handleClickNoName() { + setName("Guest"); + setModal(null); + onAction(); + } + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + setModal(null); + onAction(); + } + + return ( +
+
+

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

+
+

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

+

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

+
+
+
+

Имя

+ +
+
+ + +
+
+
+
+ ); +} + +export default SetNameModal; diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index 4c79828..79ba4e2 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -4,6 +4,7 @@ interface InputProps { type?: "text" | "password" | "email"; placeholder?: string; autoFocus?: boolean; + required?: boolean; value?: string; onChange?: (e: ChangeEvent) => void; } @@ -12,6 +13,7 @@ function Input({ type = "text", placeholder, autoFocus, + required, value, onChange, }: InputProps) { @@ -21,6 +23,7 @@ function Input({ placeholder={placeholder} className="bg-white border border-[#DAE0E5] w-[296px] h-10 px-2 py-2.5 rounded-lg text-sm outline-none" autoFocus={autoFocus} + required={required} value={value} onChange={(e) => onChange && onChange(e)} /> diff --git a/src/main.tsx b/src/main.tsx index f26d1a6..43cff56 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,8 +7,8 @@ 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 StreamPage2 from "./pages/StreamPage2"; +import StreamPage3 from "./pages/StreamPage3"; const router = createBrowserRouter([ { @@ -18,7 +18,7 @@ const router = createBrowserRouter([ }, { path: "/stream/:id", - element: , + element: , }, { path: "/history", diff --git a/src/pages/StreamPage2.tsx b/src/pages/StreamPage2.tsx index 6eaa087..71fe581 100644 --- a/src/pages/StreamPage2.tsx +++ b/src/pages/StreamPage2.tsx @@ -6,7 +6,7 @@ import "./StreamPage2.css"; import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper"; import { useParams, useSearchParams } from "react-router-dom"; -import { FormEvent, useEffect, useRef, useState } from "react"; +import { FormEvent, useEffect, useRef } from "react"; import { Transition } from "react-transition-group"; import Button from "../components/ui/Button"; // import CloseIcon from "../components/icons/CloseIcon"; @@ -55,6 +55,7 @@ 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 ( @@ -64,6 +65,11 @@ const renderer = ({ minutes, seconds }: any) => { ); }; +interface IRemoteStream { + peerId: string; + mediaStream: MediaStream; +} + function StreamPage2() { const { t, i18n } = useTranslation(); const { isPortrait } = useMobileOrientation(); @@ -73,12 +79,10 @@ function StreamPage2() { const [wsUrl, setWsUrl] = useState(); const [isEnded, setIsEnded] = useState(false); const { name, setName } = useStreamStore(); - const [userId] = useState(uuidv4()); + const userId = uuidv4(); const [step, setStep] = useState(1); const nameRef = useRef(null!); const [users, setUsers] = useState([]); - const usersRef = useRef([]); - usersRef.current = users; const fullscreenRef = useRef(null); const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef); const [isShowChat, setIsShowChat] = useState(false); @@ -86,7 +90,7 @@ function StreamPage2() { const [isShowInviteModal, setIsShowInviteModal] = useState(false); const [isVideoInitialized, setIsVideoInitialized] = useState(false); const [messageText, setMessageText] = useState(""); - const [messages, setMessages] = useState([]); + const [messages] = useState([]); const messagesRef = useRef(null); const messageTextRef = useRef(null); const parentElementRef = useRef(null); @@ -94,21 +98,24 @@ function StreamPage2() { 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 videoRef = useRef(null); - // const [users, setUsers] = useState([]); - const peerInstance = useRef(); - const mediaStreamInstance = useRef(null); - // const [isEnabledVideo, setIsEnabledVideo] = useState(true); - // const [isEnabledAudio, setIsEnabledAudio] = useState(true); + const [peerInstance, setPeerInstance] = useState(); const [permission, setPermission] = useState(); - const [remoteStreams, setRemoteStreams] = useState([]); - // const [errorMessage, setErrorMessage] = useState(""); + const isSpeaking = useIsAudioActive({ + source: localStream.getTracks().length ? localStream : null, + }); + const isCallInit = useRef(false); + const [isVideoEnabled, setIsVideoEnabled] = useState(false); const [isAudioEnabled, setIsAudioEnabled] = useState(false); - const isSpeaking = useIsAudioActive({ - source: mediaStreamInstance.current, - }); async function getLang() { const { countryCode, error }: { countryCode: string; error: string } = @@ -195,121 +202,17 @@ function StreamPage2() { return; } - setSocket( - 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, - }, - }, - }) - ); - setStep(2); } function setNameGuest() { i18n.language === "ru" ? setName("Гость") : setName("Guest"); - setSocket( - io(import.meta.env.VITE_SOCKET_URL, { - auth: { - roomId: params.id, - user: { - id: userId, - name: i18n.language === "ru" ? "Гость" : "Guest", - device: isMobile ? "mobile" : "desktop", - isAdmin: searchParams.has("admin", true), - peerId, - }, - }, - }) - ); - setStep(2); } - async function getPermission() { - try { - const mediaStream = await navigator.mediaDevices.getUserMedia({ - video: true, - audio: true, - }); - - mediaStream.getAudioTracks().forEach((track) => { - track.enabled = !track.enabled; - }); - - videoRef.current!.srcObject = mediaStream; - videoRef.current!.onloadedmetadata = async () => { - try { - await videoRef.current?.play(); - } catch (error) { - // toast.error((error as Error).message); - } - }; - - mediaStreamInstance.current = mediaStream; - - setIsVideoEnabled(true); - setPermission(true); - } catch (error) { - const mediaStream = new MediaStream(); - - mediaStreamInstance.current = mediaStream; - - videoRef.current!.srcObject = mediaStream; - videoRef.current!.onloadedmetadata = async () => { - try { - await videoRef.current?.play(); - } catch (error) { - // toast.error((error as Error).message); - } - }; - - const errorMessage = (error as Error).message; - console.log("errorMessage", errorMessage); - - setPermission(false); - } - - setStep(3); - } - - async function startCalls() { - const options = { - constraints: { - offerToReceiveVideo: true, - offerToReceiveAudio: true, - }, - }; - - for (const user of users) { - if (user.peerId === peerId) continue; - - const call = peerInstance.current!.call( - user.peerId, - mediaStreamInstance.current!, - options as any - ); - - call.on("stream", (remoteStream) => { - setRemoteStreams((prev) => [...prev, { peerId, remoteStream }]); - }); - } - } - - function disallowMic() { - setStep(3); - } - function toggleVideo() { - mediaStreamInstance.current!.getVideoTracks().forEach((track) => { + localStream.getVideoTracks().forEach((track) => { track.enabled = !track.enabled; if (!permission) return; @@ -318,7 +221,7 @@ function StreamPage2() { } function toggleAudio() { - mediaStreamInstance.current!.getAudioTracks().forEach((track) => { + localStream.getAudioTracks().forEach((track) => { track.enabled = !track.enabled; if (!permission) return; @@ -351,46 +254,135 @@ function StreamPage2() { socket?.emit("kickUser", userId); } - useEffect(() => { + 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(); - // Как только соединение с сервером PeerJS установлено, запускается событие "open" peer.on("open", (id) => { setPeerId(id); }); - // Обработка входящего вызова peer.on("call", (call) => { - call.answer(mediaStreamInstance.current!); - call.on("stream", function (remoteStream) { - setRemoteStreams((prev) => [...prev, { peerId, remoteStream }]); + call.answer(localStream || undefined); + + let accept = true; + call.on("stream", (remoteStream) => { + if (!accept) return; + setRemoteStreams((prev) => [ + ...prev, + { peerId: call.peer, mediaStream: remoteStream }, + ]); + accept = false; }); }); - peerInstance.current = peer; + 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(() => { - if (!remoteStreams.length) return; - - setRemoteStreams((prev) => - prev.filter( - (obj, idx, arr) => idx === arr.findIndex((t) => t.peerId === obj.peerId) - ) - ); - - console.log("remoteStreams", remoteStreams); - }, [remoteStreams.length]); - - useEffect(() => { - if (permission === undefined) return; - - startCalls(); - }, [permission]); - useEffect(() => { document.title = t("title"); }, [i18n.language]); @@ -419,66 +411,72 @@ function StreamPage2() { } }, [isShowChat]); - useEffect(() => { - if (!socket) return; + // useEffect(() => { + // if (!peerId) return; - console.log(params.id, userId); + // 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, + // }, + // }, + // }); - socket.on("update", (roomUsers: IUser[]) => { - setUsers(roomUsers); - // for (const user of roomUsers) { - // setRemoteStreams( - // remoteStreams.filter(({ peerId }) => peerId === user.peerId) - // ); - // } - }); + // // TODO - socket.on("message", (message: IMessage) => { - setMessages((prev) => [...prev, message]); - }); + // socket.on("message", (message: IMessage) => { + // setMessages((prev) => [...prev, message]); + // }); - socket.on("requestControl", (user: IUser) => { - if (!usersRef.current.find((user) => user.id === userId)?.isAdmin) return; + // 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, - }); - }); + // 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; + // 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, - }); - }); + // 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(); - } - }); - }, [socket]); + // socket.on("kickUser", (socketUserId: string) => { + // if (socketUserId === userId) { + // socket.disconnect(); + // window.close(); + // window.location.reload(); + // } + // }); + + // setSocket(socket); + // }, [peerId]); useEffect(() => { if (!isMobile) return; @@ -502,6 +500,24 @@ function StreamPage2() { } }, [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 ? ( @@ -772,7 +788,7 @@ function StreamPage2() {
- - */} +
diff --git a/src/pages/StreamPage3.tsx b/src/pages/StreamPage3.tsx index 9e79202..1191c50 100644 --- a/src/pages/StreamPage3.tsx +++ b/src/pages/StreamPage3.tsx @@ -1,14 +1,197 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable react-hooks/exhaustive-deps */ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { PixelStreamingWrapper2 } from "../components/PixelStreamingWrapper2"; import api from "../utils/api"; import { useParams } 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, Socket } 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 MoreIcon from "../components/icons/MoreIcon"; + +const userId = uuidv4(); function StreamPage3() { const params = useParams(); const [WSUrl, setWSUrl] = useState(""); + const localVideoRef = useRef(null); + const [localStream, setLocalStream] = useState( + new MediaStream() + ); + const [remoteStreams, setRemoteStreams, remoteStreamsRef] = useStateRef< + IRemoteStream[] + >([]); + const [peerId, setPeerId] = useState(""); + const [peerInstance, setPeerInstance] = useState(); + const [permission, setPermission] = useState(); + const isSpeaking = useIsAudioActive({ + source: localStream.getTracks().length ? localStream : null, + }); + const [users, setUsers] = useState([]); + const [me, setMe] = useState(); + const isCallInit = useRef(false); + const [roomId] = useState(params.id!); + const [socket, setSocket] = useState(); + const { setModal } = useModalStore(); + const { name } = useStreamStore(); + const [isMicEnabled, setIsMicEnabled] = useState(true); + const [isCameraEnabled, setIsCameraEnabled] = useState(true); + + 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, user: { id: userId, name, 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); + setMe(users.find((user) => user.id === userId)); + }); + + 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); + } + async function getActiveSession() { const activeSession: any = await api .get(`activeSessions/${params.id}`) @@ -33,24 +216,151 @@ function StreamPage3() { ); } + function transferControl(userId: string) { + socket?.emit("transfer-control", userId); + } + useEffect(() => { getWSUrl(); + setModal(); }, []); + useEffect(() => { + if (permission === undefined) return; + + initPeer(); + }, [permission]); + + useEffect(() => { + if (!peerId) return; + + initSocket(); + }, [peerId]); + + useEffect(() => { + if (!users.length) return; + + updateRemoteStreams(); + }, [users.length]); + return ( -
- {WSUrl && ( - - )} +
+
+
+ +
+
+
+
+

{name[0]?.toUpperCase()}

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

{name}

+
+
+
+
+ {users.map((user) => { + if (user.id !== userId) { + return ( +
+
+

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

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

{user.name}

+ + {me?.isAdmin && me?.isControlAllowed && ( +
+ {/*
*/} +
+ )} +
+ ); + } + })} +
+
+
+ {WSUrl && ( + + )} + +
+
+ +
+

{name}

+
+
+ {remoteStreams.map(({ peerId, mediaStream }) => ( +
+
+ +
); } diff --git a/src/stores/useStreamUserStore.ts b/src/stores/useStreamUserStore.ts index 5a63c05..064b45d 100644 --- a/src/stores/useStreamUserStore.ts +++ b/src/stores/useStreamUserStore.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { create } from "zustand"; import { devtools } from "zustand/middleware"; +import IUser from "../types/IUser"; interface StreamUserState { - users: any[]; - setUsers: (users: any[]) => void; + users: IUser[]; + setUsers: (users: IUser[]) => void; } const useStreamUserStore = create()( diff --git a/src/types/IRemoteStream.ts b/src/types/IRemoteStream.ts new file mode 100644 index 0000000..7d25fdf --- /dev/null +++ b/src/types/IRemoteStream.ts @@ -0,0 +1,6 @@ +interface IRemoteStream { + peerId: string; + mediaStream: MediaStream; +} + +export default IRemoteStream; diff --git a/yarn.lock b/yarn.lock index f0b049e..fbd73ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2133,6 +2133,11 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" +react-usestateref@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.9.tgz#d40bc54db116e786b6b2bb1cd20fe06e7f8187f3" + integrity sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw== + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"