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.

This commit is contained in:
2025-11-05 17:47:39 +05:00
parent ddeb7d8148
commit d5b17d60c9
8 changed files with 457 additions and 22 deletions
@@ -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<boolean>(false);
const lastSentSpeakingRef = useRef<boolean>(false);
@@ -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<HTMLDivElement>(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 (
<DraggableContainer
enabled={window.innerWidth >= 640}
@@ -63,6 +102,8 @@ export default function ParticipantsPopup({ session }: ParticipantsPopupProps) {
isLocal={participant.isLocal || false}
isOrganizer={isOrganizer}
session={session}
muteParticipant={muteParticipant}
disableParticipantVideo={disableParticipantVideo}
/>
<hr className="w-full 2xl:h-[0.69vw] h-px border-[#F6F6F6] last:hidden" />
</Fragment>
@@ -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<HTMLDivElement>(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: <VideoOffFilledIcon />,
label: "Выключить камеру",
onClick: () => {
console.log("Turn off video:", participant.id);
disableParticipantVideo(participant.id);
},
// Disabled если камера уже выключена или нет потока
disabled: isVideoOff || !participant.stream,
},
{
icon: <HandRaisedFilledIcon />,
+1 -1
View File
@@ -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" &&
+22 -1
View File
@@ -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" && (
<div className="2xl:px-[1.111vw] 2xl:py-[0.556vw] px-4 py-2 bg-[#141414]/25 backdrop-blur-[10px] 2xl:rounded-[1.111vw] rounded-2xl absolute 2xl:bottom-[1.111vw] bottom-4 left-1/2 -translate-x-1/2 z-10 flex 2xl:gap-[0.556vw] gap-2 items-center">
<p className="text-white button-m font-medium">{name}</p>
<p className="font-medium text-white button-m">{name}</p>
{isMuted && (
<div className="2xl:size-[1.111vw] size-4 text-white/50">
<MicrophoneOffFilledIcon />
+77 -2
View File
@@ -13,12 +13,35 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
const hasJoinedRoomRef = useRef(false);
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [participants, setParticipants] = useState<Participant[]>([]);
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<ChatMessage[]>([]);
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,
};
};
+88 -2
View File
@@ -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<MediaStream | null> {
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<void> {
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", {
+4 -4
View File
@@ -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
PORT=6000
SOCKET_PORT=6001
# PORT=3000
# SOCKET_PORT=3001
+184 -1
View File
@@ -59,6 +59,8 @@ interface User {
id: string;
roomId?: string;
socketId: string;
isAudioEnabled?: boolean;
isVideoEnabled?: boolean;
}
const rooms = new Map<string, Room>();
@@ -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",