This commit is contained in:
2024-07-05 20:08:41 +05:00
parent 9a8998501f
commit 5c6920843f
21 changed files with 798 additions and 282 deletions
+1 -3
View File
@@ -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
VITE_SOCKET_URL=http://localhost:5003
+4
View File
@@ -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
+1 -1
View File
@@ -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",
+1
View File
@@ -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",
+11
View File
@@ -0,0 +1,11 @@
import useModalStore from "../stores/useModalStore";
function ModalContainer2() {
const { modal } = useModalStore();
if (modal) {
return <div className="fixed top-0 left-0 w-full h-full">{modal}</div>;
}
}
export default ModalContainer2;
+38 -11
View File
@@ -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<HTMLVideoElement>(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 (
<video
ref={remoteVideoRef}
className={`aspect-video bg-black lg:w-[216px] lg:h-[162px] w-[112px] h-[84px] rounded-lg object-cover ring-2 ${
isSpeaking ? "ring-green-500" : "ring-transparent"
}`}
playsInline
autoPlay
muted={muted}
></video>
<div className="relative">
<video
ref={remoteVideoRef}
className={`aspect-video w-[216px] h-[162px] rounded-lg object-cover bg-gray-500 ring-2 ${
!_muted && isSpeaking ? "ring-green-500" : "ring-transparent"
}`}
playsInline
autoPlay
muted={_muted}
></video>
<div className="absolute bottom-0 p-2 flex items-center justify-between w-full">
<p className="text-sm text-white">{user?.name}</p>
<button className="text-white" onClick={toggleSound}>
{_muted ? <SoundOffIcon /> : <SoundOnIcon />}
</button>
</div>
</div>
);
}
+17 -8
View File
@@ -1,17 +1,26 @@
function CameraOffIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
width={24}
height={24}
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x={4.83594}
y={4.4209}
width={18.9738}
height={2}
rx={1}
transform="rotate(45 4.83594 4.4209)"
fill="#E94444"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M12 18.75H4.5a2.25 2.25 0 0 1-2.25-2.25V9m12.841 9.091L16.5 19.5m-1.409-1.409c.407-.407.659-.97.659-1.591v-9a2.25 2.25 0 0 0-2.25-2.25h-9c-.621 0-1.184.252-1.591.659m12.182 12.182L2.909 5.909M1.5 4.5l1.409 1.409"
fillRule="evenodd"
clipRule="evenodd"
d="M2.58831 6.67748C2.37266 7.06989 2.25 7.52062 2.25 8V16C2.25 17.5188 3.48122 18.75 5 18.75H13C13.4794 18.75 13.9301 18.6273 14.3225 18.4117L13.1517 17.2409C13.102 17.2469 13.0514 17.25 13 17.25H5C4.30964 17.25 3.75 16.6904 3.75 16V8C3.75 7.94865 3.7531 7.89802 3.75911 7.84829L2.58831 6.67748ZM14.25 12.1602V10.25L14.25 10.2483V8C14.25 7.30964 13.6904 6.75 13 6.75H8.83984L7.33984 5.25H13C14.5188 5.25 15.75 6.48122 15.75 8V8.97608L19.1889 7.06781C19.1983 7.0626 19.2078 7.05759 19.2174 7.05279C20.381 6.471 21.75 7.31712 21.75 8.61803V15.382C21.75 16.6829 20.381 17.529 19.2174 16.9472C19.2078 16.9424 19.1983 16.9374 19.1889 16.9322L18.814 16.7242L14.25 12.1602ZM19.898 15.6102L15.75 13.3084V10.6916L19.898 8.38976C20.062 8.31609 20.25 8.43584 20.25 8.61803V15.382C20.25 15.5642 20.062 15.6839 19.898 15.6102Z"
fill="currentColor"
/>
</svg>
);
+8 -8
View File
@@ -1,17 +1,17 @@
function CameraOnIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
width={24}
height={24}
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"
fillRule="evenodd"
clipRule="evenodd"
d="M2.25 8C2.25 6.48122 3.48122 5.25 5 5.25H13C14.5188 5.25 15.75 6.48122 15.75 8V8.97608L19.1889 7.06781C19.1983 7.0626 19.2078 7.05759 19.2174 7.05279C20.381 6.471 21.75 7.31712 21.75 8.61803V15.382C21.75 16.6829 20.381 17.529 19.2174 16.9472C19.2078 16.9424 19.1983 16.9374 19.1889 16.9322L15.75 15.0239V16C15.75 17.5188 14.5188 18.75 13 18.75H5C3.48122 18.75 2.25 17.5188 2.25 16V8ZM14.25 13.75L14.25 13.7517V16C14.25 16.6904 13.6904 17.25 13 17.25H5C4.30964 17.25 3.75 16.6904 3.75 16V8C3.75 7.30964 4.30964 6.75 5 6.75H13C13.6904 6.75 14.25 7.30964 14.25 8V10.2483L14.25 10.25M15.75 13.3084L19.898 15.6102C20.062 15.6839 20.25 15.5642 20.25 15.382V8.61803C20.25 8.43584 20.062 8.31609 19.898 8.38976L15.75 10.6916V13.3084Z"
fill="currentColor"
/>
</svg>
);
+12 -9
View File
@@ -1,20 +1,23 @@
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function DesktopIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
function DesktopIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
{...props}
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke="currentColor"
strokeLinecap="round"
d="M2.6665 3.87878C2.6665 3.20934 3.26346 2.66666 3.99984 2.66666L11.9998 2.66666C12.7362 2.66666 13.3332 3.20934 13.3332 3.87878V8.1212C13.3332 8.79064 12.7362 9.33332 11.9998 9.33332L3.99984 9.33332C3.26346 9.33332 2.6665 8.79064 2.6665 8.1212L2.6665 3.87878Z"
stroke="#ccc"
strokeWidth={1.5}
d="M2.667 3.879c0-.67.596-1.212 1.333-1.212h8c.736 0 1.333.542 1.333 1.212V8.12c0 .67-.597 1.212-1.333 1.212H4c-.737 0-1.333-.542-1.333-1.212V3.88ZM13.28 11H2.72a1 1 0 1 0 0 2h10.56a1 1 0 1 0 0-2Z"
strokeLinecap="round"
/>
<path
d="M13.2794 11H2.72092C2.29049 11 1.90835 11.2754 1.77224 11.6838C1.5564 12.3313 2.03836 13 2.72092 13L13.2794 13C13.962 13 14.4439 12.3313 14.2281 11.6838C14.092 11.2754 13.7098 11 13.2794 11Z"
stroke="#ccc"
strokeWidth={1.5}
strokeLinecap="round"
/>
</svg>
);
+6 -11
View File
@@ -1,22 +1,17 @@
import React from "react";
import { JSX } from "react/jsx-runtime";
function MobileIcon(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) {
function MobileIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
{...props}
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke="currentColor"
strokeLinecap="round"
d="M7.44428 3.33333H8.55539M5.99984 14L9.99984 14C10.7362 14 11.3332 13.403 11.3332 12.6667V3.33333C11.3332 2.59695 10.7362 2 9.99984 2L5.99984 2C5.26346 2 4.6665 2.59695 4.6665 3.33333L4.6665 12.6667C4.6665 13.403 5.26346 14 5.99984 14Z"
stroke="#ccc"
strokeWidth={1.5}
d="M7.444 3.333h1.111M6 14h4c.736 0 1.333-.597 1.333-1.333V3.333C11.333 2.597 10.736 2 10 2H6c-.737 0-1.333.597-1.333 1.333v9.334C4.667 13.403 5.263 14 6 14Z"
strokeLinecap="round"
/>
</svg>
);
+5 -5
View File
@@ -1,15 +1,15 @@
function MoreIcon() {
return (
<svg
width="24"
height="24"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="5" cy="12" r="1.5" fill="currentColor" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
<circle cx="19" cy="12" r="1.5" fill="currentColor" />
<circle cx={5} cy={12} r={1.5} fill="currentColor" />
<circle cx={12} cy={12} r={1.5} fill="currentColor" />
<circle cx={19} cy={12} r={1.5} fill="currentColor" />
</svg>
);
}
+29
View File
@@ -0,0 +1,29 @@
function SoundOffIcon() {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x={5.83594}
y={4.4209}
width={18.9738}
height={2}
rx={1}
transform="rotate(45 5.83594 4.4209)"
fill="#E94444"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.16048 8.24999H4C3.0335 8.24999 2.25 9.03349 2.25 9.99999V13.9986C2.25 14.9651 3.0335 15.7486 4 15.7486H7C8.02358 15.7486 9.216 16.1653 10.4444 16.8264C11.6597 17.4804 12.8396 18.3346 13.8208 19.1228C14.5058 19.6732 15.4333 19.6185 16.0596 19.1491L14.9312 18.0207C14.8703 18.017 14.8116 17.9947 14.7603 17.9535C13.7419 17.1353 12.4824 16.2197 11.1553 15.5055C9.84126 14.7984 8.38818 14.2486 7 14.2486H4C3.86193 14.2486 3.75 14.1366 3.75 13.9986L3.75 9.99999C3.75 9.86192 3.86193 9.74999 4 9.74999H6.66048L5.16048 8.24999ZM15.25 12.1605V6.24904C15.25 6.14342 15.1953 6.05789 15.0869 6.00838C14.9754 5.95742 14.855 5.96891 14.7603 6.04506C13.8127 6.8064 12.6563 7.65211 11.4309 8.34137L10.3252 7.23565C10.3649 7.21476 10.4046 7.19359 10.4444 7.17218C11.6597 6.51818 12.8396 5.664 13.8208 4.87572L14.2853 5.45394L13.8208 4.87572C14.9538 3.9654 16.75 4.71049 16.75 6.24904V13.6605L15.25 12.1605ZM20.3846 17.295L19.2778 16.1883C19.9035 14.9559 20.25 13.5064 20.25 12C20.25 9.90207 19.578 7.91449 18.4157 6.4702C18.156 6.1475 18.2071 5.67538 18.5298 5.41569C18.8525 5.156 19.3246 5.20708 19.5843 5.52978C20.9832 7.26809 21.75 9.59703 21.75 12C21.75 13.8956 21.2728 15.7452 20.3846 17.295Z"
fill="currentColor"
/>
</svg>
);
}
export default SoundOffIcon;
+28
View File
@@ -0,0 +1,28 @@
function SoundOnIcon() {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 6C20.2806 7.5913 21 9.74956 21 12C21 14.2504 20.2806 16.4087 19 18"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7 14.9986L4 14.9986C3.44772 14.9986 3 14.5509 3 13.9986L3 10C3 9.44771 3.44772 9 4 9L7 9C9.41176 9 12.291 7.06688 14.2905 5.4604C14.9661 4.91764 16 5.38248 16 6.24905L16 17.7495C16 18.6161 14.9661 19.0809 14.2905 18.5382C12.291 16.9317 9.41176 14.9986 7 14.9986Z"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default SoundOnIcon;
@@ -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<HTMLInputElement>) {
setName(e.target.value);
}
function handleClickNoName() {
setName("Guest");
setModal(null);
onAction();
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setModal(null);
onAction();
}
return (
<div className="flex items-center justify-center w-full h-full bg-opacity-50 backdrop-blur-2xl">
<div className="bg-white p-12 rounded-lg space-y-6">
<p className="text-2xl font-semibold">Здравствуйте!</p>
<div className="space-y-2">
<p className="font-semibold">Представьтесь, пожалуйста</p>
<p className="text-sm text-[#77828C]">
Так мы будем знать, как к вам обратиться
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-10">
<div className="space-y-1 text-xs">
<p className="text-[#77828C]">Имя</p>
<Input
value={name}
onChange={handleChangeName}
autoFocus={!name}
required
/>
</div>
<div className="flex gap-2">
<Button
variant="secondary"
type="submit"
large
onClick={handleClickNoName}
>
Не указывать
</Button>
<Button type="submit" large>
Продолжить
</Button>
</div>
</form>
</div>
</div>
);
}
export default SetNameModal;
+3
View File
@@ -4,6 +4,7 @@ interface InputProps {
type?: "text" | "password" | "email";
placeholder?: string;
autoFocus?: boolean;
required?: boolean;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => 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)}
/>
+3 -3
View File
@@ -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: <StreamPage2 />,
element: <StreamPage3 />,
},
{
path: "/history",
+223 -207
View File
@@ -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<string>();
const [isEnded, setIsEnded] = useState<boolean>(false);
const { name, setName } = useStreamStore();
const [userId] = useState(uuidv4());
const userId = uuidv4();
const [step, setStep] = useState<number>(1);
const nameRef = useRef<HTMLInputElement>(null!);
const [users, setUsers] = useState<IUser[]>([]);
const usersRef = useRef<IUser[]>([]);
usersRef.current = users;
const fullscreenRef = useRef(null);
const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef);
const [isShowChat, setIsShowChat] = useState<boolean>(false);
@@ -86,7 +90,7 @@ function StreamPage2() {
const [isShowInviteModal, setIsShowInviteModal] = useState(false);
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
const [messageText, setMessageText] = useState("");
const [messages, setMessages] = useState<IMessage[]>([]);
const [messages] = useState<IMessage[]>([]);
const messagesRef = useRef<HTMLDivElement>(null);
const messageTextRef = useRef<HTMLInputElement>(null);
const parentElementRef = useRef<HTMLDivElement>(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<HTMLVideoElement>(null);
const [localStream, setLocalStream] = useState<MediaStream>(
new MediaStream()
);
const [remoteStreams, setRemoteStreams, remoteStreamsRef] = useState<
IRemoteStream[]
>([]);
const [peerId, setPeerId] = useState<string>("");
const videoRef = useRef<HTMLVideoElement>(null);
// const [users, setUsers] = useState<IUser[]>([]);
const peerInstance = useRef<Peer>();
const mediaStreamInstance = useRef<MediaStream | null>(null);
// const [isEnabledVideo, setIsEnabledVideo] = useState(true);
// const [isEnabledAudio, setIsEnabledAudio] = useState(true);
const [peerInstance, setPeerInstance] = useState<Peer>();
const [permission, setPermission] = useState<boolean>();
const [remoteStreams, setRemoteStreams] = useState<any[]>([]);
// const [errorMessage, setErrorMessage] = useState<string>("");
const isSpeaking = useIsAudioActive({
source: localStream.getTracks().length ? localStream : null,
});
const isCallInit = useRef<boolean>(false);
const [isVideoEnabled, setIsVideoEnabled] = useState<boolean>(false);
const [isAudioEnabled, setIsAudioEnabled] = useState<boolean>(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: <InfoBlueIcon />,
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
});
// toast.info(`${user.name} запрашивает разрешение на управление`, {
// icon: <InfoBlueIcon />,
// 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: <InfoBlueIcon />,
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
});
// toast.info(`Вы получили разрешение на управление`, {
// icon: <InfoBlueIcon />,
// 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() {
<div className="absolute top-2 lg:left-2 lg:right-auto right-2 flex flex-col gap-2">
<video
ref={videoRef}
ref={localVideoRef}
className={`aspect-video bg-black rounded-lg -scale-x-100 object-cover ring-2 ${
isAudioEnabled && isSpeaking
? "ring-green-500"
@@ -786,10 +802,10 @@ function StreamPage2() {
autoPlay
muted
/>
{remoteStreams.map(({ remoteStream }, index) => (
{remoteStreams.map(({ peerId, mediaStream }) => (
<Video
key={index}
mediaStream={remoteStream}
key={peerId}
mediaStream={mediaStream}
muted={!permission}
/>
))}
@@ -1135,16 +1151,16 @@ function StreamPage2() {
</div>
</div>
<div className="flex gap-2">
<Button
{/* <Button
variant="secondary"
fullWidth
large
onClick={disallowMic}
>
<Trans i18nKey={"skip"}>Пропустить</Trans>
</Button>
<Button fullWidth large onClick={getPermission}>
<Trans i18nKey={"allow"}>Разрешить</Trans>
</Button> */}
<Button fullWidth large onClick={getUserMedia}>
<Trans i18nKey={"allow"}>Продолжить</Trans>
</Button>
</div>
</div>
+324 -14
View File
@@ -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<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 [users, setUsers] = useState<IUser[]>([]);
const [me, setMe] = useState<IUser>();
const isCallInit = useRef<boolean>(false);
const [roomId] = useState<string>(params.id!);
const [socket, setSocket] = useState<Socket>();
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(<SetNameModal onAction={getUserMedia} />);
}, []);
useEffect(() => {
if (permission === undefined) return;
initPeer();
}, [permission]);
useEffect(() => {
if (!peerId) return;
initSocket();
}, [peerId]);
useEffect(() => {
if (!users.length) return;
updateRemoteStreams();
}, [users.length]);
return (
<div className="h-screen">
{WSUrl && (
<PixelStreamingWrapper2
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: WSUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
/>
)}
<div className="h-screen flex flex-col bg-[#111C26]">
<div className="flex items-center bg-white h-12">
<div className="px-6">
<img src="/images/logo24.svg" alt="" />
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<p className="text-xs font-semibold">{name[0]?.toUpperCase()}</p>
{me?.isControlAllowed && (
<div className="absolute bottom-0 right-0 bg-[#49A1F5] w-2 h-2 rounded-full border border-white"></div>
)}
</div>
<p className="text-xs">{name}</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
icon={me?.isControlAllowed ? <HandOnIcon /> : <HandOffIcon />}
onlyIcon
onClick={() => me?.isAdmin && transferControl(me.id)}
/>
<Button
variant="secondary"
icon={isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={toggleMic}
/>
<Button
variant="secondary"
icon={isCameraEnabled ? <CameraOnIcon /> : <CameraOffIcon />}
onlyIcon
onClick={toggleCamera}
/>
</div>
<div className="h-4 w-px bg-[#DAE0E5]"></div>
{users.map((user) => {
if (user.id !== userId) {
return (
<div key={user.id} className="flex items-center gap-2">
<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>
{user?.isControlAllowed && (
<div className="absolute bottom-0 right-0 bg-[#49A1F5] w-2 h-2 rounded-full border border-white"></div>
)}
</div>
<p className="text-xs">{user.name}</p>
{me?.isAdmin && me?.isControlAllowed && (
<div className="relative">
{/* <Button
variant="secondary"
icon={<MoreIcon />}
onlyIcon
/> */}
{/* <div className="absolute"> */}
<Button
variant="secondary"
icon={<HandOnIcon />}
onlyIcon
onClick={() => transferControl(user.id)}
/>
{/* </div> */}
</div>
)}
</div>
);
}
})}
</div>
</div>
<div className="relative flex-1">
{WSUrl && (
<PixelStreamingWrapper2
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: WSUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
/>
)}
<div className="absolute top-2 left-2 space-y-2">
<div className="relative">
<video
ref={localVideoRef}
className={`aspect-video w-[216px] h-[162px] rounded-lg object-cover bg-gray-500 -scale-x-100 ring-2 ${
isMicEnabled && isSpeaking
? "ring-green-500"
: "ring-transparent"
}`}
playsInline
autoPlay
muted
></video>
<div className="absolute bottom-0 p-2">
<p className="text-sm text-white">{name}</p>
</div>
</div>
{remoteStreams.map(({ peerId, mediaStream }) => (
<Video
key={peerId}
mediaStream={mediaStream}
muted={!permission}
user={users.find((user) => user.peerId === peerId)}
/>
))}
</div>
</div>
<ModalContainer2 />
</div>
);
}
+3 -2
View File
@@ -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<StreamUserState>()(
+6
View File
@@ -0,0 +1,6 @@
interface IRemoteStream {
peerId: string;
mediaStream: MediaStream;
}
export default IRemoteStream;
+5
View File
@@ -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"