Merge branch 'main' of http://192.168.1.163:3000/inmake/stream.graff.tech-new
This commit is contained in:
@@ -2,16 +2,18 @@ import UserCamera from "./ui/UserCamera";
|
||||
import UserDevicesControls from "./ui/UserDevicesControls";
|
||||
import DraggableContainer from "./DraggableContainer";
|
||||
import { useWebRTC } from "../hooks/useWebRTC";
|
||||
import { useCallback } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface SessionUsersPanelProps {
|
||||
roomId: string;
|
||||
autoJoin?: boolean;
|
||||
mode?: "full" | "mini";
|
||||
}
|
||||
|
||||
function SessionUsersPanel({
|
||||
roomId,
|
||||
autoJoin = false,
|
||||
mode = "full",
|
||||
}: SessionUsersPanelProps) {
|
||||
const {
|
||||
localStream,
|
||||
@@ -26,21 +28,38 @@ function SessionUsersPanel({
|
||||
const hasLocalStream = localStream !== null;
|
||||
|
||||
// Callback для отправки состояния speaking
|
||||
const handleSpeakingChange = useCallback((isSpeaking: boolean) => {
|
||||
const handleSpeakingChange = (isSpeaking: boolean) => {
|
||||
updateSpeakingState?.(isSpeaking);
|
||||
}, [updateSpeakingState]);
|
||||
};
|
||||
|
||||
return (
|
||||
<DraggableContainer
|
||||
enableSnapping={true}
|
||||
autoAlign={true}
|
||||
initialCorner={innerWidth >= 640 ? "bottom-right" : "top-right"}
|
||||
padding="1.111vw"
|
||||
className="flex gap-4"
|
||||
>
|
||||
// Вычисляем количество камер для 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 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;
|
||||
};
|
||||
|
||||
const gridColumns = getGridColumns(activeCamerasCount);
|
||||
|
||||
// Вычисляем количество рядов для правильного расчета высоты
|
||||
const gridRows = Math.ceil(activeCamerasCount / gridColumns);
|
||||
|
||||
// Рендерим камеры
|
||||
const camerasContent = (
|
||||
<>
|
||||
{/* Локальная камера пользователя - показываем только если есть разрешение */}
|
||||
{localStream && (
|
||||
<UserCamera
|
||||
mode={mode}
|
||||
name="Вы"
|
||||
isMuted={isLocalAudioMuted}
|
||||
isVideoOff={isLocalVideoMuted}
|
||||
@@ -65,12 +84,13 @@ function SessionUsersPanel({
|
||||
.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} // Локальный пользователь - админ своей сессии
|
||||
isAdmin={true}
|
||||
mediaStream={participant.stream}
|
||||
hasLocalMediaPermission={hasLocalStream}
|
||||
onMute={() => console.log(`Mute user ${participant.id}`)}
|
||||
@@ -82,13 +102,53 @@ function SessionUsersPanel({
|
||||
))}
|
||||
|
||||
<UserDevicesControls
|
||||
mode={mode}
|
||||
toggleAudio={toggleAudio}
|
||||
toggleVideo={toggleVideo}
|
||||
isAudioMuted={isLocalAudioMuted}
|
||||
isVideoMuted={isLocalVideoMuted}
|
||||
hasLocalStream={hasLocalStream}
|
||||
/>
|
||||
</DraggableContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
// Для режима 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ interface UserCameraProps {
|
||||
isSpeaking?: boolean; // Для удаленных участников - получаем по Socket.IO
|
||||
onSpeakingChange?: (isSpeaking: boolean) => void; // Для локального - отправляем изменения
|
||||
hasLocalMediaPermission?: boolean; // Есть ли у локального пользователя разрешение на медиа
|
||||
mode: "full" | "mini";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function UserCamera({
|
||||
@@ -53,6 +55,8 @@ export default function UserCamera({
|
||||
isSpeaking: remoteSpeaking,
|
||||
onSpeakingChange,
|
||||
hasLocalMediaPermission = false,
|
||||
mode = "full",
|
||||
className,
|
||||
}: UserCameraProps) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
// Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay)
|
||||
@@ -243,9 +247,7 @@ export default function UserCamera({
|
||||
const newMutedState = !isAudioMuted;
|
||||
setIsAudioMuted(newMutedState);
|
||||
console.log(
|
||||
`[UserCamera] ${name} audio ${
|
||||
newMutedState ? "muted" : "unmuted"
|
||||
}`
|
||||
`[UserCamera] ${name} audio ${newMutedState ? "muted" : "unmuted"}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -268,9 +270,12 @@ export default function UserCamera({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"aspect-square h-fit group 2xl:rounded-[1.667vw] rounded-2xl relative flex-shrink-0 pointer-events-auto 2xl:hover:w-[10.833vw] 2xl:w-[6.944vw] sm:w-[15.625vw] w-[27.778vw] overflow-hidden",
|
||||
mode === "full"
|
||||
? "aspect-square h-fit group 2xl:rounded-[1.667vw] rounded-2xl relative flex-shrink-0 pointer-events-auto 2xl:hover:w-[10.833vw] 2xl:w-[6.944vw] sm:w-[15.625vw] w-[27.778vw] overflow-hidden"
|
||||
: "aspect-video 2xl:rounded-[2.222vw] rounded-[32px] overflow-hidden group relative flex-shrink-0 pointer-events-auto w-full h-full object-contain",
|
||||
isLocal && "order-last",
|
||||
isVideoOff ? "bg-green-500" : "bg-yellow-500/10"
|
||||
isVideoOff ? "bg-green-500" : "bg-yellow-500/10",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
boxShadow: isSpeaking
|
||||
|
||||
@@ -7,6 +7,7 @@ import CogFilledIcon from "../icons/CogFilledIcon";
|
||||
import useModalStore from "../../store/modalStore";
|
||||
import SettingsModal from "../modals/SettingsModal";
|
||||
import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon";
|
||||
import clsx from "clsx";
|
||||
|
||||
export interface UserDevicesControlsProps {
|
||||
toggleAudio: () => void;
|
||||
@@ -14,6 +15,7 @@ export interface UserDevicesControlsProps {
|
||||
isAudioMuted: boolean;
|
||||
isVideoMuted: boolean;
|
||||
hasLocalStream?: boolean;
|
||||
mode?: "full" | "mini";
|
||||
}
|
||||
|
||||
export default function UserDevicesControls({
|
||||
@@ -22,6 +24,7 @@ export default function UserDevicesControls({
|
||||
isAudioMuted,
|
||||
isVideoMuted,
|
||||
hasLocalStream = true,
|
||||
mode = "full",
|
||||
}: UserDevicesControlsProps) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
@@ -30,7 +33,13 @@ export default function UserDevicesControls({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden order-last 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto">
|
||||
<div
|
||||
className={clsx(
|
||||
mode === "full"
|
||||
? "hidden order-last 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto"
|
||||
: "2xl:flex hidden items-center gap-[0.278vw] p-[0.556vw] rounded-[1.667vw] ring-[0.104vw] ring-[#FFFFFF]/15 absolute bottom-[0.556vw] left-1/2 -translate-x-1/2 bg-[#00000040] backdrop-blur-[10px]"
|
||||
)}
|
||||
>
|
||||
<ControlButton
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
size="large"
|
||||
|
||||
@@ -26,6 +26,7 @@ import SessionUsersPanel from "../components/SessionUsersPanel";
|
||||
import { useWebRTC } from "../hooks/useWebRTC";
|
||||
import MicrophoneOffFilledIcon from "../components/icons/MicrophoneOffFilledIcon";
|
||||
import VideoFilledIcon from "../components/icons/VideoFilledIcon";
|
||||
import clsx from "clsx";
|
||||
|
||||
function SessionPage() {
|
||||
const { setPopup } = usePopupStore();
|
||||
@@ -93,6 +94,12 @@ function SessionPage() {
|
||||
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
|
||||
}
|
||||
|
||||
const [mode, setMode] = useState<"full" | "mini">("full");
|
||||
|
||||
function toggleMode() {
|
||||
setMode(mode === "full" ? "mini" : "full");
|
||||
}
|
||||
|
||||
// Не перенаправляем автоматически - пользователи могут продолжать общаться
|
||||
// useEffect(() => {
|
||||
// if (session?.status === "ended") {
|
||||
@@ -147,13 +154,19 @@ function SessionPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex overflow-hidden relative order-3 w-screen h-dvh bg-black justify-center_items-center touch-none">
|
||||
<div
|
||||
className={clsx(
|
||||
mode === "full"
|
||||
? "flex overflow-hidden relative order-3 w-screen h-dvh bg-black justify-center_items-center touch-none"
|
||||
: "2xl:px-[5vw] grid 2xl:gap-[0.556vw] gap-2 bg-black relative w-screen h-dvh overflow-hidden"
|
||||
)}
|
||||
>
|
||||
{/* Pixel Streaming - показывается только когда сессия активна */}
|
||||
{session.status === "started" &&
|
||||
session.mode === "stream" &&
|
||||
session.server?.localIp &&
|
||||
session.playerPort && (
|
||||
<div className="w-full h-full aspect-video">
|
||||
<div className=" absolute w-full h-full aspect-video">
|
||||
<PixelStreamingWrapper
|
||||
initialSettings={{
|
||||
ss: `ws://${session.server.localIp}:${session.playerPort}`,
|
||||
@@ -189,6 +202,11 @@ function SessionPage() {
|
||||
)}
|
||||
|
||||
<ActionsSidebarWrapper className="z-[99]">
|
||||
<FloatingActionButton onClick={toggleMode}>
|
||||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||||
{mode === "mini" ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
<FloatingActionButton
|
||||
className="max-2xl:hidden"
|
||||
onClick={handleChatOpen}
|
||||
@@ -256,7 +274,7 @@ function SessionPage() {
|
||||
</ActionsSidebarWrapper>
|
||||
|
||||
{/* WebRTC видеочат - работает всегда, пока пользователь на странице */}
|
||||
<SessionUsersPanel roomId={session.id} autoJoin={true} />
|
||||
<SessionUsersPanel roomId={session.id} autoJoin={true} mode={mode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user