From 1cacd070ebc57eda3bfcb09c138a4c516a019cc4 Mon Sep 17 00:00:00 2001 From: inmake Date: Thu, 30 Oct 2025 18:14:44 +0500 Subject: [PATCH] Enhance SessionUsersPanel and UserCamera components by implementing local speaking state management with debounce for efficient updates. Refactor grid column calculation and participant filtering for improved performance. Adjust logging in useVoiceActivity and useWebRTC hooks to reduce console noise. --- client/src/components/SessionUsersPanel.tsx | 89 +++++++++++++-------- client/src/components/ui/UserCamera.tsx | 26 +++--- client/src/hooks/useVoiceActivity.ts | 4 +- client/src/hooks/useWebRTC.ts | 58 +++++--------- 4 files changed, 94 insertions(+), 83 deletions(-) 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);