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", {