167 lines
5.8 KiB
TypeScript
167 lines
5.8 KiB
TypeScript
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";
|
|
|
|
interface SessionUsersPanelProps {
|
|
roomId: string;
|
|
autoJoin?: boolean;
|
|
mode?: "full" | "mini";
|
|
}
|
|
|
|
function SessionUsersPanel({
|
|
roomId,
|
|
autoJoin = false,
|
|
mode = "full",
|
|
}: SessionUsersPanelProps) {
|
|
const {
|
|
localStream,
|
|
participants,
|
|
isAudioMuted: isLocalAudioMuted,
|
|
isVideoMuted: isLocalVideoMuted,
|
|
toggleAudio,
|
|
toggleVideo,
|
|
updateSpeakingState,
|
|
} = useWebRTC(roomId, autoJoin);
|
|
|
|
const hasLocalStream = localStream !== null;
|
|
|
|
// Callback для отправки состояния speaking
|
|
const handleSpeakingChange = (isSpeaking: boolean) => {
|
|
updateSpeakingState?.(isSpeaking);
|
|
};
|
|
|
|
// Вычисляем количество камер для 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 gridColumns = useMemo(() => {
|
|
if (activeCamerasCount <= 2) return 1;
|
|
if (activeCamerasCount <= 4) return 2;
|
|
if (activeCamerasCount <= 9) return 3;
|
|
if (activeCamerasCount <= 16) return 4;
|
|
return 5;
|
|
}, [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 camerasContent = (
|
|
<>
|
|
{/* Локальная камера пользователя - показываем только если есть разрешение */}
|
|
{localStream && (
|
|
<UserCamera
|
|
mode={mode}
|
|
name="Вы"
|
|
isMuted={isLocalAudioMuted}
|
|
isVideoOff={isLocalVideoMuted}
|
|
isControlDisabled={false}
|
|
isAdmin={true}
|
|
isLocal={true}
|
|
mediaStream={localStream}
|
|
onMute={toggleAudio}
|
|
onVideoOff={toggleVideo}
|
|
onCanControl={() => console.log("Toggle control")}
|
|
onSpeakingChange={handleSpeakingChange}
|
|
/>
|
|
)}
|
|
|
|
{/* Камеры удаленных участников - показываем только если есть поток с активными треками */}
|
|
{activeParticipants.map((participant) => (
|
|
<UserCamera
|
|
key={participant.id}
|
|
mode={mode}
|
|
name={participant.id}
|
|
isMuted={participant.isMuted || false}
|
|
isVideoOff={participant.isVideoOff || false}
|
|
isSpeaking={participant.isSpeaking}
|
|
isControlDisabled={true}
|
|
isAdmin={true}
|
|
mediaStream={participant.stream}
|
|
hasLocalMediaPermission={hasLocalStream}
|
|
onMute={() => console.log(`Mute user ${participant.id}`)}
|
|
onVideoOff={() => console.log(`Video off user ${participant.id}`)}
|
|
onCanControl={() =>
|
|
console.log(`Can control user ${participant.id}`)
|
|
}
|
|
/>
|
|
))}
|
|
|
|
<UserDevicesControls
|
|
mode={mode}
|
|
toggleAudio={toggleAudio}
|
|
toggleVideo={toggleVideo}
|
|
isAudioMuted={isLocalAudioMuted}
|
|
isVideoMuted={isLocalVideoMuted}
|
|
hasLocalStream={hasLocalStream}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
// Для режима full используем DraggableContainer
|
|
if (mode === "full") {
|
|
return (
|
|
<DraggableContainer
|
|
enableSnapping={true}
|
|
enabled={true}
|
|
autoAlign={true}
|
|
initialCorner={innerWidth >= 640 ? "bottom-right" : "top-right"}
|
|
padding="1.111vw"
|
|
className="z-[999] flex gap-4"
|
|
>
|
|
{camerasContent}
|
|
</DraggableContainer>
|
|
);
|
|
}
|
|
|
|
// Для режима mini используем flex-обертку для центрирования и внутри grid
|
|
return (
|
|
<div className="flex justify-center items-center w-full h-full z-[99]a">
|
|
<div
|
|
className={clsx(
|
|
"grid 2xl:gap-[0.556vw] gap-2",
|
|
gridColumns === 1 && "grid-cols-1 2xl:w-[45vw] w-[calc(50vw-1rem)]",
|
|
gridColumns === 2 && "grid-cols-2 2xl:w-[90vw] w-[calc(100vw-2rem)]",
|
|
gridColumns === 3 && "grid-cols-3 2xl:w-[90vw] w-[calc(100vw-2rem)]",
|
|
gridColumns === 4 && "grid-cols-4 2xl:w-[90vw] w-[calc(100vw-2rem)]",
|
|
gridColumns === 5 && "grid-cols-5 2xl:w-[90vw] w-[calc(100vw-2rem)]",
|
|
gridRows === 1 && "auto-rows-[calc((86vh-1.111vw)/1)]",
|
|
gridRows === 2 && "auto-rows-[calc((86vh-1.667vw)/2)]",
|
|
gridRows === 3 && "auto-rows-[calc((86vh-2.222vw)/3)]",
|
|
gridRows === 4 && "auto-rows-[calc((86vh-2.778vw)/4)]",
|
|
gridRows === 5 && "auto-rows-[calc((86vh-3.333vw)/5)]"
|
|
)}
|
|
>
|
|
{camerasContent}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default SessionUsersPanel;
|