From d5b17d60c9a6ba17e59ff9d6b4d54e1dd05c4943 Mon Sep 17 00:00:00 2001 From: inmake Date: Wed, 5 Nov 2025 17:47:39 +0500 Subject: [PATCH] Enhance logging in SessionUsersPanel and ParticipantsPopup for debugging audio and video states. Update useWebRTC to initialize audio/video muted states from the service and add functionality to mute participants and disable their video. Adjust environment variables for server configuration. --- client/src/components/SessionUsersPanel.tsx | 7 + .../components/popups/ParticipantsPopup.tsx | 85 ++++++-- client/src/components/ui/Button.tsx | 2 +- client/src/components/ui/UserCamera.tsx | 23 ++- client/src/hooks/useWebRTC.ts | 79 +++++++- client/src/lib/webrtc.ts | 90 ++++++++- server/.env | 8 +- server/src/index.ts | 185 +++++++++++++++++- 8 files changed, 457 insertions(+), 22 deletions(-) diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx index dc9f33e..ec2ec9e 100644 --- a/client/src/components/SessionUsersPanel.tsx +++ b/client/src/components/SessionUsersPanel.tsx @@ -30,6 +30,13 @@ function SessionUsersPanel({ const hasLocalStream = localStream !== null; + // Логируем изменения hasLocalStream для отладки + useEffect(() => { + console.log( + `[SessionUsersPanel] hasLocalStream changed to: ${hasLocalStream}` + ); + }, [hasLocalStream]); + // State для хранения состояния speaking const [localSpeaking, setLocalSpeaking] = useState(false); const lastSentSpeakingRef = useRef(false); diff --git a/client/src/components/popups/ParticipantsPopup.tsx b/client/src/components/popups/ParticipantsPopup.tsx index b17b4d2..cc862c7 100644 --- a/client/src/components/popups/ParticipantsPopup.tsx +++ b/client/src/components/popups/ParticipantsPopup.tsx @@ -8,7 +8,7 @@ import Avatar from "../ui/Avatar"; import Button from "../ui/Button"; import ShareFilledIcon from "../icons/ShareFilledIcon"; import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon"; -import { Fragment, useRef } from "react"; +import { Fragment, useRef, useEffect } from "react"; import DraggableContainer from "../DraggableContainer"; import { useWebRTC } from "../../hooks/useWebRTC"; import type { Participant } from "../../lib/webrtc"; @@ -21,10 +21,23 @@ interface ParticipantsPopupProps { } export default function ParticipantsPopup({ session }: ParticipantsPopupProps) { - const { participants, currentUserId, localStream } = useWebRTC(); + const { + participants, + currentUserId, + localStream, + isAudioMuted, + isVideoMuted, + muteParticipant, + disableParticipantVideo, + } = useWebRTC(); const { data: user } = useMe(); const headerRef = useRef(null); + // Логируем каждый рендер компонента + console.log( + `[ParticipantsPopup RENDER] isAudioMuted=${isAudioMuted}, isVideoMuted=${isVideoMuted}` + ); + // Определяем, является ли текущий пользователь организатором const isOrganizer = !!(session.userId && user?.id === session.userId) || @@ -36,10 +49,36 @@ export default function ParticipantsPopup({ session }: ParticipantsPopupProps) { id: currentUserId, stream: localStream || undefined, isLocal: true, + isMuted: isAudioMuted, + isVideoOff: isVideoMuted, }, ...participants, ]; + // Логируем создание массива для отладки + console.log( + `[ParticipantsPopup] allParticipants created with local user: isAudioMuted=${isAudioMuted}, isVideoMuted=${isVideoMuted}` + ); + + // Логируем изменения состояния для отладки + useEffect(() => { + console.log( + `[ParticipantsPopup] Local user state changed - isMuted: ${isAudioMuted}, isVideoOff: ${isVideoMuted}` + ); + }, [isAudioMuted, isVideoMuted]); + + useEffect(() => { + console.log( + `[ParticipantsPopup] Participants:`, + participants.map((p) => ({ + id: p.id.slice(0, 8), + name: p.name, + isMuted: p.isMuted, + isVideoOff: p.isVideoOff, + })) + ); + }, [participants]); + return ( = 640} @@ -63,6 +102,8 @@ export default function ParticipantsPopup({ session }: ParticipantsPopupProps) { isLocal={participant.isLocal || false} isOrganizer={isOrganizer} session={session} + muteParticipant={muteParticipant} + disableParticipantVideo={disableParticipantVideo} />
@@ -99,23 +140,39 @@ function ParticipantItem({ isLocal, isOrganizer, session, + muteParticipant, + disableParticipantVideo, }: { participant: Participant & { isLocal?: boolean }; isLocal: boolean; isOrganizer: boolean; session: Session; + muteParticipant: (participantId: string) => void; + disableParticipantVideo: (participantId: string) => void; }) { const parentRef = useRef(null); - // Проверяем наличие аудио/видео треков - const hasAudio = - participant.stream?.getAudioTracks().some((track) => track.enabled) ?? - false; - const hasVideo = - participant.stream?.getVideoTracks().some((track) => track.enabled) ?? - false; - const isMuted = !hasAudio; - const isVideoOff = !hasVideo; + // Используем состояние из participant (для локального и удаленных участников) + // Это состояние синхронизируется через Socket.IO для удаленных участников + // и через хук useWebRTC для локального + const isMuted = participant.isMuted ?? false; + const isVideoOff = participant.isVideoOff ?? false; + + // Логируем каждый рендер + console.log( + `[ParticipantItem RENDER] ${isLocal ? "Local" : "Remote"} ${ + participant.id.slice(0, 8) + } - isMuted=${isMuted}, isVideoOff=${isVideoOff}, hasStream=${!!participant.stream}` + ); + + // Логируем состояние участника для отладки + useEffect(() => { + console.log( + `[ParticipantItem] ${isLocal ? "Local" : "Remote"} ${ + participant.id.slice(0, 8) + } - isMuted: ${isMuted}, isVideoOff: ${isVideoOff}, participant.isMuted: ${participant.isMuted}, participant.isVideoOff: ${participant.isVideoOff}` + ); + }, [isMuted, isVideoOff, isLocal, participant.id, participant.isMuted, participant.isVideoOff]); // Определяем, является ли этот конкретный участник организатором сессии const isThisParticipantOrganizer = @@ -167,14 +224,20 @@ function ParticipantItem({ label: "Выключить микрофон", onClick: () => { console.log("Mute participant:", participant.id); + muteParticipant(participant.id); }, + // Disabled если микрофон уже выключен или нет потока + disabled: isMuted || !participant.stream, }, { icon: , label: "Выключить камеру", onClick: () => { console.log("Turn off video:", participant.id); + disableParticipantVideo(participant.id); }, + // Disabled если камера уже выключена или нет потока + disabled: isVideoOff || !participant.stream, }, { icon: , diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx index 4a80847..bd205c5 100644 --- a/client/src/components/ui/Button.tsx +++ b/client/src/components/ui/Button.tsx @@ -40,7 +40,7 @@ function Button({ variant === "secondary" && "bg-[#FFFFFF] hover:bg-[#F0F0F0] active:text-[#7B60F3] active:bg-[#F3F1FD]", variant === "tertiary" && - "bg-[#FFFFFF] hover:bg-[#F0F0F0] text-[#7D7D7D] active:text-[#141414] active:bg-[#F3F3F3]", + "bg-[#FFFFFF] hover:bg-[#F0F0F0] text-[#7D7D7D] active:text-[#141414] active:bg-[#F3F3F3] disabled:bg-transparent", variant === "critical" && "text-[#FF4517] bg-[#FEF3F2] hover:bg-[#FEE4E2]", size === "large" && diff --git a/client/src/components/ui/UserCamera.tsx b/client/src/components/ui/UserCamera.tsx index 4edff71..a44e736 100644 --- a/client/src/components/ui/UserCamera.tsx +++ b/client/src/components/ui/UserCamera.tsx @@ -62,6 +62,27 @@ export default function UserCamera({ // Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay) const [isAudioMuted, setIsAudioMuted] = useState(!hasLocalMediaPermission); + // Обновляем состояние muted при изменении hasLocalMediaPermission + useEffect(() => { + if (!isLocal) { + // Для удаленных участников: если у локального нет разрешения - mute для autoplay + setIsAudioMuted(!hasLocalMediaPermission); + console.log( + `[UserCamera] ${name} audio muted state updated to ${!hasLocalMediaPermission} (hasLocalMediaPermission: ${hasLocalMediaPermission})` + ); + } + }, [hasLocalMediaPermission, isLocal, name]); + + // Явно обновляем атрибут muted у video элемента при изменении isAudioMuted + useEffect(() => { + if (ref.current && !isLocal) { + ref.current.muted = isAudioMuted; + console.log( + `[UserCamera] ${name} video element muted attribute set to ${isAudioMuted}` + ); + } + }, [isAudioMuted, isLocal, name]); + // Детекция голосовой активности (только для локального пользователя) const { isSpeaking: isVoiceActive } = useVoiceActivity( isLocal ? mediaStream : null @@ -418,7 +439,7 @@ export default function UserCamera({ {mode === "full" && (
-

{name}

+

{name}

{isMuted && (
diff --git a/client/src/hooks/useWebRTC.ts b/client/src/hooks/useWebRTC.ts index 5eda96e..35e5855 100644 --- a/client/src/hooks/useWebRTC.ts +++ b/client/src/hooks/useWebRTC.ts @@ -13,12 +13,35 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { const hasJoinedRoomRef = useRef(false); const [localStream, setLocalStream] = useState(null); const [participants, setParticipants] = useState([]); - const [isAudioMuted, setIsAudioMuted] = useState(false); - const [isVideoMuted, setIsVideoMuted] = useState(false); + // Начальное состояние берем из сервиса, если он уже существует + const [isAudioMuted, setIsAudioMuted] = useState(() => { + 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; + console.log(`[useWebRTC INIT] Initial isVideoMuted: ${initialVideoMuted}`); + return initialVideoMuted; + }); const [chatMessages, setChatMessages] = useState([]); const [isConnected, setIsConnected] = useState(false); const [isInitialized, setIsInitialized] = useState(false); + // Логируем начальное состояние при монтировании + console.log( + `[useWebRTC INIT] Component mounting with isAudioMuted=${isAudioMuted}, isVideoMuted=${isVideoMuted}` + ); + + // Отслеживаем изменения isAudioMuted и isVideoMuted + useEffect(() => { + console.log(`[useWebRTC STATE] isAudioMuted changed to: ${isAudioMuted}`); + }, [isAudioMuted]); + + useEffect(() => { + console.log(`[useWebRTC STATE] isVideoMuted changed to: ${isVideoMuted}`); + }, [isVideoMuted]); + // Мониторинг изменений участников (отключено для снижения шума в консоли) // useEffect(() => { // console.log("[useWebRTC] Participants state updated:", participants.map(p => ({ @@ -38,6 +61,8 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { if (existingStream) { console.log("[useWebRTC] Initializing with existing local stream"); setLocalStream(existingStream); + // НЕ перезаписываем состояние - оно уже правильно установлено в useState + // setIsAudioMuted и setIsVideoMuted уже получили правильные значения из webrtcServiceInstance setIsInitialized(true); } @@ -64,6 +89,20 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { onLocalStreamReady: (stream) => { console.log("[useWebRTC] Local stream ready"); setLocalStream(stream); + // Когда ВПЕРВЫЕ получаем поток, устанавливаем аудио и видео как включенные + // Используем callback форму setState чтобы проверить текущее состояние + setIsAudioMuted((current) => { + // Если текущее состояние true (выключено по умолчанию), устанавливаем false (включено) + // Иначе оставляем как есть (пользователь мог уже изменить) + const newState = current === true ? false : current; + console.log(`[useWebRTC] onLocalStreamReady: isAudioMuted ${current} -> ${newState}`); + return newState; + }); + setIsVideoMuted((current) => { + const newState = current === true ? false : current; + console.log(`[useWebRTC] onLocalStreamReady: isVideoMuted ${current} -> ${newState}`); + return newState; + }); setIsInitialized(true); }, onRemoteStreamReady: (participantId, stream) => { @@ -98,6 +137,14 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { onParticipantLeft: (participantId) => { setParticipants((prev) => prev.filter((p) => p.id !== participantId)); }, + onLocalAudioToggle: (isEnabled) => { + console.log(`[useWebRTC] Local audio toggle callback: ${isEnabled}`); + setIsAudioMuted(!isEnabled); + }, + onLocalVideoToggle: (isEnabled) => { + console.log(`[useWebRTC] Local video toggle callback: ${isEnabled}`); + setIsVideoMuted(!isEnabled); + }, onParticipantAudioToggle: (participantId, isEnabled) => { console.log(`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`); setParticipants((prev) => @@ -149,11 +196,23 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { // считаем инициализацию завершенной if (stream === null) { 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}`); + setIsAudioMuted(audioMuted); + setIsVideoMuted(videoMuted); setIsInitialized(true); } } catch (error) { console.error("[useWebRTC] Initialization error:", error); // Даже при ошибке разрешаем продолжить + // Обновляем состояние muted на основе реального состояния сервиса + const audioMuted = webrtcServiceInstance.isAudioMuted(); + const videoMuted = webrtcServiceInstance.isVideoMuted(); + console.log(`[useWebRTC] Error case - Setting isAudioMuted=${audioMuted}, isVideoMuted=${videoMuted}`); + setIsAudioMuted(audioMuted); + setIsVideoMuted(videoMuted); setIsInitialized(true); } finally { isInitializing = false; @@ -193,13 +252,17 @@ 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}`); setIsAudioMuted(!newState); + console.log(`[useWebRTC] setIsAudioMuted called with ${!newState}`); }; const toggleVideo = () => { if (!webrtcServiceInstance) return; const newState = webrtcServiceInstance.toggleVideo(); + 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) => { @@ -207,6 +270,16 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { webrtcServiceInstance.sendChatMessage(content, senderName, isAuthenticated); }; + const muteParticipant = (participantId: string) => { + if (!webrtcServiceInstance) return; + webrtcServiceInstance.muteParticipant(participantId); + }; + + const disableParticipantVideo = (participantId: string) => { + if (!webrtcServiceInstance) return; + webrtcServiceInstance.disableParticipantVideo(participantId); + }; + const joinRoom = async (roomId: string) => { if (!webrtcServiceInstance) return; await webrtcServiceInstance.joinRoom(roomId); @@ -246,5 +319,7 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { updateUserId, joinRoom, leaveRoom, + muteParticipant, + disableParticipantVideo, }; }; diff --git a/client/src/lib/webrtc.ts b/client/src/lib/webrtc.ts index 61af2ca..9fb0be3 100644 --- a/client/src/lib/webrtc.ts +++ b/client/src/lib/webrtc.ts @@ -29,6 +29,8 @@ export interface WebRTCCallbacks { onRemoteStreamReady?: (participantId: string, stream: MediaStream) => void; onRoomParticipants?: (participantIds: string[]) => void; onChatMessage?: (message: ChatMessage) => void; + onLocalAudioToggle?: (isEnabled: boolean) => void; + onLocalVideoToggle?: (isEnabled: boolean) => void; onParticipantAudioToggle?: ( participantId: string, isEnabled: boolean @@ -147,6 +149,22 @@ export function createWebRTCService(callbacks: WebRTCCallbacks = {}) { leaveRoom, sendChatMessage, updateSpeakingState, + muteParticipant: (participantId: string) => { + if (!state?.roomId) return; + console.log(`[WebRTC] Muting participant ${participantId}`); + state.socket.emit("mute-participant", { + roomId: state.roomId, + targetUserId: participantId, + }); + }, + disableParticipantVideo: (participantId: string) => { + if (!state?.roomId) return; + console.log(`[WebRTC] Disabling video for participant ${participantId}`); + state.socket.emit("disable-participant-video", { + roomId: state.roomId, + targetUserId: participantId, + }); + }, updateUserId: (newUserId: string) => { if (state) { console.log("[WebRTC] Updating userId from", state.userId, "to", newUserId); @@ -157,8 +175,16 @@ export function createWebRTCService(callbacks: WebRTCCallbacks = {}) { getCurrentUserId: () => state?.userId || "", getParticipants: () => Array.from(state?.participants.values() || []), getLocalStream: () => state?.localStream || null, - isAudioMuted: () => (state ? !state.isAudioEnabled : true), - isVideoMuted: () => (state ? !state.isVideoEnabled : true), + isAudioMuted: () => { + const result = state ? !state.isAudioEnabled : true; + 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}`); + return result; + }, hasLocalStream: () => state?.localStream !== null, addCallbacks: (newCallbacks: WebRTCCallbacks) => { if (state) { @@ -313,6 +339,22 @@ function setupSocketListeners() { callAllCallbacks("onError", new Error(error.message)); }); + // Обработка общих ошибок от сервера + socket.on("error", (error: { message: string }) => { + console.error("[WebRTC] Error received from server:", error); + + // Показываем пользователю понятное сообщение + if (error.message.includes("Unauthorized")) { + alert("У вас нет прав для выполнения этого действия"); + } else if (error.message.includes("not found")) { + alert("Сессия не найдена"); + } 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}`); @@ -347,6 +389,38 @@ function setupSocketListeners() { } }); + // Обработка принудительного выключения микрофона + socket.on("force-mute-audio", () => { + console.log("[WebRTC] Received force-mute-audio command from server"); + if (!state?.localStream) return; + + const audioTracks = state.localStream.getAudioTracks(); + audioTracks.forEach((track) => { + track.enabled = false; + }); + state.isAudioEnabled = false; + + // Уведомляем все компоненты об изменении + callAllCallbacks("onLocalAudioToggle", false); + console.log("[WebRTC] Audio forcefully muted by admin"); + }); + + // Обработка принудительного выключения камеры + socket.on("force-disable-video", () => { + console.log("[WebRTC] Received force-disable-video command from server"); + if (!state?.localStream) return; + + const videoTracks = state.localStream.getVideoTracks(); + videoTracks.forEach((track) => { + track.enabled = false; + }); + state.isVideoEnabled = false; + + // Уведомляем все компоненты об изменении + callAllCallbacks("onLocalVideoToggle", false); + console.log("[WebRTC] Video forcefully disabled by admin"); + }); + console.log("Socket listeners set up complete"); } @@ -399,6 +473,11 @@ async function initializeLocalStream(): Promise { console.warn("Продолжаем без локального медиа-потока:", errorMessage); callAllCallbacks("onError", new Error(errorMessage)); + // Устанавливаем состояние аудио и видео в false, так как нет доступа к медиа + state.isAudioEnabled = false; + state.isVideoEnabled = false; + console.log("[WebRTC] Set isAudioEnabled and isVideoEnabled to false (no media access)"); + // Возвращаем null вместо выброса ошибки, чтобы пользователь мог продолжить return null; } @@ -408,6 +487,7 @@ async function joinRoom(roomId: string): Promise { if (!state) throw new Error("WebRTC service not initialized"); console.log("Joining room:", roomId, "with user ID:", state.userId); + console.log(`[WebRTC] Joining with isAudioEnabled=${state.isAudioEnabled}, isVideoEnabled=${state.isVideoEnabled}`); state.roomId = roomId; state.socket.emit("join-room", { roomId, @@ -805,6 +885,9 @@ function toggleAudio(): boolean { }); state.isAudioEnabled = !state.isAudioEnabled; + // Уведомляем все компоненты об изменении локального аудио + callAllCallbacks("onLocalAudioToggle", state.isAudioEnabled); + // Отправляем обновление состояния аудио всем участникам if (state.roomId) { state.socket.emit("audio-toggle", { @@ -835,6 +918,9 @@ function toggleVideo(): boolean { }); state.isVideoEnabled = !state.isVideoEnabled; + // Уведомляем все компоненты об изменении локального видео + callAllCallbacks("onLocalVideoToggle", state.isVideoEnabled); + // Отправляем обновление состояния видео всем участникам if (state.roomId) { state.socket.emit("video-toggle", { diff --git a/server/.env b/server/.env index 560efa5..5f755e2 100644 --- a/server/.env +++ b/server/.env @@ -1,6 +1,6 @@ DATABASE_URL=postgres://postgres:v1sq3vD5faXL@194.26.138.94:5432/stream JWT_SECRET=b5cf2bd3894fb24191f13dc9dddaeecccc92d0ee298e7ee41c2d0aab51c28fa1 -# PORT=6000 -# SOCKET_PORT=6001 -PORT=3000 -SOCKET_PORT=3001 \ No newline at end of file +PORT=6000 +SOCKET_PORT=6001 +# PORT=3000 +# SOCKET_PORT=3001 \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 4290b98..7685bf6 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -59,6 +59,8 @@ interface User { id: string; roomId?: string; socketId: string; + isAudioEnabled?: boolean; + isVideoEnabled?: boolean; } const rooms = new Map(); @@ -119,17 +121,22 @@ io.on("connection", (socket) => { const room = rooms.get(roomId)!; room.participants.add(userId); - // Сохранить пользователя + // Сохранить пользователя с состоянием аудио/видео users.set(userId, { id: userId, roomId, socketId: socket.id, + isAudioEnabled: isAudioEnabled !== false, + isVideoEnabled: isVideoEnabled !== false, }); 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}` + ); // Уведомить других участников socket.to(roomId).emit("user-joined", userId); @@ -154,6 +161,24 @@ io.on("connection", (socket) => { participants ); 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}` + ); + socket.emit("audio-toggle", { + userId: participantId, + isEnabled: participant.isAudioEnabled !== false, + }); + socket.emit("video-toggle", { + userId: participantId, + isEnabled: participant.isVideoEnabled !== false, + }); + } + }); } ); @@ -225,6 +250,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 }); }); @@ -233,6 +266,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 }); }); @@ -243,6 +284,148 @@ io.on("connection", (socket) => { socket.to(roomId).emit("speaking-state", { userId, isSpeaking }); }); + // Обработка команды выключения микрофона участника + socket.on("mute-participant", async ({ roomId, targetUserId }) => { + 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}`); + socket.emit("error", { message: "Unauthorized: user not found" }); + return; + } + + // Проверяем, что пользователь находится в той же комнате + if (requestingUser.roomId !== roomId) { + console.warn( + `[WebRTC] User ${requestingUser.id} tried to mute participant 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 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" }); + return; + } + + 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 }); + }); + + // Обработка команды выключения камеры участника + socket.on("disable-participant-video", async ({ roomId, targetUserId }) => { + 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}`); + socket.emit("error", { message: "Unauthorized: user not found" }); + return; + } + + // Проверяем, что пользователь находится в той же комнате + if (requestingUser.roomId !== roomId) { + console.warn( + `[WebRTC] User ${requestingUser.id} tried to disable video 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 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" }); + return; + } + + 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 }); + }); + // Обработка сообщений чата socket.on( "chat-message",