upd
This commit is contained in:
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
function ToastContainer() {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default ToastContainer;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -5,6 +5,7 @@ interface IUser {
|
||||
isAdmin: boolean;
|
||||
isControlAllowed: boolean;
|
||||
isMicAllowed: boolean;
|
||||
peerId: string;
|
||||
}
|
||||
|
||||
export default IUser;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user