Update environment configuration and enhance control features in WebRTC
- Changed VITE_API_URL and VITE_WEBRTC_URL in .env to point to local IP addresses. - Added react-hot-toast for user notifications in the application. - Integrated toast notifications for control acquisition in SessionPage. - Enhanced PixelStreamingWrapper and SessionUsersPanel to manage control states for participants. - Implemented grant and revoke control functionalities in the WebRTC service, allowing dynamic control management among users. - Updated various components to reflect control states and improve user experience during sessions.
This commit is contained in:
+2
-2
@@ -1,4 +1,4 @@
|
||||
# VITE_API_URL=http://localhost:3000
|
||||
# VITE_WEBRTC_URL=http://localhost:3001
|
||||
# VITE_API_URL=http://192.168.1.136:3000
|
||||
# VITE_WEBRTC_URL=http://192.168.1.136:3001
|
||||
VITE_API_URL=https://stream.graff.estate/api
|
||||
VITE_WEBRTC_URL=https://stream.graff.estate
|
||||
@@ -12,6 +12,7 @@
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-router": "^7.9.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
@@ -401,6 +402,8 @@
|
||||
|
||||
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
|
||||
|
||||
"goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
@@ -545,6 +548,8 @@
|
||||
|
||||
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
|
||||
|
||||
"react-hot-toast": ["react-hot-toast@2.6.0", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="],
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-router": "^7.9.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 670 KiB |
@@ -11,11 +11,13 @@ import type { AllSettings } from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.7
|
||||
export interface PixelStreamingWrapperProps {
|
||||
initialSettings?: Partial<AllSettings>;
|
||||
onVideoInitialized?: () => void;
|
||||
hasControl?: boolean;
|
||||
}
|
||||
|
||||
export const PixelStreamingWrapper = ({
|
||||
initialSettings,
|
||||
onVideoInitialized,
|
||||
hasControl = true,
|
||||
}: PixelStreamingWrapperProps) => {
|
||||
// A reference to parent div element that the Pixel Streaming library attaches into:
|
||||
const videoParent = useRef<HTMLDivElement>(null);
|
||||
@@ -110,6 +112,26 @@ export const PixelStreamingWrapper = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Обновляем состояние ввода при изменении hasControl
|
||||
useEffect(() => {
|
||||
if (!pixelStreaming || !videoParent.current) return;
|
||||
|
||||
const videoElement = videoParent.current.querySelector("video");
|
||||
if (videoElement) {
|
||||
if (hasControl) {
|
||||
videoElement.style.pointerEvents = "auto";
|
||||
videoElement.style.userSelect = "auto";
|
||||
} else {
|
||||
videoElement.style.pointerEvents = "none";
|
||||
videoElement.style.userSelect = "none";
|
||||
}
|
||||
}
|
||||
|
||||
if (videoParent.current) {
|
||||
videoParent.current.style.pointerEvents = hasControl ? "auto" : "none";
|
||||
}
|
||||
}, [hasControl, pixelStreaming]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -137,6 +159,7 @@ export const PixelStreamingWrapper = ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
zIndex: 20,
|
||||
}}
|
||||
onClick={() => {
|
||||
pixelStreaming?.play();
|
||||
|
||||
@@ -34,6 +34,9 @@ function SessionUsersPanel({
|
||||
updateSpeakingState,
|
||||
muteParticipant,
|
||||
disableParticipantVideo,
|
||||
grantControl,
|
||||
revokeControl,
|
||||
hasControl: localHasControl,
|
||||
currentUserId,
|
||||
} = useWebRTC(roomId, autoJoin);
|
||||
|
||||
@@ -212,13 +215,15 @@ function SessionUsersPanel({
|
||||
name="Вы"
|
||||
isMuted={isLocalAudioMuted}
|
||||
isVideoOff={isLocalVideoMuted}
|
||||
hasControl={false}
|
||||
hasControl={localHasControl}
|
||||
isAdmin={isLocalUserOrganizer}
|
||||
isLocal={true}
|
||||
mediaStream={localStream}
|
||||
onSpeakingChange={handleSpeakingChange}
|
||||
isLocalUserOrganizer={isLocalUserOrganizer}
|
||||
participantId={currentUserId}
|
||||
onGrantControl={grantControl}
|
||||
onRevokeControl={revokeControl}
|
||||
className={clsx(
|
||||
mode === "full" &&
|
||||
(activeCamerasCount <= 2
|
||||
@@ -249,7 +254,7 @@ function SessionUsersPanel({
|
||||
isMuted={participant.isMuted || false}
|
||||
isVideoOff={participant.isVideoOff || false}
|
||||
isSpeaking={participant.isSpeaking}
|
||||
hasControl={false}
|
||||
hasControl={participant.hasControl || false}
|
||||
isAdmin={isParticipantOrganizer(participant.id) || undefined}
|
||||
mediaStream={participant.stream}
|
||||
hasLocalMediaPermission={hasLocalStream}
|
||||
@@ -257,6 +262,8 @@ function SessionUsersPanel({
|
||||
participantId={participant.id}
|
||||
onMuteParticipant={muteParticipant}
|
||||
onDisableParticipantVideo={disableParticipantVideo}
|
||||
onGrantControl={grantControl}
|
||||
onRevokeControl={revokeControl}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -267,6 +274,14 @@ function SessionUsersPanel({
|
||||
isAudioMuted={isLocalAudioMuted}
|
||||
isVideoMuted={isLocalVideoMuted}
|
||||
hasLocalStream={hasLocalStream}
|
||||
hasControl={localHasControl}
|
||||
onToggleControl={() => {
|
||||
// Организатор всегда может вернуть управление себе
|
||||
if (isLocalUserOrganizer) {
|
||||
revokeControl();
|
||||
}
|
||||
}}
|
||||
isOrganizer={isLocalUserOrganizer}
|
||||
/>
|
||||
</DraggableContainer>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,9 @@ export default function ParticipantsPopup({ session }: ParticipantsPopupProps) {
|
||||
isVideoMuted,
|
||||
muteParticipant,
|
||||
disableParticipantVideo,
|
||||
grantControl,
|
||||
revokeControl,
|
||||
hasControl: localHasControl,
|
||||
} = useWebRTC();
|
||||
const { data: user } = useMe();
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -103,6 +106,9 @@ export default function ParticipantsPopup({ session }: ParticipantsPopupProps) {
|
||||
session={session}
|
||||
muteParticipant={muteParticipant}
|
||||
disableParticipantVideo={disableParticipantVideo}
|
||||
grantControl={grantControl}
|
||||
revokeControl={revokeControl}
|
||||
localHasControl={localHasControl}
|
||||
/>
|
||||
<hr className="w-full 2xl:h-[0.69vw] h-px border-[#F6F6F6] last:hidden" />
|
||||
</Fragment>
|
||||
@@ -141,6 +147,9 @@ function ParticipantItem({
|
||||
session,
|
||||
muteParticipant,
|
||||
disableParticipantVideo,
|
||||
grantControl,
|
||||
revokeControl,
|
||||
localHasControl,
|
||||
}: {
|
||||
participant: Participant & { isLocal?: boolean };
|
||||
isLocal: boolean;
|
||||
@@ -148,6 +157,9 @@ function ParticipantItem({
|
||||
session: Session;
|
||||
muteParticipant: (participantId: string) => void;
|
||||
disableParticipantVideo: (participantId: string) => void;
|
||||
grantControl: (participantId: string) => void;
|
||||
revokeControl: () => void;
|
||||
localHasControl: boolean;
|
||||
}) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -226,6 +238,11 @@ function ParticipantItem({
|
||||
<VideoOffFilledIcon />
|
||||
</div>
|
||||
)}
|
||||
{(participant.hasControl || (isLocal && localHasControl)) && (
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#7B60F3]">
|
||||
<HandRaisedFilledIcon />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Действия только для удаленных участников и только для организатора */}
|
||||
{!isLocal && isOrganizer && (
|
||||
@@ -254,9 +271,17 @@ function ParticipantItem({
|
||||
},
|
||||
{
|
||||
icon: <HandRaisedFilledIcon />,
|
||||
label: "Передать управление",
|
||||
label: participant.hasControl
|
||||
? "Забрать управление"
|
||||
: "Передать управление",
|
||||
onClick: () => {
|
||||
console.log("Grant control:", participant.id);
|
||||
if (participant.hasControl) {
|
||||
console.log("Revoke control");
|
||||
revokeControl();
|
||||
} else {
|
||||
console.log("Grant control:", participant.id);
|
||||
grantControl(participant.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import Warning from "../indicators/Warning";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
interface ControlButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -39,14 +37,6 @@ function ControlButton({
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
{disabled && size === "large" && (
|
||||
<Tooltip
|
||||
label="Нет доступа"
|
||||
className="absolute 2xl:-top-[0.139vw] 2xl:-right-[0.139vw] -top-0.5 -right-0.5"
|
||||
>
|
||||
<Warning type="critical" className="text-white" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import Button from "./Button";
|
||||
import ChatFilledIcon from "../icons/ChatFilledIcon";
|
||||
import UsersFilledIcon from "../icons/UsersFilledIcon";
|
||||
import ShareFilledIcon from "../icons/ShareFilledIcon";
|
||||
import CogFilledIcon from "../icons/CogFilledIcon";
|
||||
// import CogFilledIcon from "../icons/CogFilledIcon";
|
||||
import usePopupStore from "../../store/popupStore";
|
||||
import useModalStore from "../../store/modalStore";
|
||||
// import useModalStore from "../../store/modalStore";
|
||||
import ChatPopup from "../popups/ChatPopup";
|
||||
import ParticipantsPopup from "../popups/ParticipantsPopup";
|
||||
import SharePopup from "../popups/SharePopup";
|
||||
import SettingsModal from "../modals/SettingsModal";
|
||||
// import SettingsModal from "../modals/SettingsModal";
|
||||
import clsx from "clsx";
|
||||
import { useClickAway } from "@uidotdev/usehooks";
|
||||
import type { Session } from "../../types/Session";
|
||||
@@ -28,7 +28,7 @@ function ControlsPopover({ session, parentRef }: ControlsPopoverProps) {
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsOpened(false));
|
||||
|
||||
const { setPopup } = usePopupStore();
|
||||
const { setModal } = useModalStore();
|
||||
// const { setModal } = useModalStore();
|
||||
|
||||
function handleClickOpenChatPopup() {
|
||||
setIsOpened(false);
|
||||
@@ -50,10 +50,10 @@ function ControlsPopover({ session, parentRef }: ControlsPopoverProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function handleClickOpenSettingsModal() {
|
||||
setIsOpened(false);
|
||||
setModal(<SettingsModal />);
|
||||
}
|
||||
// function handleClickOpenSettingsModal() {
|
||||
// setIsOpened(false);
|
||||
// setModal(<SettingsModal />);
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="order-3 2xl:hidden z-[9999]" ref={ref}>
|
||||
@@ -102,7 +102,7 @@ function ControlsPopover({ session, parentRef }: ControlsPopoverProps) {
|
||||
</div>
|
||||
Пригласить
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="tertiary"
|
||||
className="w-full !justify-start"
|
||||
onClick={handleClickOpenSettingsModal}
|
||||
@@ -111,7 +111,7 @@ function ControlsPopover({ session, parentRef }: ControlsPopoverProps) {
|
||||
<CogFilledIcon />
|
||||
</div>
|
||||
Настройки
|
||||
</Button>
|
||||
</Button> */}
|
||||
</PopoverWrapper>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,8 @@ interface UserCameraProps {
|
||||
participantId?: string; // ID участника для управления
|
||||
onMuteParticipant?: (participantId: string) => void;
|
||||
onDisableParticipantVideo?: (participantId: string) => void;
|
||||
onGrantControl?: (participantId: string) => void;
|
||||
onRevokeControl?: () => void;
|
||||
}
|
||||
|
||||
export default function UserCamera({
|
||||
@@ -54,6 +56,8 @@ export default function UserCamera({
|
||||
participantId,
|
||||
onMuteParticipant,
|
||||
onDisableParticipantVideo,
|
||||
onGrantControl,
|
||||
// onRevokeControl,
|
||||
}: UserCameraProps) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const actionsPopoverParentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -448,7 +452,10 @@ export default function UserCamera({
|
||||
icon: <HandRaisedFilledIcon />,
|
||||
label: "Передать управление",
|
||||
onClick: () => {
|
||||
console.log("Grant control:", participantId);
|
||||
if (onGrantControl && participantId) {
|
||||
console.log("Grant control:", participantId);
|
||||
onGrantControl(participantId);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@ import ControlButton from "./ControlButton";
|
||||
import VideoFilledIcon from "../icons/VideoFilledIcon";
|
||||
import VideoOffFilledIcon from "../icons/VideoOffFilledIcon";
|
||||
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
|
||||
import CogFilledIcon from "../icons/CogFilledIcon";
|
||||
import useModalStore from "../../store/modalStore";
|
||||
import SettingsModal from "../modals/SettingsModal";
|
||||
// import CogFilledIcon from "../icons/CogFilledIcon";
|
||||
// import useModalStore from "../../store/modalStore";
|
||||
// import SettingsModal from "../modals/SettingsModal";
|
||||
import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon";
|
||||
import clsx from "clsx";
|
||||
|
||||
@@ -16,6 +16,9 @@ export interface UserDevicesControlsProps {
|
||||
isVideoMuted: boolean;
|
||||
hasLocalStream?: boolean;
|
||||
mode?: "full" | "mini";
|
||||
hasControl?: boolean;
|
||||
onToggleControl?: () => void;
|
||||
isOrganizer?: boolean;
|
||||
}
|
||||
|
||||
export default function UserDevicesControls({
|
||||
@@ -25,12 +28,15 @@ export default function UserDevicesControls({
|
||||
isVideoMuted,
|
||||
hasLocalStream = true,
|
||||
mode = "full",
|
||||
hasControl = false,
|
||||
onToggleControl,
|
||||
isOrganizer = false,
|
||||
}: UserDevicesControlsProps) {
|
||||
const { setModal } = useModalStore();
|
||||
// const { setModal } = useModalStore();
|
||||
|
||||
function ToggleSettings() {
|
||||
setModal(<SettingsModal />);
|
||||
}
|
||||
// function ToggleSettings() {
|
||||
// setModal(<SettingsModal />);
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -70,15 +76,31 @@ export default function UserDevicesControls({
|
||||
<ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
icon={<HandRaisedFilledIcon />}
|
||||
onClick={() => console.log("Toggle can control")}
|
||||
icon={
|
||||
<div className="relative">
|
||||
<HandRaisedFilledIcon />
|
||||
{!hasControl && (
|
||||
<div className="flex absolute inset-0 justify-center items-center">
|
||||
<div className="2xl:w-[1.389vw] 2xl:h-[0.139vw] w-5 h-[2px] bg-white rotate-45" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onClick={() => {
|
||||
// У организатора кнопка возвращает управление себе, только если его нет
|
||||
if (isOrganizer && !hasControl && onToggleControl) {
|
||||
onToggleControl();
|
||||
}
|
||||
}}
|
||||
className={hasControl ? "bg-[#7B60F3]" : ""}
|
||||
disabled={!isOrganizer}
|
||||
/>
|
||||
<ControlButton
|
||||
{/* <ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
icon={<CogFilledIcon />}
|
||||
onClick={ToggleSettings}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+119
-23
@@ -15,18 +15,35 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
||||
// Начальное состояние берем из сервиса, если он уже существует
|
||||
const [isAudioMuted, setIsAudioMuted] = useState(() => {
|
||||
const initialAudioMuted = webrtcServiceInstance ? webrtcServiceInstance.isAudioMuted() : true;
|
||||
const initialAudioMuted = webrtcServiceInstance
|
||||
? webrtcServiceInstance.isAudioMuted()
|
||||
: true;
|
||||
console.log(`[useWebRTC INIT] Initial isAudioMuted: ${initialAudioMuted}`);
|
||||
return initialAudioMuted;
|
||||
});
|
||||
const [isVideoMuted, setIsVideoMuted] = useState(() => {
|
||||
const initialVideoMuted = webrtcServiceInstance ? webrtcServiceInstance.isVideoMuted() : true;
|
||||
const initialVideoMuted = webrtcServiceInstance
|
||||
? webrtcServiceInstance.isVideoMuted()
|
||||
: true;
|
||||
console.log(`[useWebRTC INIT] Initial isVideoMuted: ${initialVideoMuted}`);
|
||||
return initialVideoMuted;
|
||||
});
|
||||
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [hasControl, setHasControl] = useState(() => {
|
||||
const initialControl = webrtcServiceInstance
|
||||
? webrtcServiceInstance.hasControl()
|
||||
: false;
|
||||
console.log(`[useWebRTC INIT] Initial hasControl: ${initialControl}`);
|
||||
return initialControl;
|
||||
});
|
||||
const hasControlRef = useRef(hasControl);
|
||||
|
||||
// Синхронизируем ref при изменении состояния
|
||||
useEffect(() => {
|
||||
hasControlRef.current = hasControl;
|
||||
}, [hasControl]);
|
||||
|
||||
// Логируем начальное состояние при монтировании
|
||||
console.log(
|
||||
@@ -67,15 +84,24 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
}
|
||||
|
||||
const existingParticipants = webrtcServiceInstance.getParticipants();
|
||||
console.log("[useWebRTC] Component mounted, existing participants:", existingParticipants.length);
|
||||
console.log(
|
||||
"[useWebRTC] Component mounted, existing participants:",
|
||||
existingParticipants.length
|
||||
);
|
||||
if (existingParticipants.length > 0) {
|
||||
console.log("[useWebRTC] Initializing with participants:", existingParticipants.map(p => p.id));
|
||||
console.log(
|
||||
"[useWebRTC] Initializing with participants:",
|
||||
existingParticipants.map((p) => p.id)
|
||||
);
|
||||
setParticipants(existingParticipants);
|
||||
}
|
||||
|
||||
const existingMessages = webrtcServiceInstance.getChatMessages();
|
||||
if (existingMessages.length > 0) {
|
||||
console.log("[useWebRTC] Initializing with existing messages:", existingMessages.length);
|
||||
console.log(
|
||||
"[useWebRTC] Initializing with existing messages:",
|
||||
existingMessages.length
|
||||
);
|
||||
setChatMessages(existingMessages);
|
||||
}
|
||||
|
||||
@@ -95,27 +121,40 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
// Если текущее состояние true (выключено по умолчанию), устанавливаем false (включено)
|
||||
// Иначе оставляем как есть (пользователь мог уже изменить)
|
||||
const newState = current === true ? false : current;
|
||||
console.log(`[useWebRTC] onLocalStreamReady: isAudioMuted ${current} -> ${newState}`);
|
||||
console.log(
|
||||
`[useWebRTC] onLocalStreamReady: isAudioMuted ${current} -> ${newState}`
|
||||
);
|
||||
return newState;
|
||||
});
|
||||
setIsVideoMuted((current) => {
|
||||
const newState = current === true ? false : current;
|
||||
console.log(`[useWebRTC] onLocalStreamReady: isVideoMuted ${current} -> ${newState}`);
|
||||
console.log(
|
||||
`[useWebRTC] onLocalStreamReady: isVideoMuted ${current} -> ${newState}`
|
||||
);
|
||||
return newState;
|
||||
});
|
||||
setIsInitialized(true);
|
||||
},
|
||||
onRemoteStreamReady: (participantId, stream) => {
|
||||
console.log("[useWebRTC] onRemoteStreamReady called for:", participantId);
|
||||
console.log(
|
||||
"[useWebRTC] onRemoteStreamReady called for:",
|
||||
participantId
|
||||
);
|
||||
setParticipants((prev) => {
|
||||
const existing = prev.find((p) => p.id === participantId);
|
||||
if (existing) {
|
||||
console.log("[useWebRTC] Updating stream for existing participant:", participantId);
|
||||
console.log(
|
||||
"[useWebRTC] Updating stream for existing participant:",
|
||||
participantId
|
||||
);
|
||||
return prev.map((p) =>
|
||||
p.id === participantId ? { ...p, stream } : p
|
||||
);
|
||||
} else {
|
||||
console.log("[useWebRTC] Adding new participant with stream:", participantId);
|
||||
console.log(
|
||||
"[useWebRTC] Adding new participant with stream:",
|
||||
participantId
|
||||
);
|
||||
return [...prev, { id: participantId, stream }];
|
||||
}
|
||||
});
|
||||
@@ -124,7 +163,10 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
setIsConnected(true);
|
||||
},
|
||||
onParticipantJoined: (participant) => {
|
||||
console.log("[useWebRTC] onParticipantJoined called for:", participant.id);
|
||||
console.log(
|
||||
"[useWebRTC] onParticipantJoined called for:",
|
||||
participant.id
|
||||
);
|
||||
setParticipants((prev) => {
|
||||
if (prev.find((p) => p.id === participant.id)) {
|
||||
console.log("[useWebRTC] Participant already in list, skipping");
|
||||
@@ -146,7 +188,9 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
setIsVideoMuted(!isEnabled);
|
||||
},
|
||||
onParticipantAudioToggle: (participantId, isEnabled) => {
|
||||
console.log(`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`);
|
||||
console.log(
|
||||
`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`
|
||||
);
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === participantId ? { ...p, isMuted: !isEnabled } : p
|
||||
@@ -154,7 +198,9 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
);
|
||||
},
|
||||
onParticipantVideoToggle: (participantId, isEnabled) => {
|
||||
console.log(`[useWebRTC] Video toggle for ${participantId}: ${isEnabled}`);
|
||||
console.log(
|
||||
`[useWebRTC] Video toggle for ${participantId}: ${isEnabled}`
|
||||
);
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === participantId ? { ...p, isVideoOff: !isEnabled } : p
|
||||
@@ -163,15 +209,31 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
},
|
||||
onParticipantSpeakingChange: (participantId, isSpeaking) => {
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === participantId ? { ...p, isSpeaking } : p
|
||||
)
|
||||
prev.map((p) => (p.id === participantId ? { ...p, isSpeaking } : p))
|
||||
);
|
||||
},
|
||||
onChatMessage: (message) => {
|
||||
console.log("[useWebRTC] onChatMessage called:", message);
|
||||
setChatMessages((prev) => [...prev, message]);
|
||||
},
|
||||
onParticipantControlToggle: (participantId, hasControl) => {
|
||||
console.log(
|
||||
`[useWebRTC] Participant ${participantId} control toggle: ${hasControl}`
|
||||
);
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) => (p.id === participantId ? { ...p, hasControl } : p))
|
||||
);
|
||||
},
|
||||
onControlGranted: () => {
|
||||
console.log("[useWebRTC] Local user granted control");
|
||||
hasControlRef.current = true;
|
||||
setHasControl(true);
|
||||
},
|
||||
onControlRevoked: () => {
|
||||
console.log("[useWebRTC] Local user revoked control");
|
||||
hasControlRef.current = false;
|
||||
setHasControl(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("[useWebRTC] Error:", error);
|
||||
},
|
||||
@@ -191,15 +253,19 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
try {
|
||||
isInitializing = true;
|
||||
const stream = await webrtcServiceInstance.initializeLocalStream();
|
||||
|
||||
|
||||
// Даже если stream === null (пользователь отказался от разрешений),
|
||||
// считаем инициализацию завершенной
|
||||
if (stream === null) {
|
||||
console.log("[useWebRTC] Initialized without local stream (user denied permissions)");
|
||||
console.log(
|
||||
"[useWebRTC] Initialized without local stream (user denied permissions)"
|
||||
);
|
||||
// Обновляем состояние muted на основе реального состояния сервиса
|
||||
const audioMuted = webrtcServiceInstance.isAudioMuted();
|
||||
const videoMuted = webrtcServiceInstance.isVideoMuted();
|
||||
console.log(`[useWebRTC] Setting isAudioMuted=${audioMuted}, isVideoMuted=${videoMuted}`);
|
||||
console.log(
|
||||
`[useWebRTC] Setting isAudioMuted=${audioMuted}, isVideoMuted=${videoMuted}`
|
||||
);
|
||||
setIsAudioMuted(audioMuted);
|
||||
setIsVideoMuted(videoMuted);
|
||||
setIsInitialized(true);
|
||||
@@ -210,7 +276,9 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
// Обновляем состояние muted на основе реального состояния сервиса
|
||||
const audioMuted = webrtcServiceInstance.isAudioMuted();
|
||||
const videoMuted = webrtcServiceInstance.isVideoMuted();
|
||||
console.log(`[useWebRTC] Error case - Setting isAudioMuted=${audioMuted}, isVideoMuted=${videoMuted}`);
|
||||
console.log(
|
||||
`[useWebRTC] Error case - Setting isAudioMuted=${audioMuted}, isVideoMuted=${videoMuted}`
|
||||
);
|
||||
setIsAudioMuted(audioMuted);
|
||||
setIsVideoMuted(videoMuted);
|
||||
setIsInitialized(true);
|
||||
@@ -244,6 +312,13 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
const joinRoomAsync = async () => {
|
||||
await webrtcServiceInstance!.joinRoom(roomId);
|
||||
hasJoinedRoomRef.current = true;
|
||||
// Синхронизируем состояние управления после присоединения
|
||||
const currentHasControl = webrtcServiceInstance!.hasControl();
|
||||
hasControlRef.current = currentHasControl;
|
||||
setHasControl(currentHasControl);
|
||||
console.log(
|
||||
`[useWebRTC] Synced hasControl after join: ${currentHasControl}`
|
||||
);
|
||||
};
|
||||
|
||||
joinRoomAsync();
|
||||
@@ -252,7 +327,9 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
const toggleAudio = () => {
|
||||
if (!webrtcServiceInstance) return;
|
||||
const newState = webrtcServiceInstance.toggleAudio();
|
||||
console.log(`[useWebRTC] toggleAudio: newState=${newState}, setting isAudioMuted=${!newState}`);
|
||||
console.log(
|
||||
`[useWebRTC] toggleAudio: newState=${newState}, setting isAudioMuted=${!newState}`
|
||||
);
|
||||
setIsAudioMuted(!newState);
|
||||
console.log(`[useWebRTC] setIsAudioMuted called with ${!newState}`);
|
||||
};
|
||||
@@ -260,12 +337,18 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
const toggleVideo = () => {
|
||||
if (!webrtcServiceInstance) return;
|
||||
const newState = webrtcServiceInstance.toggleVideo();
|
||||
console.log(`[useWebRTC] toggleVideo: newState=${newState}, setting isVideoMuted=${!newState}`);
|
||||
console.log(
|
||||
`[useWebRTC] toggleVideo: newState=${newState}, setting isVideoMuted=${!newState}`
|
||||
);
|
||||
setIsVideoMuted(!newState);
|
||||
console.log(`[useWebRTC] setIsVideoMuted called with ${!newState}`);
|
||||
};
|
||||
|
||||
const sendMessage = (content: string, senderName?: string, isAuthenticated?: boolean) => {
|
||||
const sendMessage = (
|
||||
content: string,
|
||||
senderName?: string,
|
||||
isAuthenticated?: boolean
|
||||
) => {
|
||||
if (!webrtcServiceInstance) return;
|
||||
webrtcServiceInstance.sendChatMessage(content, senderName, isAuthenticated);
|
||||
};
|
||||
@@ -303,6 +386,16 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
webrtcServiceInstance.updateUserId(newUserId);
|
||||
};
|
||||
|
||||
const grantControl = (participantId: string) => {
|
||||
if (!webrtcServiceInstance) return;
|
||||
webrtcServiceInstance.grantControl(participantId);
|
||||
};
|
||||
|
||||
const revokeControl = () => {
|
||||
if (!webrtcServiceInstance) return;
|
||||
webrtcServiceInstance.revokeControl();
|
||||
};
|
||||
|
||||
return {
|
||||
localStream,
|
||||
participants,
|
||||
@@ -321,5 +414,8 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
leaveRoom,
|
||||
muteParticipant,
|
||||
disableParticipantVideo,
|
||||
grantControl,
|
||||
revokeControl,
|
||||
hasControl,
|
||||
};
|
||||
};
|
||||
|
||||
+155
-43
@@ -19,6 +19,7 @@ export interface Participant {
|
||||
isMuted?: boolean;
|
||||
isVideoOff?: boolean;
|
||||
isSpeaking?: boolean;
|
||||
hasControl?: boolean;
|
||||
iceCandidateQueue?: RTCIceCandidate[];
|
||||
}
|
||||
|
||||
@@ -43,6 +44,12 @@ export interface WebRTCCallbacks {
|
||||
participantId: string,
|
||||
isSpeaking: boolean
|
||||
) => void;
|
||||
onParticipantControlToggle?: (
|
||||
participantId: string,
|
||||
hasControl: boolean
|
||||
) => void;
|
||||
onControlGranted?: () => void;
|
||||
onControlRevoked?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
@@ -54,6 +61,7 @@ interface WebRTCState {
|
||||
userId: string;
|
||||
isAudioEnabled: boolean;
|
||||
isVideoEnabled: boolean;
|
||||
hasControl: boolean;
|
||||
callbacks: WebRTCCallbacks[]; // Изменено на массив
|
||||
chatMessages: ChatMessage[];
|
||||
}
|
||||
@@ -135,6 +143,7 @@ export function createWebRTCService(callbacks: WebRTCCallbacks = {}) {
|
||||
userId,
|
||||
isAudioEnabled: true,
|
||||
isVideoEnabled: true,
|
||||
hasControl: false,
|
||||
callbacks: [callbacks], // Массив коллбэков
|
||||
chatMessages: [],
|
||||
};
|
||||
@@ -167,7 +176,12 @@ export function createWebRTCService(callbacks: WebRTCCallbacks = {}) {
|
||||
},
|
||||
updateUserId: (newUserId: string) => {
|
||||
if (state) {
|
||||
console.log("[WebRTC] Updating userId from", state.userId, "to", newUserId);
|
||||
console.log(
|
||||
"[WebRTC] Updating userId from",
|
||||
state.userId,
|
||||
"to",
|
||||
newUserId
|
||||
);
|
||||
state.userId = newUserId;
|
||||
}
|
||||
},
|
||||
@@ -177,15 +191,35 @@ export function createWebRTCService(callbacks: WebRTCCallbacks = {}) {
|
||||
getLocalStream: () => state?.localStream || null,
|
||||
isAudioMuted: () => {
|
||||
const result = state ? !state.isAudioEnabled : true;
|
||||
console.log(`[WebRTC] isAudioMuted() called: state.isAudioEnabled=${state?.isAudioEnabled}, returning ${result}`);
|
||||
console.log(
|
||||
`[WebRTC] isAudioMuted() called: state.isAudioEnabled=${state?.isAudioEnabled}, returning ${result}`
|
||||
);
|
||||
return result;
|
||||
},
|
||||
isVideoMuted: () => {
|
||||
const result = state ? !state.isVideoEnabled : true;
|
||||
console.log(`[WebRTC] isVideoMuted() called: state.isVideoEnabled=${state?.isVideoEnabled}, returning ${result}`);
|
||||
console.log(
|
||||
`[WebRTC] isVideoMuted() called: state.isVideoEnabled=${state?.isVideoEnabled}, returning ${result}`
|
||||
);
|
||||
return result;
|
||||
},
|
||||
hasControl: () => state?.hasControl || false,
|
||||
hasLocalStream: () => state?.localStream !== null,
|
||||
grantControl: (participantId: string) => {
|
||||
if (!state?.roomId) return;
|
||||
console.log(`[WebRTC] Granting control to ${participantId}`);
|
||||
state.socket.emit("grant-control", {
|
||||
roomId: state.roomId,
|
||||
targetUserId: participantId,
|
||||
});
|
||||
},
|
||||
revokeControl: () => {
|
||||
if (!state?.roomId) return;
|
||||
console.log("[WebRTC] Revoking control");
|
||||
state.socket.emit("revoke-control", {
|
||||
roomId: state.roomId,
|
||||
});
|
||||
},
|
||||
addCallbacks: (newCallbacks: WebRTCCallbacks) => {
|
||||
if (state) {
|
||||
state.callbacks.push(newCallbacks);
|
||||
@@ -342,7 +376,7 @@ function setupSocketListeners() {
|
||||
// Обработка общих ошибок от сервера
|
||||
socket.on("error", (error: { message: string }) => {
|
||||
console.error("[WebRTC] Error received from server:", error);
|
||||
|
||||
|
||||
// Показываем пользователю понятное сообщение
|
||||
if (error.message.includes("Unauthorized")) {
|
||||
alert("У вас нет прав для выполнения этого действия");
|
||||
@@ -351,43 +385,56 @@ function setupSocketListeners() {
|
||||
} else {
|
||||
alert(`Ошибка: ${error.message}`);
|
||||
}
|
||||
|
||||
|
||||
callAllCallbacks("onError", new Error(error.message));
|
||||
});
|
||||
|
||||
// Audio/Video toggle handlers
|
||||
socket.on("audio-toggle", ({ userId, isEnabled }: { userId: string; isEnabled: boolean }) => {
|
||||
console.log(`[WebRTC] Received audio-toggle from ${userId}: ${isEnabled}`);
|
||||
if (!state) return;
|
||||
socket.on(
|
||||
"audio-toggle",
|
||||
({ userId, isEnabled }: { userId: string; isEnabled: boolean }) => {
|
||||
console.log(
|
||||
`[WebRTC] Received audio-toggle from ${userId}: ${isEnabled}`
|
||||
);
|
||||
if (!state) return;
|
||||
|
||||
const participant = state.participants.get(userId);
|
||||
if (participant) {
|
||||
participant.isMuted = !isEnabled;
|
||||
callAllCallbacks("onParticipantAudioToggle", userId, isEnabled);
|
||||
const participant = state.participants.get(userId);
|
||||
if (participant) {
|
||||
participant.isMuted = !isEnabled;
|
||||
callAllCallbacks("onParticipantAudioToggle", userId, isEnabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
socket.on("video-toggle", ({ userId, isEnabled }: { userId: string; isEnabled: boolean }) => {
|
||||
console.log(`[WebRTC] Received video-toggle from ${userId}: ${isEnabled}`);
|
||||
if (!state) return;
|
||||
socket.on(
|
||||
"video-toggle",
|
||||
({ userId, isEnabled }: { userId: string; isEnabled: boolean }) => {
|
||||
console.log(
|
||||
`[WebRTC] Received video-toggle from ${userId}: ${isEnabled}`
|
||||
);
|
||||
if (!state) return;
|
||||
|
||||
const participant = state.participants.get(userId);
|
||||
if (participant) {
|
||||
participant.isVideoOff = !isEnabled;
|
||||
callAllCallbacks("onParticipantVideoToggle", userId, isEnabled);
|
||||
const participant = state.participants.get(userId);
|
||||
if (participant) {
|
||||
participant.isVideoOff = !isEnabled;
|
||||
callAllCallbacks("onParticipantVideoToggle", userId, isEnabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
socket.on("speaking-state", ({ userId, isSpeaking }: { userId: string; isSpeaking: boolean }) => {
|
||||
if (!state) return;
|
||||
socket.on(
|
||||
"speaking-state",
|
||||
({ userId, isSpeaking }: { userId: string; isSpeaking: boolean }) => {
|
||||
if (!state) return;
|
||||
|
||||
const participant = state.participants.get(userId);
|
||||
if (participant) {
|
||||
participant.isSpeaking = isSpeaking;
|
||||
// Уведомляем callback для обновления UI
|
||||
callAllCallbacks("onParticipantSpeakingChange", userId, isSpeaking);
|
||||
const participant = state.participants.get(userId);
|
||||
if (participant) {
|
||||
participant.isSpeaking = isSpeaking;
|
||||
// Уведомляем callback для обновления UI
|
||||
callAllCallbacks("onParticipantSpeakingChange", userId, isSpeaking);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Обработка принудительного выключения микрофона
|
||||
socket.on("force-mute-audio", () => {
|
||||
@@ -399,7 +446,7 @@ function setupSocketListeners() {
|
||||
track.enabled = false;
|
||||
});
|
||||
state.isAudioEnabled = false;
|
||||
|
||||
|
||||
// Уведомляем все компоненты об изменении
|
||||
callAllCallbacks("onLocalAudioToggle", false);
|
||||
console.log("[WebRTC] Audio forcefully muted by admin");
|
||||
@@ -415,12 +462,58 @@ function setupSocketListeners() {
|
||||
track.enabled = false;
|
||||
});
|
||||
state.isVideoEnabled = false;
|
||||
|
||||
|
||||
// Уведомляем все компоненты об изменении
|
||||
callAllCallbacks("onLocalVideoToggle", false);
|
||||
console.log("[WebRTC] Video forcefully disabled by admin");
|
||||
});
|
||||
|
||||
// Обработка событий управления PixelStreaming
|
||||
socket.on(
|
||||
"control-toggle",
|
||||
({ userId, hasControl }: { userId: string; hasControl: boolean }) => {
|
||||
console.log(`[WebRTC] Control toggle for ${userId}: ${hasControl}`);
|
||||
if (!state) return;
|
||||
|
||||
// Если это текущий пользователь, обновляем локальное состояние
|
||||
if (userId === state.userId) {
|
||||
// Вызываем коллбэк только если состояние действительно изменилось
|
||||
const previousControl = state.hasControl;
|
||||
state.hasControl = hasControl;
|
||||
if (hasControl && !previousControl) {
|
||||
callAllCallbacks("onControlGranted");
|
||||
} else if (!hasControl && previousControl) {
|
||||
callAllCallbacks("onControlRevoked");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем состояние для участника
|
||||
// Если участника еще нет, добавляем его
|
||||
let participant = state.participants.get(userId);
|
||||
if (!participant) {
|
||||
participant = addParticipant(userId);
|
||||
}
|
||||
|
||||
participant.hasControl = hasControl;
|
||||
callAllCallbacks("onParticipantControlToggle", userId, hasControl);
|
||||
}
|
||||
);
|
||||
|
||||
socket.on("control-granted", () => {
|
||||
console.log("[WebRTC] Received control-granted event");
|
||||
if (!state) return;
|
||||
state.hasControl = true;
|
||||
callAllCallbacks("onControlGranted");
|
||||
});
|
||||
|
||||
socket.on("control-revoked", () => {
|
||||
console.log("[WebRTC] Received control-revoked event");
|
||||
if (!state) return;
|
||||
state.hasControl = false;
|
||||
callAllCallbacks("onControlRevoked");
|
||||
});
|
||||
|
||||
console.log("Socket listeners set up complete");
|
||||
}
|
||||
|
||||
@@ -476,7 +569,9 @@ async function initializeLocalStream(): Promise<MediaStream | null> {
|
||||
// Устанавливаем состояние аудио и видео в false, так как нет доступа к медиа
|
||||
state.isAudioEnabled = false;
|
||||
state.isVideoEnabled = false;
|
||||
console.log("[WebRTC] Set isAudioEnabled and isVideoEnabled to false (no media access)");
|
||||
console.log(
|
||||
"[WebRTC] Set isAudioEnabled and isVideoEnabled to false (no media access)"
|
||||
);
|
||||
|
||||
// Возвращаем null вместо выброса ошибки, чтобы пользователь мог продолжить
|
||||
return null;
|
||||
@@ -486,11 +581,19 @@ async function initializeLocalStream(): Promise<MediaStream | null> {
|
||||
async function joinRoom(roomId: string): Promise<void> {
|
||||
if (!state) throw new Error("WebRTC service not initialized");
|
||||
|
||||
// Проверяем, не присоединились ли мы уже к этой комнате
|
||||
if (state.roomId === roomId) {
|
||||
console.log(`[WebRTC] Already in room ${roomId}, skipping join`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Joining room:", roomId, "with user ID:", state.userId);
|
||||
console.log(`[WebRTC] Joining with isAudioEnabled=${state.isAudioEnabled}, isVideoEnabled=${state.isVideoEnabled}`);
|
||||
console.log(
|
||||
`[WebRTC] Joining with isAudioEnabled=${state.isAudioEnabled}, isVideoEnabled=${state.isVideoEnabled}`
|
||||
);
|
||||
state.roomId = roomId;
|
||||
state.socket.emit("join-room", {
|
||||
roomId,
|
||||
state.socket.emit("join-room", {
|
||||
roomId,
|
||||
userId: state.userId,
|
||||
isAudioEnabled: state.isAudioEnabled,
|
||||
isVideoEnabled: state.isVideoEnabled,
|
||||
@@ -884,10 +987,10 @@ function toggleAudio(): boolean {
|
||||
track.enabled = !track.enabled;
|
||||
});
|
||||
state.isAudioEnabled = !state.isAudioEnabled;
|
||||
|
||||
|
||||
// Уведомляем все компоненты об изменении локального аудио
|
||||
callAllCallbacks("onLocalAudioToggle", state.isAudioEnabled);
|
||||
|
||||
|
||||
// Отправляем обновление состояния аудио всем участникам
|
||||
if (state.roomId) {
|
||||
state.socket.emit("audio-toggle", {
|
||||
@@ -897,7 +1000,7 @@ function toggleAudio(): boolean {
|
||||
});
|
||||
console.log(`[WebRTC] Sent audio-toggle: ${state.isAudioEnabled}`);
|
||||
}
|
||||
|
||||
|
||||
return state.isAudioEnabled;
|
||||
}
|
||||
|
||||
@@ -917,10 +1020,10 @@ function toggleVideo(): boolean {
|
||||
track.enabled = !track.enabled;
|
||||
});
|
||||
state.isVideoEnabled = !state.isVideoEnabled;
|
||||
|
||||
|
||||
// Уведомляем все компоненты об изменении локального видео
|
||||
callAllCallbacks("onLocalVideoToggle", state.isVideoEnabled);
|
||||
|
||||
|
||||
// Отправляем обновление состояния видео всем участникам
|
||||
if (state.roomId) {
|
||||
state.socket.emit("video-toggle", {
|
||||
@@ -930,7 +1033,7 @@ function toggleVideo(): boolean {
|
||||
});
|
||||
console.log(`[WebRTC] Sent video-toggle: ${state.isVideoEnabled}`);
|
||||
}
|
||||
|
||||
|
||||
return state.isVideoEnabled;
|
||||
}
|
||||
|
||||
@@ -945,11 +1048,20 @@ function updateSpeakingState(isSpeaking: boolean): void {
|
||||
});
|
||||
}
|
||||
|
||||
function sendChatMessage(content: string, senderName?: string, isAuthenticated?: boolean): void {
|
||||
function sendChatMessage(
|
||||
content: string,
|
||||
senderName?: string,
|
||||
isAuthenticated?: boolean
|
||||
): void {
|
||||
if (!state || !content.trim() || !state.roomId) return;
|
||||
|
||||
console.log("📤 Sending message via Socket.IO:", content);
|
||||
console.log("📤 isAuthenticated:", isAuthenticated, "state.userId:", state.userId);
|
||||
console.log(
|
||||
"📤 isAuthenticated:",
|
||||
isAuthenticated,
|
||||
"state.userId:",
|
||||
state.userId
|
||||
);
|
||||
|
||||
// Определяем userId и guestId на основе реальной авторизации
|
||||
// Если пользователь авторизован, state.userId содержит настоящий userId
|
||||
|
||||
@@ -14,6 +14,7 @@ import PopupContainer from "./components/PopupContainer";
|
||||
import ToastsContainer from "./components/toasts/ToastsContainer";
|
||||
import TestPage from "./pages/TestPage";
|
||||
import SessionPage from "./pages/SessionPage";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -56,5 +57,24 @@ createRoot(document.getElementById("root")!).render(
|
||||
<ModalContainer />
|
||||
<PopupContainer />
|
||||
<ToastsContainer />
|
||||
<Toaster
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: "#fff",
|
||||
color: "#333",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
boxShadow: "0 4px 40px rgba(15, 16, 17, 0.1)",
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: "#FF4517",
|
||||
secondary: "#fff",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ import clsx from "clsx";
|
||||
import { useMe } from "../hooks/useAuth";
|
||||
import QuitSessionModal from "../components/modals/QuitSessionModal";
|
||||
import useModalStore from "../store/modalStore";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
function SessionPage() {
|
||||
const { setPopup, popupType } = usePopupStore();
|
||||
@@ -104,11 +105,13 @@ function SessionPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const [mode, setMode] = useState<"full" | "mini">("mini");
|
||||
const [mode] = useState<"full" | "mini">("mini");
|
||||
// const [mode, setMode] = useState<"full" | "mini">("mini");
|
||||
const [isVideoInitialized, setIsVideoInitialized] = useState(false);
|
||||
|
||||
function toggleMode() {
|
||||
setMode(mode === "full" ? "mini" : "full");
|
||||
}
|
||||
// function toggleMode() {
|
||||
// setMode(mode === "full" ? "mini" : "full");
|
||||
// }
|
||||
|
||||
function handleQuitSessionModalOpen() {
|
||||
setModal(<QuitSessionModal onQuitSession={() => navigate("/test")} />);
|
||||
@@ -121,9 +124,27 @@ function SessionPage() {
|
||||
toggleVideo,
|
||||
isVideoMuted,
|
||||
participants,
|
||||
hasControl,
|
||||
} = useWebRTC(session?.id, true);
|
||||
|
||||
const actionsSidebarRef = useRef<HTMLDivElement>(null);
|
||||
const prevHasControlRef = useRef(hasControl);
|
||||
|
||||
// Отслеживаем изменения hasControl и показываем toast
|
||||
useEffect(() => {
|
||||
if (hasControl && !prevHasControlRef.current) {
|
||||
toast.success("Управление получено", {
|
||||
duration: 3000,
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
prevHasControlRef.current = hasControl;
|
||||
}, [hasControl]);
|
||||
|
||||
// Сбрасываем состояние инициализации видео при изменении параметров сессии
|
||||
useEffect(() => {
|
||||
setIsVideoInitialized(false);
|
||||
}, [session?.server?.localIp, session?.playerPort]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -168,7 +189,7 @@ function SessionPage() {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"overflow-hidden relative order-3 w-screen h-dvh bg-black touch-none max-2xl:flex max-2xl:portrait:items-center"
|
||||
"overflow-hidden relative order-3 w-screen bg-black h-dvh touch-none max-2xl:flex max-2xl:portrait:items-center"
|
||||
)}
|
||||
>
|
||||
{/* Pixel Streaming - показывается только когда сессия активна */}
|
||||
@@ -179,7 +200,8 @@ function SessionPage() {
|
||||
<div className="absolute z-0 w-full h-full aspect-video">
|
||||
<PixelStreamingWrapper
|
||||
initialSettings={{
|
||||
ss: `ws://${session.server.localIp}:${session.playerPort}`,
|
||||
ss: `wss://a1.sess.stream.graff.tech/server/${session.server.localIp}:${session.playerPort}/`,
|
||||
// ss: `ws://${session.server.localIp}:${session.playerPort}`,
|
||||
AutoPlayVideo: true,
|
||||
AutoConnect: true,
|
||||
StartVideoMuted: true,
|
||||
@@ -187,10 +209,38 @@ function SessionPage() {
|
||||
WaitForStreamer: true,
|
||||
StreamerId: "DefaultStreamer",
|
||||
}}
|
||||
hasControl={hasControl}
|
||||
onVideoInitialized={() => {
|
||||
console.log("Video initialized");
|
||||
setIsVideoInitialized(true);
|
||||
}}
|
||||
/>
|
||||
{/* Лоадер загрузки видео */}
|
||||
{!isVideoInitialized && (
|
||||
<div className="flex absolute inset-0 z-10 justify-center items-center bg-black/80">
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<div className="size-12 text-[#7B60F3] animate-spin">
|
||||
<LoaderIcon />
|
||||
</div>
|
||||
<div className="text-white">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Блокирующий overlay когда нет прав управления */}
|
||||
{!hasControl && (
|
||||
<div
|
||||
className="absolute inset-0 z-[100] pointer-events-auto cursor-pointer"
|
||||
onClick={() => {
|
||||
toast.error(
|
||||
"Управление недоступно. Запросите права у организатора",
|
||||
{
|
||||
duration: 4000,
|
||||
position: "top-center",
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -212,11 +262,11 @@ function SessionPage() {
|
||||
)}
|
||||
|
||||
<ActionsSidebarWrapper ref={actionsSidebarRef} className="z-[150]">
|
||||
<FloatingActionButton onClick={toggleMode}>
|
||||
{/* <FloatingActionButton onClick={toggleMode}>
|
||||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||||
{mode === "full" ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
</FloatingActionButton> */}
|
||||
<FloatingActionButton
|
||||
className="max-2xl:hidden"
|
||||
onClick={handleChatOpen}
|
||||
|
||||
@@ -32,7 +32,8 @@ function TestPage() {
|
||||
const response = await api
|
||||
.post("sessions", {
|
||||
json: {
|
||||
appId: "2914d736-b928-461c-b58f-e5d35d8b605d",
|
||||
// appId: "60b0a46a-15f6-40db-8f1e-a288c78f8631",
|
||||
appId: "1e762115-8bab-4d41-9bec-55c4a4ad3fbe",
|
||||
mode: "stream",
|
||||
tier: "demo",
|
||||
},
|
||||
|
||||
+487
-48
@@ -61,10 +61,13 @@ interface User {
|
||||
socketId: string;
|
||||
isAudioEnabled?: boolean;
|
||||
isVideoEnabled?: boolean;
|
||||
hasControl?: boolean;
|
||||
}
|
||||
|
||||
const rooms = new Map<string, Room>();
|
||||
const users = new Map<string, User>();
|
||||
// Таймеры для автоматического завершения сессий (roomId -> NodeJS.Timeout)
|
||||
const sessionEndTimers = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
// Вспомогательные функции
|
||||
function findUserBySocketId(socketId: string): User | undefined {
|
||||
@@ -81,6 +84,93 @@ function findSocketIdByUserId(userId: string): string | undefined {
|
||||
return user?.socketId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отменить таймер автоматического завершения сессии для комнаты
|
||||
*/
|
||||
function cancelSessionEndTimer(roomId: string) {
|
||||
const timer = sessionEndTimers.get(roomId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
sessionEndTimers.delete(roomId);
|
||||
console.log(
|
||||
`[Session Auto-End] Cancelled auto-end timer for room ${roomId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить таймер автоматического завершения сессии через 3 минуты
|
||||
*/
|
||||
async function startSessionEndTimer(roomId: string) {
|
||||
// Отменяем существующий таймер, если есть
|
||||
cancelSessionEndTimer(roomId);
|
||||
|
||||
// Проверяем статус сессии - если уже завершена, не запускаем таймер
|
||||
try {
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
if (session && (session.status === "ended" || session.status === "ending")) {
|
||||
console.log(
|
||||
`[Session Auto-End] Session ${roomId} is already ended or ending, skipping auto-end timer`
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Session Auto-End] Error checking session status for ${roomId}:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
// Продолжаем выполнение, если не удалось проверить статус
|
||||
}
|
||||
|
||||
const TIMEOUT_MS = 3 * 60 * 1000; // 3 минуты
|
||||
|
||||
console.log(
|
||||
`[Session Auto-End] Starting auto-end timer for room ${roomId} (will end in 3 minutes if no participants join)`
|
||||
);
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
try {
|
||||
// Проверяем, что комната все еще пустая
|
||||
const room = rooms.get(roomId);
|
||||
if (room && room.participants.size === 0) {
|
||||
console.log(
|
||||
`[Session Auto-End] Room ${roomId} has been empty for 3 minutes, ending session`
|
||||
);
|
||||
|
||||
// Завершаем сессию
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
if (session && (session.status === "started" || session.status === "starting")) {
|
||||
await serverSessionService.end(roomId);
|
||||
console.log(
|
||||
`[Session Auto-End] Session ${roomId} has been ended automatically`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`[Session Auto-End] Session ${roomId} is already ended or ending, skipping`
|
||||
);
|
||||
}
|
||||
|
||||
// Удаляем комнату и таймер
|
||||
rooms.delete(roomId);
|
||||
sessionEndTimers.delete(roomId);
|
||||
} else {
|
||||
console.log(
|
||||
`[Session Auto-End] Room ${roomId} has participants now, cancelling auto-end`
|
||||
);
|
||||
sessionEndTimers.delete(roomId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Session Auto-End] Error ending session ${roomId}:`,
|
||||
error instanceof Error ? error.message : error
|
||||
);
|
||||
sessionEndTimers.delete(roomId);
|
||||
}
|
||||
}, TIMEOUT_MS);
|
||||
|
||||
sessionEndTimers.set(roomId, timer);
|
||||
}
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`[WebRTC] User connected: ${socket.id}`);
|
||||
|
||||
@@ -92,9 +182,32 @@ io.on("connection", (socket) => {
|
||||
`[WebRTC] User ${userId} (socket: ${socket.id}) joining room ${roomId}, audio: ${isAudioEnabled}, video: ${isVideoEnabled}`
|
||||
);
|
||||
|
||||
// Покинуть предыдущую комнату если была
|
||||
// Проверяем существующего пользователя
|
||||
const existingUser = users.get(userId);
|
||||
if (existingUser?.roomId) {
|
||||
|
||||
// Если пользователь уже в этой комнате с тем же socket.id - это дубликат запроса
|
||||
if (
|
||||
existingUser &&
|
||||
existingUser.roomId === roomId &&
|
||||
existingUser.socketId === socket.id
|
||||
) {
|
||||
console.log(
|
||||
`[WebRTC] User ${userId} is already in room ${roomId} with same socket, ignoring duplicate join`
|
||||
);
|
||||
// Но все равно отправляем состояние управления на случай если оно не было получено
|
||||
socket.emit("control-toggle", {
|
||||
userId,
|
||||
hasControl: existingUser.hasControl || false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Покинуть предыдущую комнату если была
|
||||
if (
|
||||
existingUser &&
|
||||
existingUser.roomId &&
|
||||
existingUser.roomId !== roomId
|
||||
) {
|
||||
console.log(
|
||||
`[WebRTC] User ${userId} leaving previous room ${existingUser.roomId}`
|
||||
);
|
||||
@@ -103,6 +216,19 @@ io.on("connection", (socket) => {
|
||||
if (prevRoom) {
|
||||
prevRoom.participants.delete(userId);
|
||||
socket.to(existingUser.roomId).emit("user-left", userId);
|
||||
|
||||
// Если предыдущая комната стала пустой, запускаем таймер завершения сессии
|
||||
if (prevRoom.participants.size === 0) {
|
||||
console.log(
|
||||
`[WebRTC] Previous room ${existingUser.roomId} is now empty, starting auto-end timer`
|
||||
);
|
||||
startSessionEndTimer(existingUser.roomId).catch((error) => {
|
||||
console.error(
|
||||
`[WebRTC] Error starting auto-end timer for room ${existingUser.roomId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,23 +245,88 @@ io.on("connection", (socket) => {
|
||||
}
|
||||
|
||||
const room = rooms.get(roomId)!;
|
||||
room.participants.add(userId);
|
||||
|
||||
// Сохранить пользователя с состоянием аудио/видео
|
||||
users.set(userId, {
|
||||
// Если пользователь уже в участниках комнаты (переподключение), удаляем его из старого socket
|
||||
if (room.participants.has(userId)) {
|
||||
console.log(
|
||||
`[WebRTC] User ${userId} is reconnecting to room ${roomId}, updating socket`
|
||||
);
|
||||
// Не возвращаемся - продолжаем обработку для обновления socketId и отправки состояния
|
||||
} else {
|
||||
room.participants.add(userId);
|
||||
// Отменяем таймер завершения сессии, так как появился участник
|
||||
cancelSessionEndTimer(roomId);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь владельцем сессии
|
||||
let hasControl = false;
|
||||
try {
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
if (session) {
|
||||
// Владелец - это userId (для авторизованных) или guestId (для гостей)
|
||||
const isOwner = Boolean(
|
||||
(session.userId && session.userId === userId) ||
|
||||
(session.guestId && session.guestId === userId)
|
||||
);
|
||||
|
||||
if (isOwner) {
|
||||
// Проверяем, есть ли в комнате участники с управлением (кроме самого организатора)
|
||||
let hasControllerInRoom = false;
|
||||
for (const [uid, user] of users.entries()) {
|
||||
if (user.roomId === roomId && user.hasControl && uid !== userId) {
|
||||
hasControllerInRoom = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если нет участников с управлением, передаем управление организатору
|
||||
if (!hasControllerInRoom) {
|
||||
hasControl = true;
|
||||
console.log(
|
||||
`[WebRTC] Owner ${userId} joining/reconnecting, no controller in room, granting control`
|
||||
);
|
||||
} else {
|
||||
// Если есть участник с управлением, сохраняем текущее состояние организатора
|
||||
// (если у организатора было управление, оно остается, если не было - остается без управления)
|
||||
hasControl = existingUser?.hasControl || false;
|
||||
console.log(
|
||||
`[WebRTC] Owner ${userId} joining/reconnecting, controller exists in room, keeping current state (hasControl=${hasControl})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Для не-организаторов сохраняем существующее состояние управления
|
||||
hasControl = existingUser?.hasControl || false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[WebRTC] Error checking session owner:`, error);
|
||||
}
|
||||
|
||||
// Сохранить/обновить пользователя с состоянием аудио/видео и управления
|
||||
const userData: User = {
|
||||
id: userId,
|
||||
roomId,
|
||||
socketId: socket.id,
|
||||
isAudioEnabled: isAudioEnabled !== false,
|
||||
isVideoEnabled: isVideoEnabled !== false,
|
||||
});
|
||||
hasControl: hasControl || false,
|
||||
};
|
||||
|
||||
// Если пользователь уже существует, обновляем его данные (особенно socketId при переподключении)
|
||||
if (existingUser) {
|
||||
Object.assign(existingUser, userData);
|
||||
} else {
|
||||
users.set(userId, userData);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[WebRTC] Room ${roomId} now has participants:`,
|
||||
Array.from(room.participants)
|
||||
);
|
||||
console.log(
|
||||
`[WebRTC] User ${userId} media state: audio=${isAudioEnabled !== false}, video=${isVideoEnabled !== false}`
|
||||
`[WebRTC] User ${userId} media state: audio=${
|
||||
isAudioEnabled !== false
|
||||
}, video=${isVideoEnabled !== false}`
|
||||
);
|
||||
|
||||
// Уведомить других участников
|
||||
@@ -162,12 +353,12 @@ io.on("connection", (socket) => {
|
||||
);
|
||||
socket.emit("room-participants", participants);
|
||||
|
||||
// Отправить состояние аудио/видео существующих участников новому пользователю
|
||||
// Отправить состояние аудио/видео и управления существующих участников новому пользователю
|
||||
participants.forEach((participantId) => {
|
||||
const participant = users.get(participantId);
|
||||
if (participant) {
|
||||
console.log(
|
||||
`[WebRTC] Sending ${participantId} media state to ${userId}: audio=${participant.isAudioEnabled}, video=${participant.isVideoEnabled}`
|
||||
`[WebRTC] Sending ${participantId} media state to ${userId}: audio=${participant.isAudioEnabled}, video=${participant.isVideoEnabled}, hasControl=${participant.hasControl}`
|
||||
);
|
||||
socket.emit("audio-toggle", {
|
||||
userId: participantId,
|
||||
@@ -177,8 +368,26 @@ io.on("connection", (socket) => {
|
||||
userId: participantId,
|
||||
isEnabled: participant.isVideoEnabled !== false,
|
||||
});
|
||||
socket.emit("control-toggle", {
|
||||
userId: participantId,
|
||||
hasControl: participant.hasControl || false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Отправить состояние управления нового пользователя всем в комнате
|
||||
socket.to(roomId).emit("control-toggle", {
|
||||
userId,
|
||||
hasControl,
|
||||
});
|
||||
|
||||
// Отправить состояние управления самому новому пользователю
|
||||
socket.emit("control-toggle", {
|
||||
userId,
|
||||
hasControl,
|
||||
});
|
||||
|
||||
console.log(`[WebRTC] User ${userId} hasControl: ${hasControl}`);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -192,10 +401,17 @@ io.on("connection", (socket) => {
|
||||
room.participants.delete(userId);
|
||||
socket.to(roomId).emit("user-left", userId);
|
||||
|
||||
// Удалить пустую комнату
|
||||
// Если комната стала пустой, запускаем таймер завершения сессии
|
||||
if (room.participants.size === 0) {
|
||||
rooms.delete(roomId);
|
||||
console.log(`[WebRTC] Deleted empty room ${roomId}`);
|
||||
console.log(
|
||||
`[WebRTC] Room ${roomId} is now empty, starting auto-end timer`
|
||||
);
|
||||
startSessionEndTimer(roomId).catch((error) => {
|
||||
console.error(
|
||||
`[WebRTC] Error starting auto-end timer for room ${roomId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,14 +466,14 @@ io.on("connection", (socket) => {
|
||||
console.log(
|
||||
`[WebRTC] Audio toggle from ${userId} in room ${roomId}: ${isEnabled}`
|
||||
);
|
||||
|
||||
|
||||
// Обновляем сохраненное состояние пользователя
|
||||
const user = users.get(userId);
|
||||
if (user) {
|
||||
user.isAudioEnabled = isEnabled;
|
||||
console.log(`[WebRTC] Updated ${userId} audio state to ${isEnabled}`);
|
||||
}
|
||||
|
||||
|
||||
// Отправляем всем в комнате (кроме отправителя)
|
||||
socket.to(roomId).emit("audio-toggle", { userId, isEnabled });
|
||||
});
|
||||
@@ -266,14 +482,14 @@ io.on("connection", (socket) => {
|
||||
console.log(
|
||||
`[WebRTC] Video toggle from ${userId} in room ${roomId}: ${isEnabled}`
|
||||
);
|
||||
|
||||
|
||||
// Обновляем сохраненное состояние пользователя
|
||||
const user = users.get(userId);
|
||||
if (user) {
|
||||
user.isVideoEnabled = isEnabled;
|
||||
console.log(`[WebRTC] Updated ${userId} video state to ${isEnabled}`);
|
||||
}
|
||||
|
||||
|
||||
// Отправляем всем в комнате (кроме отправителя)
|
||||
socket.to(roomId).emit("video-toggle", { userId, isEnabled });
|
||||
});
|
||||
@@ -289,15 +505,17 @@ io.on("connection", (socket) => {
|
||||
console.log(
|
||||
`[WebRTC] Mute participant request: ${targetUserId} in room ${roomId}`
|
||||
);
|
||||
|
||||
|
||||
// Получаем информацию о пользователе, который отправил команду
|
||||
const requestingUser = findUserBySocketId(socket.id);
|
||||
if (!requestingUser) {
|
||||
console.warn(`[WebRTC] Unauthorized mute request from unknown socket ${socket.id}`);
|
||||
console.warn(
|
||||
`[WebRTC] Unauthorized mute request from unknown socket ${socket.id}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: user not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Проверяем, что пользователь находится в той же комнате
|
||||
if (requestingUser.roomId !== roomId) {
|
||||
console.warn(
|
||||
@@ -306,7 +524,7 @@ io.on("connection", (socket) => {
|
||||
socket.emit("error", { message: "Unauthorized: not in the same room" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Проверяем, что пользователь является организатором сессии
|
||||
try {
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
@@ -315,44 +533,51 @@ io.on("connection", (socket) => {
|
||||
socket.emit("error", { message: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Проверяем, что запрашивающий пользователь - организатор
|
||||
// Организатор - это userId (для авторизованных) или guestId (для гостей)
|
||||
const isOrganizer =
|
||||
const isOrganizer =
|
||||
(session.userId && session.userId === requestingUser.id) ||
|
||||
(session.guestId && session.guestId === requestingUser.id);
|
||||
|
||||
|
||||
if (!isOrganizer) {
|
||||
console.warn(
|
||||
`[WebRTC] User ${requestingUser.id} is not the organizer of session ${roomId}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: only organizer can mute participants" });
|
||||
socket.emit("error", {
|
||||
message: "Unauthorized: only organizer can mute participants",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebRTC] User ${requestingUser.id} is authorized as organizer`);
|
||||
|
||||
console.log(
|
||||
`[WebRTC] User ${requestingUser.id} is authorized as organizer`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[WebRTC] Error checking session organizer:`, error);
|
||||
socket.emit("error", { message: "Failed to verify permissions" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Обновляем состояние участника
|
||||
const targetUser = users.get(targetUserId);
|
||||
if (targetUser) {
|
||||
targetUser.isAudioEnabled = false;
|
||||
console.log(`[WebRTC] Updated ${targetUserId} audio state to false`);
|
||||
}
|
||||
|
||||
|
||||
// Отправляем команду конкретному участнику
|
||||
const targetSocketId = findSocketIdByUserId(targetUserId);
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("force-mute-audio");
|
||||
console.log(`[WebRTC] Sent force-mute-audio to ${targetUserId}`);
|
||||
}
|
||||
|
||||
|
||||
// Уведомляем всех в комнате об изменении состояния
|
||||
io.to(roomId).emit("audio-toggle", { userId: targetUserId, isEnabled: false });
|
||||
io.to(roomId).emit("audio-toggle", {
|
||||
userId: targetUserId,
|
||||
isEnabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка команды выключения камеры участника
|
||||
@@ -360,15 +585,17 @@ io.on("connection", (socket) => {
|
||||
console.log(
|
||||
`[WebRTC] Disable video request: ${targetUserId} in room ${roomId}`
|
||||
);
|
||||
|
||||
|
||||
// Получаем информацию о пользователе, который отправил команду
|
||||
const requestingUser = findUserBySocketId(socket.id);
|
||||
if (!requestingUser) {
|
||||
console.warn(`[WebRTC] Unauthorized disable video request from unknown socket ${socket.id}`);
|
||||
console.warn(
|
||||
`[WebRTC] Unauthorized disable video request from unknown socket ${socket.id}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: user not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Проверяем, что пользователь находится в той же комнате
|
||||
if (requestingUser.roomId !== roomId) {
|
||||
console.warn(
|
||||
@@ -377,7 +604,7 @@ io.on("connection", (socket) => {
|
||||
socket.emit("error", { message: "Unauthorized: not in the same room" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Проверяем, что пользователь является организатором сессии
|
||||
try {
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
@@ -386,44 +613,251 @@ io.on("connection", (socket) => {
|
||||
socket.emit("error", { message: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Проверяем, что запрашивающий пользователь - организатор
|
||||
// Организатор - это userId (для авторизованных) или guestId (для гостей)
|
||||
const isOrganizer =
|
||||
const isOrganizer =
|
||||
(session.userId && session.userId === requestingUser.id) ||
|
||||
(session.guestId && session.guestId === requestingUser.id);
|
||||
|
||||
|
||||
if (!isOrganizer) {
|
||||
console.warn(
|
||||
`[WebRTC] User ${requestingUser.id} is not the organizer of session ${roomId}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: only organizer can disable video" });
|
||||
socket.emit("error", {
|
||||
message: "Unauthorized: only organizer can disable video",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebRTC] User ${requestingUser.id} is authorized as organizer`);
|
||||
|
||||
console.log(
|
||||
`[WebRTC] User ${requestingUser.id} is authorized as organizer`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[WebRTC] Error checking session organizer:`, error);
|
||||
socket.emit("error", { message: "Failed to verify permissions" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Обновляем состояние участника
|
||||
const targetUser = users.get(targetUserId);
|
||||
if (targetUser) {
|
||||
targetUser.isVideoEnabled = false;
|
||||
console.log(`[WebRTC] Updated ${targetUserId} video state to false`);
|
||||
}
|
||||
|
||||
|
||||
// Отправляем команду конкретному участнику
|
||||
const targetSocketId = findSocketIdByUserId(targetUserId);
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("force-disable-video");
|
||||
console.log(`[WebRTC] Sent force-disable-video to ${targetUserId}`);
|
||||
}
|
||||
|
||||
|
||||
// Уведомляем всех в комнате об изменении состояния
|
||||
io.to(roomId).emit("video-toggle", { userId: targetUserId, isEnabled: false });
|
||||
io.to(roomId).emit("video-toggle", {
|
||||
userId: targetUserId,
|
||||
isEnabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка передачи управления PixelStreaming
|
||||
socket.on("grant-control", async ({ roomId, targetUserId }) => {
|
||||
console.log(
|
||||
`[WebRTC] Grant control request: ${targetUserId} in room ${roomId}`
|
||||
);
|
||||
|
||||
// Получаем информацию о пользователе, который отправил команду
|
||||
const requestingUser = findUserBySocketId(socket.id);
|
||||
if (!requestingUser) {
|
||||
console.warn(
|
||||
`[WebRTC] Unauthorized grant control request from unknown socket ${socket.id}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: user not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь находится в той же комнате
|
||||
if (requestingUser.roomId !== roomId) {
|
||||
console.warn(
|
||||
`[WebRTC] User ${requestingUser.id} tried to grant control in room ${roomId}, but is in room ${requestingUser.roomId}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: not in the same room" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь является владельцем сессии
|
||||
try {
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
if (!session) {
|
||||
console.warn(`[WebRTC] Session ${roomId} not found`);
|
||||
socket.emit("error", { message: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что запрашивающий пользователь - владелец
|
||||
// Владелец - это userId (для авторизованных) или guestId (для гостей)
|
||||
const isOwner =
|
||||
(session.userId && session.userId === requestingUser.id) ||
|
||||
(session.guestId && session.guestId === requestingUser.id);
|
||||
|
||||
if (!isOwner) {
|
||||
console.warn(
|
||||
`[WebRTC] User ${requestingUser.id} is not the owner of session ${roomId}`
|
||||
);
|
||||
socket.emit("error", {
|
||||
message: "Unauthorized: only owner can grant control",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebRTC] User ${requestingUser.id} is authorized as owner`);
|
||||
} catch (error) {
|
||||
console.error(`[WebRTC] Error checking session owner:`, error);
|
||||
socket.emit("error", { message: "Failed to verify permissions" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что целевой пользователь существует и находится в комнате
|
||||
const targetUser = users.get(targetUserId);
|
||||
if (!targetUser || targetUser.roomId !== roomId) {
|
||||
console.warn(
|
||||
`[WebRTC] Target user ${targetUserId} not found or not in room ${roomId}`
|
||||
);
|
||||
socket.emit("error", { message: "Target user not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Находим текущего пользователя с управлением в этой комнате
|
||||
let currentController: User | undefined;
|
||||
for (const [userId, user] of users.entries()) {
|
||||
if (user.roomId === roomId && user.hasControl) {
|
||||
currentController = user;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если есть текущий контроллер и это не целевой пользователь, отзываем управление
|
||||
if (currentController && currentController.id !== targetUserId) {
|
||||
currentController.hasControl = false;
|
||||
const currentControllerSocketId = findSocketIdByUserId(
|
||||
currentController.id
|
||||
);
|
||||
if (currentControllerSocketId) {
|
||||
io.to(currentControllerSocketId).emit("control-revoked");
|
||||
console.log(`[WebRTC] Revoked control from ${currentController.id}`);
|
||||
}
|
||||
// Уведомляем всех в комнате об отзыве управления
|
||||
io.to(roomId).emit("control-toggle", {
|
||||
userId: currentController.id,
|
||||
hasControl: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Предоставляем управление целевому пользователю
|
||||
targetUser.hasControl = true;
|
||||
const targetSocketId = findSocketIdByUserId(targetUserId);
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit("control-granted");
|
||||
console.log(`[WebRTC] Granted control to ${targetUserId}`);
|
||||
}
|
||||
|
||||
// Уведомляем всех в комнате о предоставлении управления
|
||||
io.to(roomId).emit("control-toggle", {
|
||||
userId: targetUserId,
|
||||
hasControl: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка возврата управления владельцем
|
||||
socket.on("revoke-control", async ({ roomId }) => {
|
||||
console.log(`[WebRTC] Revoke control request in room ${roomId}`);
|
||||
|
||||
// Получаем информацию о пользователе, который отправил команду
|
||||
const requestingUser = findUserBySocketId(socket.id);
|
||||
if (!requestingUser) {
|
||||
console.warn(
|
||||
`[WebRTC] Unauthorized revoke control request from unknown socket ${socket.id}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: user not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь находится в той же комнате
|
||||
if (requestingUser.roomId !== roomId) {
|
||||
console.warn(
|
||||
`[WebRTC] User ${requestingUser.id} tried to revoke control in room ${roomId}, but is in room ${requestingUser.roomId}`
|
||||
);
|
||||
socket.emit("error", { message: "Unauthorized: not in the same room" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь является владельцем сессии
|
||||
try {
|
||||
const session = await serverSessionService.findById(roomId);
|
||||
if (!session) {
|
||||
console.warn(`[WebRTC] Session ${roomId} not found`);
|
||||
socket.emit("error", { message: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что запрашивающий пользователь - владелец
|
||||
const isOwner =
|
||||
(session.userId && session.userId === requestingUser.id) ||
|
||||
(session.guestId && session.guestId === requestingUser.id);
|
||||
|
||||
if (!isOwner) {
|
||||
console.warn(
|
||||
`[WebRTC] User ${requestingUser.id} is not the owner of session ${roomId}`
|
||||
);
|
||||
socket.emit("error", {
|
||||
message: "Unauthorized: only owner can revoke control",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WebRTC] User ${requestingUser.id} is authorized as owner`);
|
||||
} catch (error) {
|
||||
console.error(`[WebRTC] Error checking session owner:`, error);
|
||||
socket.emit("error", { message: "Failed to verify permissions" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Находим текущего пользователя с управлением в этой комнате
|
||||
let currentController: User | undefined;
|
||||
for (const [userId, user] of users.entries()) {
|
||||
if (user.roomId === roomId && user.hasControl) {
|
||||
currentController = user;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если есть текущий контроллер, отзываем управление
|
||||
if (currentController) {
|
||||
currentController.hasControl = false;
|
||||
const currentControllerSocketId = findSocketIdByUserId(
|
||||
currentController.id
|
||||
);
|
||||
if (currentControllerSocketId) {
|
||||
io.to(currentControllerSocketId).emit("control-revoked");
|
||||
console.log(`[WebRTC] Revoked control from ${currentController.id}`);
|
||||
}
|
||||
// Уведомляем всех в комнате об отзыве управления
|
||||
io.to(roomId).emit("control-toggle", {
|
||||
userId: currentController.id,
|
||||
hasControl: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Возвращаем управление владельцу
|
||||
requestingUser.hasControl = true;
|
||||
socket.emit("control-granted");
|
||||
console.log(`[WebRTC] Returned control to owner ${requestingUser.id}`);
|
||||
|
||||
// Уведомляем всех в комнате о возврате управления владельцу
|
||||
io.to(roomId).emit("control-toggle", {
|
||||
userId: requestingUser.id,
|
||||
hasControl: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка сообщений чата
|
||||
@@ -494,7 +928,7 @@ io.on("connection", (socket) => {
|
||||
const messageData = {
|
||||
sessionId: roomId,
|
||||
userId: userId || null, // userId для авторизованных пользователей
|
||||
guestId: userId ? null : (guestId || null), // guestId только если нет userId
|
||||
guestId: userId ? null : guestId || null, // guestId только если нет userId
|
||||
senderName: finalSenderName, // Имя отправителя
|
||||
content,
|
||||
type: "text" as const,
|
||||
@@ -561,12 +995,17 @@ io.on("connection", (socket) => {
|
||||
.to(disconnectedUser.roomId)
|
||||
.emit("user-left", disconnectedUser.id);
|
||||
|
||||
// Удалить пустую комнату
|
||||
// Если комната стала пустой, запускаем таймер завершения сессии
|
||||
if (room.participants.size === 0) {
|
||||
rooms.delete(disconnectedUser.roomId);
|
||||
console.log(
|
||||
`[WebRTC] Deleted empty room ${disconnectedUser.roomId}`
|
||||
`[WebRTC] Room ${disconnectedUser.roomId} is now empty, starting auto-end timer`
|
||||
);
|
||||
startSessionEndTimer(disconnectedUser.roomId).catch((error) => {
|
||||
console.error(
|
||||
`[WebRTC] Error starting auto-end timer for room ${disconnectedUser.roomId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "session-server",
|
||||
interpreter: "bun",
|
||||
script: "./dist/index.js",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -192,6 +192,8 @@ function getGpuFreeMb(): number {
|
||||
{
|
||||
encoding: "utf-8",
|
||||
timeout: 5000, // 5 секунд таймаут
|
||||
windowsHide: true, // Скрыть окно консоли на Windows
|
||||
stdio: "pipe", // Перенаправить вывод в pipe вместо создания нового окна
|
||||
}
|
||||
);
|
||||
|
||||
@@ -754,6 +756,7 @@ function killProcessTree(pid: number): void {
|
||||
execSync(`taskkill /pid ${pid} /T /F`, {
|
||||
stdio: "ignore",
|
||||
timeout: 10000,
|
||||
windowsHide: true, // Скрыть окно консоли на Windows
|
||||
});
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Дерево процессов для PID ${pid} успешно завершено`
|
||||
|
||||
Reference in New Issue
Block a user