upd
This commit is contained in:
+223
-207
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user