This commit is contained in:
2024-07-03 18:57:18 +05:00
parent bf669a4d59
commit 9a8998501f
17 changed files with 515 additions and 260 deletions
+4
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.1",
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.5": "^0.0.12",
"@livekit/components-react": "^2.0.3",
"@livekit/components-styles": "^1.0.10",
"@uidotdev/usehooks": "^2.4.1",
@@ -20,6 +21,7 @@
"i18next-browser-languagedetector": "^7.2.0",
"ky": "^1.1.3",
"livekit-client": "^1.13.2",
"peerjs": "^1.5.4",
"react": "^18.2.0",
"react-calendar": "^4.3.0",
"react-countdown": "^2.3.5",
@@ -38,12 +40,14 @@
"socket.io-client": "^4.7.4",
"ua-parser-js": "^1.0.35",
"use-clipboard-copy": "^0.2.0",
"use-is-audio-active": "^1.0.0",
"usehooks-ts": "^3.0.1",
"uuid": "^9.0.1",
"zustand": "^4.3.9"
},
"devDependencies": {
"@types/node": "^20.11.17",
"@types/peerjs": "^1.1.0",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-input-mask": "^3.0.2",
+10 -5
View File
@@ -31,11 +31,12 @@ function App() {
state.setIsOpen,
]);
const [loading, setLoading] = useState<boolean>(false);
const [countdownTimer, setCountdownTimer] = useState(15);
const [countdownTimer, setCountdownTimer] = useState(10);
const { t, i18n } = useTranslation();
const build = searchParams.get("build") || null;
const type = searchParams.get("type") || "demo";
const endAt = searchParams.get("endAt");
const [streamUrl, setStreamUrl] = useState<string>();
function toastError(text: string) {
toast.error(text, {
@@ -95,13 +96,11 @@ function App() {
.json();
if (response.stream) {
setStreamUrl(`/stream/${response.stream}`);
setInterval(() => {
setCountdownTimer((prev) => prev - 1);
}, 1000);
setTimeout(() => {
navigate(`/stream/${response.stream}`);
}, 15000);
} else if (response.error) {
toastError(response.error);
setLoading(false);
@@ -117,6 +116,12 @@ function App() {
}
}
useEffect(() => {
if (countdownTimer > 0 || !streamUrl) return;
navigate(streamUrl);
}, [countdownTimer]);
useEffect(() => {
getLang();
+1 -3
View File
@@ -28,7 +28,6 @@ import "react-toastify/dist/ReactToastify.css";
import AlertIcon from "./components/icons/AlertIcon";
import useSocketStore from "./stores/useSocketStore";
import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
import ToggleMic from "./components/ToggleMic";
import Chat from "./components/Chat";
// import AFKTimerModal from "./components/modals/AFKTimerModal";
import { differenceInMilliseconds, format, parseISO } from "date-fns";
@@ -380,13 +379,12 @@ function StreamPage() {
</button> */}
<LiveKitRoom
video={false}
video={true}
audio={true}
token={token}
serverUrl={livekitServerUrl}
>
<RoomAudioRenderer />
<ToggleMic socket={socket} handleUpdate={update} />
</LiveKitRoom>
<div className="relative group outline-none w-10 h-10 bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90 flex justify-center items-center">
+91
View File
@@ -0,0 +1,91 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable no-empty */
import { useEffect, useRef, useState } from "react";
import {
Config,
AllSettings,
PixelStreaming,
} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.5";
export interface PixelStreamingWrapperProps {
initialSettings?: Partial<AllSettings>;
}
export const PixelStreamingWrapper2 = ({
initialSettings,
}: PixelStreamingWrapperProps) => {
// A reference to parent div element that the Pixel Streaming library attaches into:
const videoParent = useRef<HTMLDivElement>(null);
// Pixel streaming library instance is stored into this state variable after initialization:
const [pixelStreaming, setPixelStreaming] = useState<PixelStreaming>();
// A boolean state variable that determines if the Click to play overlay is shown:
const [clickToPlayVisible, setClickToPlayVisible] = useState(false);
// Run on component mount:
useEffect(() => {
if (videoParent.current) {
// Attach Pixel Streaming library to videoParent element:
const config = new Config({ initialSettings });
const streaming = new PixelStreaming(config, {
videoElementParent: videoParent.current,
});
// register a playStreamRejected handler to show Click to play overlay if needed:
streaming.addEventListener("playStreamRejected", () => {
setClickToPlayVisible(true);
});
// Save the library instance into component state so that it can be accessed later:
setPixelStreaming(streaming);
// Clean up on component unmount:
return () => {
try {
streaming.disconnect();
} catch {}
};
}
}, []);
return (
<div
style={{
width: "100%",
height: "100%",
position: "relative",
}}
>
<div
style={{
width: "100%",
height: "100%",
}}
ref={videoParent}
/>
{clickToPlayVisible && (
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
}}
onClick={() => {
pixelStreaming?.play();
setClickToPlayVisible(false);
}}
>
<div>Click to play</div>
</div>
)}
</div>
);
};
-13
View File
@@ -10,7 +10,6 @@ import { Trans } from "react-i18next";
import i18n from "../i18n";
import { useState } from "react";
import LoaderIcon from "./icons/LoaderIcon";
import api from "../utils/api";
function SidebarTab4() {
const {
@@ -28,17 +27,6 @@ function SidebarTab4() {
const [isLoading, setIsLoading] = useState<boolean>(false);
async function sendInvite(email: string, link: string) {
try {
const reuslt: any = await api
.post("sendInvite", { json: { email, link } })
.json();
console.log("reuslt", reuslt);
} catch (error) {
console.log({ error: (error as Error).message });
}
}
async function handleClickSignUp() {
if (!selectedTime || !selectedDay) {
return;
@@ -63,7 +51,6 @@ function SidebarTab4() {
})
.json();
sendInvite(email, result.url);
setUrl(result.url);
setCurrentTab(currentTab + 1);
setIsLoading(false);
-5
View File
@@ -1,5 +0,0 @@
function ToastContainer() {
return <div></div>;
}
export default ToastContainer;
-69
View File
@@ -1,69 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import { useRoomContext } from "@livekit/components-react";
import { Track } from "livekit-client";
import { useEffect, useState } from "react";
import MicroOnIcon from "./icons/MicroOnIcon";
import MicroOffIcon from "./icons/MicroOffIcon";
function ToggleMic({ socket, handleUpdate }: any) {
const room = useRoomContext();
const [muted, setMuted] = useState(true);
function toggleMic(value: boolean) {
const audioTrack = room.localParticipant.getTrack(Track.Source.Microphone);
if (!audioTrack) return;
if (value) {
audioTrack.mute();
} else {
audioTrack.unmute();
}
setMuted(!audioTrack.isMuted);
}
useEffect(() => {
if (!socket) return;
socket.on("update", (socketId: string, data: { [key: string]: any }) => {
if (
socket.id === socketId &&
Object.prototype.hasOwnProperty.call(data, "muted")
) {
toggleMic(data.muted);
}
});
}, [socket]);
useEffect(() => {
// room.on(RoomEvent.TrackMuted, (_, participant) => {
// console.log(participant);
// });
// room.on(RoomEvent.TrackUnmuted, (_, participant) => {
// console.log(participant);
// });
room.on("localTrackPublished", () => {
room.localParticipant.getTrack(Track.Source.Microphone)?.mute();
});
}, []);
return (
<button
onClick={() =>
handleUpdate(room.localParticipant.identity, {
muted: !room.localParticipant.getTrack(Track.Source.Microphone)
?.isMuted,
})
}
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
>
{!muted ? <MicroOnIcon /> : <MicroOffIcon />}
</button>
);
}
export default ToggleMic;
+35
View File
@@ -0,0 +1,35 @@
import { useEffect, useRef } from "react";
import useIsAudioActive from "use-is-audio-active";
interface Props {
mediaStream: MediaStream | null;
muted: boolean;
}
function Video({ mediaStream, muted }: Props) {
const remoteVideoRef = useRef<HTMLVideoElement>(null);
const isSpeaking = useIsAudioActive({ source: mediaStream });
useEffect(() => {
if (!remoteVideoRef.current) return;
remoteVideoRef.current.srcObject = mediaStream;
remoteVideoRef.current.onloadedmetadata = () => {
remoteVideoRef.current?.play();
};
}, [mediaStream]);
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>
);
}
export default Video;
+20
View File
@@ -0,0 +1,20 @@
function CameraOffIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<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"
/>
</svg>
);
}
export default CameraOffIcon;
+20
View File
@@ -0,0 +1,20 @@
function CameraOnIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<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"
/>
</svg>
);
}
export default CameraOnIcon;
+1 -1
View File
@@ -8,7 +8,7 @@ import App from "./App";
import HistoryPage from "./HistoryPage";
import ScheduledPage from "./ScheduledPage";
import StreamPage2 from "./pages/StreamPage2";
// import StreamPage from "./StreamPage";
// import StreamPage3 from "./pages/StreamPage3";
const router = createBrowserRouter([
{
+197 -164
View File
@@ -11,7 +11,7 @@ import { Transition } from "react-transition-group";
import Button from "../components/ui/Button";
// import CloseIcon from "../components/icons/CloseIcon";
import HandOffIcon from "../components/icons/HandOffIcon";
import MicroOffIcon from "../components/icons/MicroOffIcon";
// import MicroOffIcon from "../components/icons/MicroOffIcon";
import PersonsIcon from "../components/icons/PersonsIcon";
import MicroOnIcon from "../components/icons/MicroOnIcon";
// import MoreIcon from "../components/icons/MoreIcon";
@@ -41,15 +41,6 @@ import DesktopIcon from "../components/icons/DesktopIcon";
import { format } from "date-fns";
import IUser from "../types/IUser";
import IMessage from "../types/IMessage";
import ky from "ky";
import {
RemoteParticipant,
RemoteTrack,
RemoteTrackPublication,
Room,
RoomEvent,
Track,
} from "livekit-client";
import InfoIcon from "../components/icons/InfoIcon";
import Rotate64Icon from "../components/icons/Rotate64Icon";
import { useClipboard } from "use-clipboard-copy";
@@ -57,6 +48,13 @@ import QRCode from "react-qr-code";
import Star12Icon from "../components/icons/Star12Icon";
import { Trans, useTranslation } from "react-i18next";
import Countdown from "react-countdown";
import useStreamStore from "../stores/useStreamStore";
import Peer from "peerjs";
import Video from "../components/Video";
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";
const renderer = ({ minutes, seconds }: any) => {
return (
@@ -74,7 +72,7 @@ function StreamPage2() {
const [socket, setSocket] = useState<Socket>();
const [wsUrl, setWsUrl] = useState<string>();
const [isEnded, setIsEnded] = useState<boolean>(false);
const [name, setName] = useState<string>("");
const { name, setName } = useStreamStore();
const [userId] = useState(uuidv4());
const [step, setStep] = useState<number>(1);
const nameRef = useRef<HTMLInputElement>(null!);
@@ -86,8 +84,6 @@ function StreamPage2() {
const [isShowChat, setIsShowChat] = useState<boolean>(false);
const [isShowUsers, setIsShowUsers] = useState<boolean>(false);
const [isShowInviteModal, setIsShowInviteModal] = useState(false);
const [liveKitRoom, setLiveKitRoom] = useState<Room>();
const [isMicEnabled, setIsMicEnabled] = useState<boolean>(false);
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
const [messageText, setMessageText] = useState("");
const [messages, setMessages] = useState<IMessage[]>([]);
@@ -98,6 +94,21 @@ function StreamPage2() {
const link = window.location.origin + window.location.pathname;
const [anyNewMessages, setAnyNewMessages] = useState(false);
const [endAt, setEndAt] = useState();
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 [permission, setPermission] = useState<boolean>();
const [remoteStreams, setRemoteStreams] = useState<any[]>([]);
// const [errorMessage, setErrorMessage] = useState<string>("");
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 } =
@@ -125,17 +136,6 @@ function StreamPage2() {
});
}
function handleTrackSubscribed(
track: RemoteTrack,
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
console.log(track, publication, participant);
const element = track.attach();
parentElementRef.current?.append(element);
}
async function sendMessage(e: FormEvent) {
e.preventDefault();
@@ -204,6 +204,7 @@ function StreamPage2() {
name: name,
device: isMobile ? "mobile" : "desktop",
isAdmin: searchParams.has("admin", true),
peerId,
},
},
})
@@ -224,6 +225,7 @@ function StreamPage2() {
name: i18n.language === "ru" ? "Гость" : "Guest",
device: isMobile ? "mobile" : "desktop",
isAdmin: searchParams.has("admin", true),
peerId,
},
},
})
@@ -232,78 +234,73 @@ function StreamPage2() {
setStep(2);
}
async function mute() {
const result = await liveKitRoom?.localParticipant
?.getTrack(Track.Source.Microphone)
?.mute();
if (!result) return;
setIsMicEnabled(false);
}
async function unmute() {
const result = await liveKitRoom?.localParticipant
?.getTrack(Track.Source.Microphone)
?.unmute();
if (!result) return;
setIsMicEnabled(true);
}
async function allowMic() {
if (!liveKitRoom) return;
async function getPermission() {
try {
const result = await liveKitRoom.localParticipant.setMicrophoneEnabled(
true
);
console.log("result", result);
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
setIsMicEnabled(
!liveKitRoom?.localParticipant?.getTrack(Track.Source.Microphone)
?.isMuted
);
setStep(3);
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) {
if (error instanceof Error) {
if (error.message === "Requested device not found") {
toast.warn(
"Устройство ввода не найдено. Подключите микрофон или нажмите кнопку «Пропустить»",
{
icon: <InfoIcon className="text-[#F2994A]" />,
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
}
);
}
const mediaStream = new MediaStream();
if (error.message === "Permission denied") {
toast.warn(
"Вы заблокировали доступ к микрофону в настройках. Разрешите доступ в настройках вашего браузера и попробуйте снова.",
{
icon: <InfoIcon className="text-[#F2994A]" />,
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
}
);
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 }]);
});
}
}
@@ -311,6 +308,24 @@ function StreamPage2() {
setStep(3);
}
function toggleVideo() {
mediaStreamInstance.current!.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
if (!permission) return;
setIsVideoEnabled(track.enabled);
});
}
function toggleAudio() {
mediaStreamInstance.current!.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
if (!permission) return;
setIsAudioEnabled(track.enabled);
});
}
function transferControl(userId: string) {
socket?.emit("transferControl", userId);
}
@@ -336,32 +351,46 @@ function StreamPage2() {
socket?.emit("kickUser", userId);
}
async function getLiveKitToken() {
const { token }: any = await ky
.get(
`https://coord.graff.tech/getToken?roomName=${params.id}&participantName=${userId}`
)
.json();
return token;
}
async function liveKitConnect() {
const liveKitWsUrl = "wss://livekit.stream.graff.tech";
const liveKitToken = await getLiveKitToken();
const room = new Room();
room.on(RoomEvent.TrackSubscribed, handleTrackSubscribed);
await room.connect(liveKitWsUrl, liveKitToken);
setLiveKitRoom(room);
}
useEffect(() => {
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 }]);
});
});
peerInstance.current = peer;
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]);
@@ -369,7 +398,7 @@ function StreamPage2() {
useEffect(() => {
if (!name) return;
setName(() => name.trim());
setName(name.trim());
}, [name]);
useEffect(() => {
@@ -397,6 +426,11 @@ function StreamPage2() {
socket.on("update", (roomUsers: IUser[]) => {
setUsers(roomUsers);
// for (const user of roomUsers) {
// setRemoteStreams(
// remoteStreams.filter(({ peerId }) => peerId === user.peerId)
// );
// }
});
socket.on("message", (message: IMessage) => {
@@ -444,24 +478,8 @@ function StreamPage2() {
window.location.reload();
}
});
liveKitConnect();
}, [socket]);
useEffect(() => {
console.log(liveKitRoom);
if (!liveKitRoom) return;
liveKitRoom.on(RoomEvent.TrackMuted, (_, participant) => {
console.log(participant);
});
liveKitRoom.on(RoomEvent.TrackUnmuted, (_, participant) => {
console.log(participant);
});
}, [liveKitRoom]);
useEffect(() => {
if (!isMobile) return;
if (isShowUsers) {
@@ -535,12 +553,17 @@ function StreamPage2() {
<Tooltip text="Запросить управление" />
</Button>
)}
<Button
variant="secondary"
icon={isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
icon={isVideoEnabled ? <CameraOnIcon /> : <CameraOffIcon />}
onlyIcon
onClick={() => (isMicEnabled ? mute() : unmute())}
onClick={toggleVideo}
/>
<Button
variant="secondary"
icon={isAudioEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={toggleAudio}
/>
</div>
<div className="w-px h-4 bg-[#DAE0E5]"></div>
@@ -678,9 +701,9 @@ function StreamPage2() {
<hr className="bg-[#DAE0E5] w-4" />
<Button
variant="secondary"
icon={isMicEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
icon={isAudioEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={() => (isMicEnabled ? mute() : unmute())}
onClick={toggleAudio}
/>
</div>
<div className="">
@@ -746,6 +769,31 @@ function StreamPage2() {
}
></div>
)}
<div className="absolute top-2 lg:left-2 lg:right-auto right-2 flex flex-col gap-2">
<video
ref={videoRef}
className={`aspect-video bg-black rounded-lg -scale-x-100 object-cover ring-2 ${
isAudioEnabled && isSpeaking
? "ring-green-500"
: "ring-transparent"
} ${
permission
? "lg:w-[216px] lg:h-[162px] w-[112px] h-[84px]"
: "w-0 h-0"
}`}
playsInline
autoPlay
muted
/>
{remoteStreams.map(({ remoteStream }, index) => (
<Video
key={index}
mediaStream={remoteStream}
muted={!permission}
/>
))}
</div>
</div>
{(isShowUsers || isShowChat) && (
@@ -1074,46 +1122,31 @@ function StreamPage2() {
</div>
<p className="text-sm">
<Trans i18nKey={"allowMicrophoneUse"}>
Разрешите использование микрофона
Разрешите использование камеры и микрофона
</Trans>
</p>
</div>
<div className="mb-10">
<p className="text-[#77828C] text-xs">
<Trans i18nKey={"turnOffMicrophone"}>
Выключить микрофон можно в любой момент
Выключить камеру и микрофон можно в любой момент
</Trans>
</p>
</div>
</div>
{liveKitRoom ? (
<div className="flex gap-2">
<Button
variant="secondary"
fullWidth
large
onClick={disallowMic}
>
<Trans i18nKey={"skip"}>Пропустить</Trans>
</Button>
<Button fullWidth large onClick={allowMic}>
<Trans i18nKey={"allow"}>Разрешить</Trans>
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<img
src="/icons/LoaderPrimary.png"
alt=""
className="w-8 h-8 animate-spin"
/>
<span className="text-sm text-[#77828C]">
<Trans i18nKey={"connectingToVoiceServer"}>
Подключение к голосовому серверу
</Trans>
</span>
</div>
)}
<div className="flex gap-2">
<Button
variant="secondary"
fullWidth
large
onClick={disallowMic}
>
<Trans i18nKey={"skip"}>Пропустить</Trans>
</Button>
<Button fullWidth large onClick={getPermission}>
<Trans i18nKey={"allow"}>Разрешить</Trans>
</Button>
</div>
</div>
)}
</Transition>
+58
View File
@@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import { PixelStreamingWrapper2 } from "../components/PixelStreamingWrapper2";
import api from "../utils/api";
import { useParams } from "react-router-dom";
function StreamPage3() {
const params = useParams();
const [WSUrl, setWSUrl] = useState<string>("");
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") {
return;
}
setWSUrl(
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.name}/${activeSession.cirrusPort}/`
);
}
useEffect(() => {
getWSUrl();
}, []);
return (
<div className="h-screen">
{WSUrl && (
<PixelStreamingWrapper2
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: WSUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
/>
)}
</div>
);
}
export default StreamPage3;
+26
View File
@@ -0,0 +1,26 @@
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
interface State {
name: string;
}
interface Actions {
setName: (name: string) => void;
}
const useStreamStore = create<State & Actions>()(
devtools(
persist(
(set) => ({
name: "",
setName: (name) => set({ name }),
}),
{
name: "auth",
}
)
)
);
export default useStreamStore;
View File
+1
View File
@@ -5,6 +5,7 @@ interface IUser {
isAdmin: boolean;
isControlAllowed: boolean;
isMicAllowed: boolean;
peerId: string;
}
export default IUser;
+51
View File
@@ -43,6 +43,13 @@
dependencies:
sdp "^3.1.0"
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.5@^0.0.12":
version "0.0.12"
resolved "https://registry.yarnpkg.com/@epicgames-ps/lib-pixelstreamingfrontend-ue5.5/-/lib-pixelstreamingfrontend-ue5.5-0.0.12.tgz#8840133405b811d12fd7ac79bec1b75db274b743"
integrity sha512-vLJTrjuxhG+TqDhTK+ETwEmN0jSorBMdKG5AH2C01jL6IRFI21ipxD8lQ9ySq9vnfjXmhh036pEtJwR5DlY6sQ==
dependencies:
sdp "^3.1.0"
"@esbuild/android-arm64@0.18.20":
version "0.18.20"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
@@ -293,6 +300,11 @@
resolved "https://registry.yarnpkg.com/@livekit/components-styles/-/components-styles-1.0.10.tgz#41becdb7649629e586daea02b0b8bf4375f75ccf"
integrity sha512-WCTtXnMAcZiXgo2N+1SmlcpPwltpGp8fW+x83P9OnHHTK3qrKEzfcfKi1z0xS8VMu/KKLgJm+yhAlCmZPme/RA==
"@msgpack/msgpack@^2.8.0":
version "2.8.0"
resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-2.8.0.tgz#4210deb771ee3912964f14a15ddfb5ff877e70b9"
integrity sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -437,6 +449,13 @@
dependencies:
undici-types "~5.26.4"
"@types/peerjs@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/peerjs/-/peerjs-1.1.0.tgz#822962d78b26dc43c113fac0a8bf653e12851487"
integrity sha512-dVocsfYFg5QQuUB9OAxfrSvz4br4pyX+7M61ZJSRiYtE3NdayShk1p1Y8b9TmCj724TwHskVraeF7wyPl0rYcg==
dependencies:
peerjs "*"
"@types/prop-types@*":
version "15.7.11"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
@@ -1095,6 +1114,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eventemitter3@^4.0.7:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@@ -1849,6 +1873,21 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
peerjs-js-binarypack@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/peerjs-js-binarypack/-/peerjs-js-binarypack-2.1.0.tgz#f0fc822d3cb54ab1022f4bd580308475e8f77b70"
integrity sha512-YIwCC+pTzp3Bi8jPI9UFKO0t0SLo6xALnHkiNt/iUFmUUZG0fEEmEyFKvjsDKweiFitzHRyhuh6NvyJZ4nNxMg==
peerjs@*, peerjs@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/peerjs/-/peerjs-1.5.4.tgz#bcf933406d07fad9b2a34ae2e8215ba3f1878672"
integrity sha512-yFsoLMnurJKlQbx6kVSBpOp+AlNldY1JQS2BrSsHLKCZnq6t7saHleuHM5svuLNbQkUJXHLF3sKOJB1K0xulOw==
dependencies:
"@msgpack/msgpack" "^2.8.0"
eventemitter3 "^4.0.7"
peerjs-js-binarypack "^2.1.0"
webrtc-adapter "^9.0.0"
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -2471,6 +2510,11 @@ use-clipboard-copy@^0.2.0:
dependencies:
clipboard-copy "^3.0.0"
use-is-audio-active@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/use-is-audio-active/-/use-is-audio-active-1.0.0.tgz#4e0fcb99c9d75782da3f2e2cf7f7da00bf5da397"
integrity sha512-8GZaxCe7DaBi1VeDDiHY9+aO1vCb0OCQb6+Gwf3j46Nxo/sITcgPmKKt0TiJVgbDlbEVP4RWu9krpadA57V6Ag==
use-sync-external-store@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
@@ -2530,6 +2574,13 @@ webrtc-adapter@^8.1.1:
dependencies:
sdp "^3.2.0"
webrtc-adapter@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz#d4efa22ca9604cb2c8cdb9e492815ba37acfa0b2"
integrity sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==
dependencies:
sdp "^3.2.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"