This commit is contained in:
2024-07-08 17:34:40 +05:00
parent 5c6920843f
commit 37bce16c51
6 changed files with 311 additions and 209 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.1",
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.4",
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.5": "^0.0.12",
"@livekit/components-react": "^2.0.3",
"@livekit/components-styles": "^1.0.10",
-84
View File
@@ -80,90 +80,6 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
}, [isShow]);
return (
// <div className={`max-h-[calc(100vh-48px)]`}>
// <div
// className={`overflow-y-scroll relative h-full bg-white flex flex-col ${
// isShow ? "w-[296px]" : "w-0 hidden"
// }`}
// >
// <div
// className={`fixed z-10 top-0 bg-white w-full flex items-center justify-between p-2 pl-4`}
// >
// <p className="font-semibold">Чат</p>
// <div className="flex">
// <Button
// variant="tertiary"
// icon={<CloseIcon />}
// onlyIcon
// onClick={onClose}
// />
// </div>
// </div>
// <div
// ref={messagesRef}
// className="flex flex-col gap-2 p-4 pt-12 pb-[72px]"
// >
// {messages.map((message) => (
// <div
// className={`flex gap-1 items-end ${
// message.user.id === userId ? "self-end" : ""
// }`}
// >
// {message.user.id !== userId && (
// <div className="">
// <div className="h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
// {message.user.name[0].toUpperCase()}
// </div>
// </div>
// )}
// <div className="relative">
// <div
// className={`p-2 rounded-tl-[4px] rounded-r-lg flex flex-col gap-1 ${
// message.user.id !== userId ? "bg-[#F0F1F2]" : "bg-[#C4DDF5]"
// }`}
// >
// {message.user.id !== userId && (
// <p className="text-sm text-[#49A1F5] font-semibold">
// {message.user.name}
// </p>
// )}
// <p
// className="text-sm break-words"
// style={{ wordBreak: "break-word" }}
// >
// {message.text}
// </p>
// <p className="text-xs self-end text-[#767676]">
// {message.time}
// </p>
// </div>
// {message.user.id !== userId && (
// <div className="absolute bottom-0 -left-[7px]">
// <SubtracktIcon />
// </div>
// )}
// </div>
// </div>
// ))}
// </div>
// <div className="fixed z-10 bottom-0 p-4 bg-white w-full">
// <form onSubmit={sendMessage} className="flex gap-3 border-t pt-3">
// <input
// ref={textRef}
// type="text"
// placeholder="Напишите сообщение..."
// className="outline-none w-full bg-white text-sm"
// value={message}
// onChange={(e) => setMessage(e.target.value)}
// />
// <button type="submit" className="text-[#49A1F5]">
// <SendChatIcon />
// </button>
// </form>
// </div>
// </div>
// </div>
<div
className={`h-full flex flex-col ${
isShow ? "w-[296px] p-4" : "w-0 overflow-hidden"
+14
View File
@@ -16,6 +16,7 @@ function Video({ mediaStream, muted, user }: Props) {
const remoteVideoRef = useRef<HTMLVideoElement>(null);
const isSpeaking = useIsAudioActive({ source: mediaStream });
const [_muted, setMuted] = useState(muted);
const [isLoading, setIsLoading] = useState(true);
function toggleSound() {
if (!remoteVideoRef.current) return;
@@ -30,6 +31,10 @@ function Video({ mediaStream, muted, user }: Props) {
remoteVideoRef.current.onloadedmetadata = () => {
remoteVideoRef.current?.play();
};
remoteVideoRef.current.onplay = () => {
setIsLoading(false);
};
}, [mediaStream]);
useEffect(() => {
@@ -55,6 +60,15 @@ function Video({ mediaStream, muted, user }: Props) {
{_muted ? <SoundOffIcon /> : <SoundOnIcon />}
</button>
</div>
{isLoading && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<img
src="/icons/Loader.png"
alt=""
className="animate-spin w-6 h-6"
/>
</div>
)}
</div>
);
}
@@ -0,0 +1,96 @@
import { Trans } from "react-i18next";
import QRCode from "react-qr-code";
import CloseIcon from "../../icons/CloseIcon";
import LinkIcon from "../../icons/LinkIcon";
import Button from "../../ui/Button";
import useModalStore from "../../../stores/useModalStore";
import { useClipboard } from "use-clipboard-copy";
import { toast, Bounce, ToastContainer } from "react-toastify";
import InfoIcon from "../../icons/InfoBlueIcon";
function InviteModal() {
const { setModal } = useModalStore();
const clipboard = useClipboard();
const link = window.location.origin + window.location.pathname;
function handleClickClipboard() {
clipboard.copy();
toast.info("Ссылка скопирована в буфер обмена", {
icon: <InfoIcon className="text-blue-500" />,
position: "top-center",
autoClose: 3000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
return (
<div className="absolute top-0 lg:left-0 left-12 lg:w-full w-[calc(100vw-48px)] h-full lg:bg-black lg:bg-opacity-50 bg-white sm:border-none border-l border-[#DAE0E5] flex flex-col lg:items-center lg:justify-center">
<div className="bg-white flex flex-col lg:rounded-lg lg:w-[400px] lg:flex-none flex-1">
<div className="p-2 pl-6 flex items-center justify-between border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">
<Trans i18nKey={"invite"}>Пригласить</Trans>
</p>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={() => setModal(null)}
/>
</div>
<div className="py-4 px-6 flex-1 flex flex-col gap-8">
<div className="flex items-center lg:justify-between justify-center gap-8">
<QRCode
size={128}
value={link}
className="rounded-lg p-3 shadow-lg"
/>
<p className="font-semibold text-right">
<Trans i18nKey={"scanQRCode"}>
Отсканируйте QR-код,
<br />
чтобы присоедениться
<br />к демонстрации
</Trans>
</p>
</div>
<div className="">
<Button
icon={<LinkIcon />}
fullWidth
large
onClick={handleClickClipboard}
>
<Trans i18nKey={"copyLinkToConnect"}>
Скопировать ссылку для подключения
</Trans>
</Button>
<input ref={clipboard.target} type="hidden" value={link} />
</div>
{/* <div>
<form onSubmit={(e) => e.preventDefault()} className="flex gap-2">
<input
type="email"
placeholder="Email"
className="text-sm bg-transparent border border-[#DAE0E5] rounded-lg px-2 pb-0.5 outline-none h-10 w-full"
/>
<Button type="submit" large className="" disabled>
<Trans i18nKey={"invite"}>Пригласить</Trans>
</Button>
</form>
</div> */}
</div>
</div>
<ToastContainer />
</div>
);
}
export default InviteModal;
+196 -120
View File
@@ -24,6 +24,13 @@ 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 { Trans } from "react-i18next";
import { isIOS } from "react-device-detect";
import WindowIcon from "../components/icons/WindowIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
import ShareIcon from "../components/icons/ShareIcon";
import { useFullscreen } from "ahooks";
import InviteModal from "../components/modals/stream/InviteModal";
// import MoreIcon from "../components/icons/MoreIcon";
@@ -55,6 +62,10 @@ function StreamPage3() {
const { name } = useStreamStore();
const [isMicEnabled, setIsMicEnabled] = useState(true);
const [isCameraEnabled, setIsCameraEnabled] = useState(true);
const [isEnded, setIsEnded] = useState<boolean>();
const [, setEndAt] = useState<Date>();
const fullscreenRef = useRef(null);
const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef);
async function startCall(remotePeerId: string) {
if (!peerInstance) return;
@@ -152,6 +163,10 @@ function StreamPage3() {
setMe(users.find((user) => user.id === userId));
});
socket.on("request-control", (userId) => {
console.log("request-control", userId);
});
socket.on("connect", () => {
setSocket(socket);
});
@@ -192,37 +207,59 @@ function StreamPage3() {
}, 500);
}
async function getActiveSession() {
const activeSession: any = await api
.get(`activeSessions/${params.id}`)
.json();
// if (activeSession?.endAt) {
// setEndAt(activeSession.endAt);
// }
return activeSession;
}
async function getWSUrl() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
setIsEnded(false);
setWSUrl(
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.name}/${activeSession.cirrusPort}/`
);
setModal(<SetNameModal onAction={getUserMedia} />);
checkSessionStatus();
}
function transferControl(userId: string) {
socket?.emit("transfer-control", userId);
}
function requestControl(userId: string) {
console.log("requestControl func", userId);
socket?.emit("request-control", userId);
}
async function getActiveSession() {
const activeSession: any = await api
.get(`activeSessions/${params.id}`)
.json();
if (activeSession?.endAt) {
setEndAt(activeSession.endAt);
}
return activeSession;
}
async function checkSessionStatus() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
setTimeout(async () => {
await checkSessionStatus();
}, 1000);
}
useEffect(() => {
getWSUrl();
setModal(<SetNameModal onAction={getUserMedia} />);
}, []);
useEffect(() => {
@@ -244,123 +281,162 @@ function StreamPage3() {
}, [users.length]);
return (
<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 ref={fullscreenRef} className="h-[100dvh] flex flex-col bg-[#111C26]">
{isEnded === false ? (
<>
<div className="flex items-center bg-white h-12 px-6">
<div className="pr-6">
<img src="/images/logo24.svg" alt="" />
</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)}
/>
<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)
: requestControl(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>
<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
{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 className="absolute"> */}
<Button
variant="secondary"
icon={<HandOnIcon />}
onlyIcon
onClick={() => transferControl(user.id)}
/>
{/* </div> */}
</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 className="flex gap-2 ml-auto">
<Button
variant="secondary"
icon={<ShareIcon />}
onlyIcon
onClick={() => setModal(<InviteModal />)}
/>
{!isIOS && (
<Button
variant="secondary"
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
onlyIcon
onClick={toggleFullscreen}
/>
)}
</div>
</div>
{remoteStreams.map(({ peerId, mediaStream }) => (
<Video
key={peerId}
mediaStream={mediaStream}
muted={!permission}
user={users.find((user) => user.peerId === peerId)}
/>
))}
</div>
</div>
<div className="relative flex-1 flex">
{WSUrl && (
<PixelStreamingWrapper2
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: WSUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
/>
)}
<ModalContainer2 />
{!users.find((user) => user.id === userId)?.isControlAllowed && (
<div
className="absolute top-0 left-0 w-full h-full"
onClick={() => alert("")}
></div>
)}
<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 className="flex-1 flex items-center justify-center p-8">
<p className="text-2xl text-white font-gilroy text-center">
<Trans i18nKey={"demonstrationCompleted"}>
Данная демонстрация была завершена
</Trans>
</p>
</div>
)}
</div>
);
}
+4 -4
View File
@@ -36,10 +36,10 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.1.tgz#e93c13942592cf5ef01aa8297444dc192beee52f"
integrity sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/-/lib-pixelstreamingfrontend-ue5.3-1.0.1.tgz#ba7d0fb42ede74109fcbb2510d7f6a4442bed7a0"
integrity sha512-DLeMbwi/szf4/rQAPXFl1YH5lT5kHJ2GcnxYNPvYVWZ9xgX5hjieREXyi8DaxCAPF81e+ev57TjZKJy7R3tvpw==
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/-/lib-pixelstreamingfrontend-ue5.3-1.0.4.tgz#71533a4f940627702d26896eb3380d08c6d05523"
integrity sha512-gqt2lFGLys3YOvloK9X/gpk+ZPgRlLi+uB9kWWWMJd+Gih0jgRwc6GJ0b+LuIVmZau4qNUbTmfOklKpti0G07g==
dependencies:
sdp "^3.1.0"