diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx index d708f83..e8db0ca 100644 --- a/client/src/components/SessionUsersPanel.tsx +++ b/client/src/components/SessionUsersPanel.tsx @@ -1,9 +1,9 @@ -import { useMemo } from "react"; import UserCamera from "./ui/UserCamera"; import UserDevicesControls from "./ui/UserDevicesControls"; import DraggableContainer from "./DraggableContainer"; import { useWebRTC } from "../hooks/useWebRTC"; import clsx from "clsx"; +import { useEffect, useRef, useState } from "react"; interface SessionUsersPanelProps { roomId: string; @@ -28,47 +28,62 @@ function SessionUsersPanel({ const hasLocalStream = localStream !== null; - // Callback для отправки состояния speaking + // State для хранения состояния speaking + const [localSpeaking, setLocalSpeaking] = useState(false); + const lastSentSpeakingRef = useRef(false); + const speakingStateTimeoutRef = useRef(null); + + // Callback для получения изменений состояния speaking от UserCamera const handleSpeakingChange = (isSpeaking: boolean) => { - updateSpeakingState?.(isSpeaking); + setLocalSpeaking(isSpeaking); }; - // Вычисляем количество камер для grid - мемоизируем для избежания лишних вычислений - const activeCamerasCount = useMemo( - () => - (localStream ? 1 : 0) + - participants.filter( - (p) => p.stream != null && p.stream.getTracks().length > 0 - ).length, - [localStream, participants] - ); + // useEffect для throttle и отправки состояния speaking через socket + useEffect(() => { + // Отправляем только если состояние действительно изменилось + if (lastSentSpeakingRef.current === localSpeaking) { + return; + } + // Очищаем предыдущий таймер + if (speakingStateTimeoutRef.current) { + clearTimeout(speakingStateTimeoutRef.current); + } + + // Отправляем состояние с задержкой (debounce 300ms) + speakingStateTimeoutRef.current = setTimeout(() => { + lastSentSpeakingRef.current = localSpeaking; + updateSpeakingState?.(localSpeaking); + speakingStateTimeoutRef.current = null; + }, 300); + + return () => { + if (speakingStateTimeoutRef.current) { + clearTimeout(speakingStateTimeoutRef.current); + } + }; + }, [localSpeaking, updateSpeakingState]); + + // Вычисляем количество камер для grid + const activeCamerasCount = + (localStream ? 8 : 0) + + participants.filter( + (p) => p.stream != null && p.stream.getTracks().length > 0 + ).length; // Определяем количество колонок в зависимости от количества камер // 1-2 камеры: 1 колонка (друг под другом), 3-4: 2 колонки, 5-9: 3 колонки, 10-16: 4 колонки, 17-25: 5 колонок - const gridColumns = useMemo(() => { - if (activeCamerasCount <= 2) return 1; - if (activeCamerasCount <= 4) return 2; - if (activeCamerasCount <= 9) return 3; - if (activeCamerasCount <= 16) return 4; + const getGridColumns = (count: number): number => { + if (count <= 2) return 1; + if (count <= 4) return 2; + if (count <= 9) return 3; + if (count <= 16) return 4; return 5; - }, [activeCamerasCount]); + }; + + const gridColumns = getGridColumns(activeCamerasCount); // Вычисляем количество рядов для правильного расчета высоты - const gridRows = useMemo( - () => Math.ceil(activeCamerasCount / gridColumns), - [activeCamerasCount, gridColumns] - ); - - // Фильтруем участников с активными потоками - мемоизируем для избежания лишних фильтраций - const activeParticipants = useMemo( - () => - participants.filter( - (participant) => - participant.stream != null && - participant.stream.getTracks().length > 0 - ), - [participants] - ); + const gridRows = Math.ceil(activeCamerasCount / gridColumns); // Рендерим камеры const camerasContent = ( @@ -92,7 +107,13 @@ function SessionUsersPanel({ )} {/* Камеры удаленных участников - показываем только если есть поток с активными треками */} - {activeParticipants.map((participant) => ( + {participants + .filter( + (participant) => + participant.stream != null && + participant.stream.getTracks().length > 0 + ) + .map((participant) => ( (localSpeaking); + useEffect(() => { - if (isLocal && onSpeakingChange) { + if (isLocal && onSpeakingChange && prevLocalSpeakingRef.current !== localSpeaking) { + prevLocalSpeakingRef.current = localSpeaking; onSpeakingChange(localSpeaking); } }, [isLocal, localSpeaking, onSpeakingChange]); @@ -83,16 +87,16 @@ export default function UserCamera({ // isSpeaking уже учитывает threshold и debounce (1 сек) const ringOpacity = isSpeaking ? 1 : 0; - // Логируем для отладки - useEffect(() => { - console.log( - `[${name}${ - isLocal ? " (local)" : "" - }] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed( - 2 - )}, isMuted: ${isMuted}` - ); - }, [isSpeaking, ringOpacity, name, isMuted, isLocal]); + // Логируем для отладки (отключено для снижения шума) + // useEffect(() => { + // console.log( + // `[${name}${ + // isLocal ? " (local)" : "" + // }] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed( + // 2 + // )}, isMuted: ${isMuted}` + // ); + // }, [isSpeaking, ringOpacity, name, isMuted, isLocal]); useEffect(() => { if (ref.current && mediaStream) { diff --git a/client/src/hooks/useVoiceActivity.ts b/client/src/hooks/useVoiceActivity.ts index f3fbd8f..296f22b 100644 --- a/client/src/hooks/useVoiceActivity.ts +++ b/client/src/hooks/useVoiceActivity.ts @@ -132,9 +132,9 @@ export function useVoiceActivity( } } - // Логируем каждые 30 вызовов (~500ms при частоте 60 Hz) + // Логируем каждые 180 вызовов (~3 секунды при частоте 60 Hz) - снижено для меньшего шума frameCount++; - if (frameCount % 30 === 0) { + if (frameCount % 180 === 0) { console.log( `[VoiceActivity] Level: ${audioLevel.toFixed( 1 diff --git a/client/src/hooks/useWebRTC.ts b/client/src/hooks/useWebRTC.ts index 821ded3..3e3a34f 100644 --- a/client/src/hooks/useWebRTC.ts +++ b/client/src/hooks/useWebRTC.ts @@ -19,6 +19,14 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { const [isConnected, setIsConnected] = useState(false); const [isInitialized, setIsInitialized] = useState(false); + // Мониторинг изменений участников (отключено для снижения шума в консоли) + // useEffect(() => { + // console.log("[useWebRTC] Participants state updated:", participants.map(p => ({ + // id: p.id, + // hasStream: !!p.stream, + // }))); + // }, [participants]); + useEffect(() => { // Создаем сервис только один раз (синглтон) if (!webrtcServiceInstance) { @@ -63,11 +71,6 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { setParticipants((prev) => { const existing = prev.find((p) => p.id === participantId); if (existing) { - // Если поток уже тот же самый, не обновляем - if (existing.stream === stream) { - console.log("[useWebRTC] Stream already set for:", participantId); - return prev; - } console.log("[useWebRTC] Updating stream for existing participant:", participantId); return prev.map((p) => p.id === participantId ? { ...p, stream } : p @@ -97,43 +100,26 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { }, onParticipantAudioToggle: (participantId, isEnabled) => { console.log(`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`); - setParticipants((prev) => { - const participant = prev.find((p) => p.id === participantId); - const newMutedState = !isEnabled; - // Только обновляем, если значение действительно изменилось - if (participant && participant.isMuted === newMutedState) { - return prev; - } - return prev.map((p) => - p.id === participantId ? { ...p, isMuted: newMutedState } : p - ); - }); + setParticipants((prev) => + prev.map((p) => + p.id === participantId ? { ...p, isMuted: !isEnabled } : p + ) + ); }, onParticipantVideoToggle: (participantId, isEnabled) => { console.log(`[useWebRTC] Video toggle for ${participantId}: ${isEnabled}`); - setParticipants((prev) => { - const participant = prev.find((p) => p.id === participantId); - const newVideoOffState = !isEnabled; - // Только обновляем, если значение действительно изменилось - if (participant && participant.isVideoOff === newVideoOffState) { - return prev; - } - return prev.map((p) => - p.id === participantId ? { ...p, isVideoOff: newVideoOffState } : p - ); - }); + setParticipants((prev) => + prev.map((p) => + p.id === participantId ? { ...p, isVideoOff: !isEnabled } : p + ) + ); }, onParticipantSpeakingChange: (participantId, isSpeaking) => { - setParticipants((prev) => { - const participant = prev.find((p) => p.id === participantId); - // Только обновляем, если значение действительно изменилось - if (participant && participant.isSpeaking === isSpeaking) { - return prev; - } - return prev.map((p) => + setParticipants((prev) => + prev.map((p) => p.id === participantId ? { ...p, isSpeaking } : p - ); - }); + ) + ); }, onChatMessage: (message) => { console.log("[useWebRTC] onChatMessage called:", message);