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() {
- {remoteStreams.map(({ remoteStream }, index) => (
+ {remoteStreams.map(({ peerId, mediaStream }) => (
))}
@@ -1135,16 +1151,16 @@ 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}
+
+
+ : }
+ onlyIcon
+ onClick={() => me?.isAdmin && transferControl(me.id)}
+ />
+
+ : }
+ onlyIcon
+ onClick={toggleMic}
+ />
+ : }
+ onlyIcon
+ onClick={toggleCamera}
+ />
+
+
+ {users.map((user) => {
+ if (user.id !== userId) {
+ return (
+
+
+
+ {name[0]?.toUpperCase()}
+
+ {user?.isControlAllowed && (
+
+ )}
+
+
{user.name}
+
+ {me?.isAdmin && me?.isControlAllowed && (
+
+ {/*
}
+ onlyIcon
+ /> */}
+ {/*
*/}
+ }
+ onlyIcon
+ onClick={() => transferControl(user.id)}
+ />
+ {/*
*/}
+
+ )}
+
+ );
+ }
+ })}
+
+
+
+ {WSUrl && (
+
+ )}
+
+
+
+ {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"