diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx index 47313a1..d708f83 100644 --- a/client/src/components/SessionUsersPanel.tsx +++ b/client/src/components/SessionUsersPanel.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import UserCamera from "./ui/UserCamera"; import UserDevicesControls from "./ui/UserDevicesControls"; import DraggableContainer from "./DraggableContainer"; @@ -32,26 +33,42 @@ function SessionUsersPanel({ updateSpeakingState?.(isSpeaking); }; - // Вычисляем количество камер для grid - const activeCamerasCount = - (localStream ? 8 : 0) + - participants.filter( - (p) => p.stream != null && p.stream.getTracks().length > 0 - ).length; + // Вычисляем количество камер для grid - мемоизируем для избежания лишних вычислений + const activeCamerasCount = useMemo( + () => + (localStream ? 1 : 0) + + participants.filter( + (p) => p.stream != null && p.stream.getTracks().length > 0 + ).length, + [localStream, participants] + ); + // Определяем количество колонок в зависимости от количества камер // 1-2 камеры: 1 колонка (друг под другом), 3-4: 2 колонки, 5-9: 3 колонки, 10-16: 4 колонки, 17-25: 5 колонок - 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; + const gridColumns = useMemo(() => { + if (activeCamerasCount <= 2) return 1; + if (activeCamerasCount <= 4) return 2; + if (activeCamerasCount <= 9) return 3; + if (activeCamerasCount <= 16) return 4; return 5; - }; - - const gridColumns = getGridColumns(activeCamerasCount); + }, [activeCamerasCount]); // Вычисляем количество рядов для правильного расчета высоты - const gridRows = Math.ceil(activeCamerasCount / gridColumns); + 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 camerasContent = ( @@ -75,13 +92,7 @@ function SessionUsersPanel({ )} {/* Камеры удаленных участников - показываем только если есть поток с активными треками */} - {participants - .filter( - (participant) => - participant.stream != null && - participant.stream.getTracks().length > 0 - ) - .map((participant) => ( + {activeParticipants.map((participant) => ( { 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) { @@ -71,6 +63,11 @@ 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 @@ -100,26 +97,43 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => { }, onParticipantAudioToggle: (participantId, isEnabled) => { console.log(`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`); - setParticipants((prev) => - prev.map((p) => - p.id === participantId ? { ...p, isMuted: !isEnabled } : p - ) - ); + 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 + ); + }); }, onParticipantVideoToggle: (participantId, isEnabled) => { console.log(`[useWebRTC] Video toggle for ${participantId}: ${isEnabled}`); - setParticipants((prev) => - prev.map((p) => - p.id === participantId ? { ...p, isVideoOff: !isEnabled } : p - ) - ); + 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 + ); + }); }, onParticipantSpeakingChange: (participantId, isSpeaking) => { - setParticipants((prev) => - prev.map((p) => + setParticipants((prev) => { + const participant = prev.find((p) => p.id === participantId); + // Только обновляем, если значение действительно изменилось + if (participant && participant.isSpeaking === isSpeaking) { + return prev; + } + return prev.map((p) => p.id === participantId ? { ...p, isSpeaking } : p - ) - ); + ); + }); }, onChatMessage: (message) => { console.log("[useWebRTC] onChatMessage called:", message); diff --git a/client/src/pages/SessionPage.tsx b/client/src/pages/SessionPage.tsx index 40cd59a..b5e1827 100644 --- a/client/src/pages/SessionPage.tsx +++ b/client/src/pages/SessionPage.tsx @@ -166,7 +166,7 @@ function SessionPage() { session.mode === "stream" && session.server?.localIp && session.playerPort && ( -
+