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" "preview": "vite preview"
}, },
"dependencies": { "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", "@epicgames-ps/lib-pixelstreamingfrontend-ue5.5": "^0.0.12",
"@livekit/components-react": "^2.0.3", "@livekit/components-react": "^2.0.3",
"@livekit/components-styles": "^1.0.10", "@livekit/components-styles": "^1.0.10",
-84
View File
@@ -80,90 +80,6 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
}, [isShow]); }, [isShow]);
return ( 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 <div
className={`h-full flex flex-col ${ className={`h-full flex flex-col ${
isShow ? "w-[296px] p-4" : "w-0 overflow-hidden" 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 remoteVideoRef = useRef<HTMLVideoElement>(null);
const isSpeaking = useIsAudioActive({ source: mediaStream }); const isSpeaking = useIsAudioActive({ source: mediaStream });
const [_muted, setMuted] = useState(muted); const [_muted, setMuted] = useState(muted);
const [isLoading, setIsLoading] = useState(true);
function toggleSound() { function toggleSound() {
if (!remoteVideoRef.current) return; if (!remoteVideoRef.current) return;
@@ -30,6 +31,10 @@ function Video({ mediaStream, muted, user }: Props) {
remoteVideoRef.current.onloadedmetadata = () => { remoteVideoRef.current.onloadedmetadata = () => {
remoteVideoRef.current?.play(); remoteVideoRef.current?.play();
}; };
remoteVideoRef.current.onplay = () => {
setIsLoading(false);
};
}, [mediaStream]); }, [mediaStream]);
useEffect(() => { useEffect(() => {
@@ -55,6 +60,15 @@ function Video({ mediaStream, muted, user }: Props) {
{_muted ? <SoundOffIcon /> : <SoundOnIcon />} {_muted ? <SoundOffIcon /> : <SoundOnIcon />}
</button> </button>
</div> </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> </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 MicroOffIcon from "../components/icons/MicroOffIcon";
import CameraOnIcon from "../components/icons/CameraOnIcon"; import CameraOnIcon from "../components/icons/CameraOnIcon";
import CameraOffIcon from "../components/icons/CameraOffIcon"; 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"; // import MoreIcon from "../components/icons/MoreIcon";
@@ -55,6 +62,10 @@ function StreamPage3() {
const { name } = useStreamStore(); const { name } = useStreamStore();
const [isMicEnabled, setIsMicEnabled] = useState(true); const [isMicEnabled, setIsMicEnabled] = useState(true);
const [isCameraEnabled, setIsCameraEnabled] = 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) { async function startCall(remotePeerId: string) {
if (!peerInstance) return; if (!peerInstance) return;
@@ -152,6 +163,10 @@ function StreamPage3() {
setMe(users.find((user) => user.id === userId)); setMe(users.find((user) => user.id === userId));
}); });
socket.on("request-control", (userId) => {
console.log("request-control", userId);
});
socket.on("connect", () => { socket.on("connect", () => {
setSocket(socket); setSocket(socket);
}); });
@@ -192,37 +207,59 @@ function StreamPage3() {
}, 500); }, 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() { async function getWSUrl() {
const activeSession = await getActiveSession(); const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") { if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return; return;
} }
setIsEnded(false);
setWSUrl( setWSUrl(
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.name}/${activeSession.cirrusPort}/` `wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.name}/${activeSession.cirrusPort}/`
); );
setModal(<SetNameModal onAction={getUserMedia} />);
checkSessionStatus();
} }
function transferControl(userId: string) { function transferControl(userId: string) {
socket?.emit("transfer-control", userId); 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(() => { useEffect(() => {
getWSUrl(); getWSUrl();
setModal(<SetNameModal onAction={getUserMedia} />);
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -244,123 +281,162 @@ function StreamPage3() {
}, [users.length]); }, [users.length]);
return ( return (
<div className="h-screen flex flex-col bg-[#111C26]"> <div ref={fullscreenRef} className="h-[100dvh] flex flex-col bg-[#111C26]">
<div className="flex items-center bg-white h-12"> {isEnded === false ? (
<div className="px-6"> <>
<img src="/images/logo24.svg" alt="" /> <div className="flex items-center bg-white h-12 px-6">
</div> <div className="pr-6">
<div className="flex items-center gap-4"> <img src="/images/logo24.svg" alt="" />
<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> </div>
<p className="text-xs">{name}</p> <div className="flex items-center gap-4">
</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<Button <p className="text-xs font-semibold">
variant="secondary" {name[0]?.toUpperCase()}
icon={me?.isControlAllowed ? <HandOnIcon /> : <HandOffIcon />} </p>
onlyIcon {me?.isControlAllowed && (
onClick={() => me?.isAdmin && transferControl(me.id)} <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 {me?.isAdmin && me?.isControlAllowed && (
variant="secondary" <div className="relative">
icon={isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />} {/* <Button
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" variant="secondary"
icon={<MoreIcon />} icon={<MoreIcon />}
onlyIcon onlyIcon
/> */} /> */}
{/* <div className="absolute"> */} {/* <div className="absolute"> */}
<Button <Button
variant="secondary" variant="secondary"
icon={<HandOnIcon />} icon={<HandOnIcon />}
onlyIcon onlyIcon
onClick={() => transferControl(user.id)} onClick={() => transferControl(user.id)}
/> />
{/* </div> */} {/* </div> */}
</div>
)}
</div> </div>
)} );
</div> }
); })}
} </div>
})} <div className="flex gap-2 ml-auto">
</div> <Button
</div> variant="secondary"
<div className="relative flex-1"> icon={<ShareIcon />}
{WSUrl && ( onlyIcon
<PixelStreamingWrapper2 onClick={() => setModal(<InviteModal />)}
initialSettings={{ />
AutoPlayVideo: true, {!isIOS && (
AutoConnect: true, <Button
ss: WSUrl, variant="secondary"
StartVideoMuted: true, icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
HoveringMouse: true, onlyIcon
WaitForStreamer: true, onClick={toggleFullscreen}
}} />
/> )}
)}
<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>
</div> </div>
{remoteStreams.map(({ peerId, mediaStream }) => ( <div className="relative flex-1 flex">
<Video {WSUrl && (
key={peerId} <PixelStreamingWrapper2
mediaStream={mediaStream} initialSettings={{
muted={!permission} AutoPlayVideo: true,
user={users.find((user) => user.peerId === peerId)} AutoConnect: true,
/> ss: WSUrl,
))} StartVideoMuted: true,
</div> HoveringMouse: true,
</div> 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> </div>
); );
} }
+4 -4
View File
@@ -36,10 +36,10 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.1.tgz#e93c13942592cf5ef01aa8297444dc192beee52f" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.1.tgz#e93c13942592cf5ef01aa8297444dc192beee52f"
integrity sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg== integrity sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3@^1.0.1": "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3@^1.0.4":
version "1.0.1" version "1.0.4"
resolved "https://registry.yarnpkg.com/@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/-/lib-pixelstreamingfrontend-ue5.3-1.0.1.tgz#ba7d0fb42ede74109fcbb2510d7f6a4442bed7a0" resolved "https://registry.yarnpkg.com/@epicgames-ps/lib-pixelstreamingfrontend-ue5.3/-/lib-pixelstreamingfrontend-ue5.3-1.0.4.tgz#71533a4f940627702d26896eb3380d08c6d05523"
integrity sha512-DLeMbwi/szf4/rQAPXFl1YH5lT5kHJ2GcnxYNPvYVWZ9xgX5hjieREXyi8DaxCAPF81e+ev57TjZKJy7R3tvpw== integrity sha512-gqt2lFGLys3YOvloK9X/gpk+ZPgRlLi+uB9kWWWMJd+Gih0jgRwc6GJ0b+LuIVmZau4qNUbTmfOklKpti0G07g==
dependencies: dependencies:
sdp "^3.1.0" sdp "^3.1.0"