Merge branch 'main' of http://192.168.1.163:3000/inmake/stream.graff.tech-new
This commit is contained in:
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
VITE_API_URL=http://localhost:3000
|
# VITE_API_URL=http://localhost:3000
|
||||||
VITE_WEBRTC_URL=http://localhost:3001
|
# VITE_WEBRTC_URL=http://localhost:3001
|
||||||
# VITE_API_URL=https://stream.graff.estate/api
|
VITE_API_URL=https://stream.graff.estate/api
|
||||||
# VITE_WEBRTC_URL=https://stream.graff.estate
|
VITE_WEBRTC_URL=https://stream.graff.estate
|
||||||
@@ -496,7 +496,7 @@ export default function DraggableContainer({
|
|||||||
|
|
||||||
// Если компонент отключен, просто рендерим children без стилей и логики
|
// Если компонент отключен, просто рендерим children без стилей и логики
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return <>{children}</>;
|
return <div className={className}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useWebRTC } from "../hooks/useWebRTC";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const LOCAL_CAMERAS_COUNT = 10;
|
||||||
|
|
||||||
interface SessionUsersPanelProps {
|
interface SessionUsersPanelProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
autoJoin?: boolean;
|
autoJoin?: boolean;
|
||||||
@@ -33,11 +35,59 @@ function SessionUsersPanel({
|
|||||||
const lastSentSpeakingRef = useRef<boolean>(false);
|
const lastSentSpeakingRef = useRef<boolean>(false);
|
||||||
const speakingStateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const speakingStateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// State для отслеживания размеров и ориентации экрана
|
||||||
|
const [windowDimensions, setWindowDimensions] = useState({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
// Callback для получения изменений состояния speaking от UserCamera
|
// Callback для получения изменений состояния speaking от UserCamera
|
||||||
const handleSpeakingChange = (isSpeaking: boolean) => {
|
const handleSpeakingChange = (isSpeaking: boolean) => {
|
||||||
setLocalSpeaking(isSpeaking);
|
setLocalSpeaking(isSpeaking);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// useEffect для отслеживания изменения ориентации и размеров экрана
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowDimensions({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrientationChange = () => {
|
||||||
|
// Небольшая задержка для корректного получения новых размеров после поворота
|
||||||
|
setTimeout(() => {
|
||||||
|
setWindowDimensions({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Слушаем событие resize (срабатывает при изменении размера окна)
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
// Слушаем событие orientationchange (срабатывает при повороте устройства)
|
||||||
|
window.addEventListener("orientationchange", handleOrientationChange);
|
||||||
|
|
||||||
|
// Также слушаем изменения в screen.orientation API (современный способ)
|
||||||
|
if (screen.orientation) {
|
||||||
|
screen.orientation.addEventListener("change", handleOrientationChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
window.removeEventListener("orientationchange", handleOrientationChange);
|
||||||
|
if (screen.orientation) {
|
||||||
|
screen.orientation.removeEventListener(
|
||||||
|
"change",
|
||||||
|
handleOrientationChange
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// useEffect для throttle и отправки состояния speaking через socket
|
// useEffect для throttle и отправки состояния speaking через socket
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Отправляем только если состояние действительно изменилось
|
// Отправляем только если состояние действительно изменилось
|
||||||
@@ -66,45 +116,80 @@ function SessionUsersPanel({
|
|||||||
|
|
||||||
// Вычисляем количество камер для grid
|
// Вычисляем количество камер для grid
|
||||||
const activeCamerasCount =
|
const activeCamerasCount =
|
||||||
(localStream ? 8 : 0) +
|
(localStream ? LOCAL_CAMERAS_COUNT : 0) +
|
||||||
participants.filter(
|
participants.filter(
|
||||||
(p) => p.stream != null && p.stream.getTracks().length > 0
|
(p) => p.stream != null && p.stream.getTracks().length > 0
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
// Определяем количество колонок в зависимости от количества камер
|
// Определяем количество колонок в зависимости от количества камер
|
||||||
// 1-2 камеры: 1 колонка (друг под другом), 3-4: 2 колонки, 5-9: 3 колонки, 10-16: 4 колонки, 17-25: 5 колонок
|
const getDesktopGridColumns = (count: number): number => {
|
||||||
const getGridColumns = (count: number): number => {
|
|
||||||
if (count <= 2) return 1;
|
if (count <= 2) return 1;
|
||||||
if (count <= 4) return 2;
|
if (count <= 4) return 2;
|
||||||
if (count <= 9) return 3;
|
if (count <= 9) return 3;
|
||||||
if (count <= 16) return 4;
|
return 4;
|
||||||
return 5;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const gridColumns = getGridColumns(activeCamerasCount);
|
const getMobilePortraitGridColumns = (count: number): number => {
|
||||||
|
if (count <= 3) return 1;
|
||||||
|
if (count <= 12) return 2;
|
||||||
|
return 3;
|
||||||
|
};
|
||||||
|
|
||||||
// Вычисляем количество рядов для правильного расчета высоты
|
const getMobileLandscapeGridColumns = (count: number): number => {
|
||||||
const gridRows = Math.ceil(activeCamerasCount / gridColumns);
|
if (count <= 3) return count;
|
||||||
|
if (count <= 6) return 3;
|
||||||
|
return 4;
|
||||||
|
};
|
||||||
|
|
||||||
// Рендерим камеры
|
const gridColumns =
|
||||||
const camerasContent = (
|
windowDimensions.width >= 1440
|
||||||
<>
|
? getDesktopGridColumns(activeCamerasCount)
|
||||||
{/* Локальная камера пользователя - показываем только если есть разрешение */}
|
: windowDimensions.height / windowDimensions.width < 1
|
||||||
{localStream && (
|
? getMobileLandscapeGridColumns(activeCamerasCount)
|
||||||
<UserCamera
|
: getMobilePortraitGridColumns(activeCamerasCount);
|
||||||
mode={mode}
|
|
||||||
name="Вы"
|
return (
|
||||||
isMuted={isLocalAudioMuted}
|
<DraggableContainer
|
||||||
isVideoOff={isLocalVideoMuted}
|
enableSnapping={mode === "full"}
|
||||||
isControlDisabled={false}
|
enabled={mode === "full"}
|
||||||
isAdmin={true}
|
autoAlign={true}
|
||||||
isLocal={true}
|
initialCorner={
|
||||||
mediaStream={localStream}
|
windowDimensions.width >= 640 ? "bottom-right" : "top-right"
|
||||||
onMute={toggleAudio}
|
}
|
||||||
onVideoOff={toggleVideo}
|
padding="1.111vw"
|
||||||
onCanControl={() => console.log("Toggle control")}
|
className={clsx(
|
||||||
onSpeakingChange={handleSpeakingChange}
|
"z-[999] 2xl:gap-[0.556vw] gap-2",
|
||||||
/>
|
mode === "full"
|
||||||
|
? "flex"
|
||||||
|
: `2xl:p-[5vw] p-4 w-full 2xl:h-dvh max-2xl:portrait:max-h-[calc(100dvh-17.778vw)] max-2xl:landscape:max-h-[calc(100dvh-8.75vw)] grid grid-cols-${gridColumns}`
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
{localStream &&
|
||||||
|
Array.from({ length: LOCAL_CAMERAS_COUNT }).map((_, index) => (
|
||||||
|
<UserCamera
|
||||||
|
key={index}
|
||||||
|
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}
|
||||||
|
className={clsx(
|
||||||
|
mode === "mini" &&
|
||||||
|
(activeCamerasCount <= 2
|
||||||
|
? "m-auto"
|
||||||
|
: activeCamerasCount > 12
|
||||||
|
? "!aspect-square w-full"
|
||||||
|
: "w-full")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Камеры удаленных участников - показываем только если есть поток с активными треками */}
|
{/* Камеры удаленных участников - показываем только если есть поток с активными треками */}
|
||||||
{participants
|
{participants
|
||||||
@@ -115,6 +200,14 @@ function SessionUsersPanel({
|
|||||||
)
|
)
|
||||||
.map((participant) => (
|
.map((participant) => (
|
||||||
<UserCamera
|
<UserCamera
|
||||||
|
className={clsx(
|
||||||
|
mode === "mini" &&
|
||||||
|
(activeCamerasCount <= 2
|
||||||
|
? "m-auto"
|
||||||
|
: activeCamerasCount > 12
|
||||||
|
? "!aspect-square w-full"
|
||||||
|
: "w-full")
|
||||||
|
)}
|
||||||
key={participant.id}
|
key={participant.id}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
name={participant.id}
|
name={participant.id}
|
||||||
@@ -141,47 +234,32 @@ function SessionUsersPanel({
|
|||||||
isVideoMuted={isLocalVideoMuted}
|
isVideoMuted={isLocalVideoMuted}
|
||||||
hasLocalStream={hasLocalStream}
|
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
|
// Для режима mini используем flex-обертку для центрирования и внутри grid
|
||||||
return (
|
// return (
|
||||||
<div className="flex justify-center items-center w-full h-full z-[99]a">
|
// <div className="flex justify-center items-center w-full h-full z-[99]a">
|
||||||
<div
|
// <div
|
||||||
className={clsx(
|
// className={clsx(
|
||||||
"grid 2xl:gap-[0.556vw] gap-2",
|
// "grid 2xl:gap-[0.556vw] gap-2",
|
||||||
gridColumns === 1 && "grid-cols-1 2xl:w-[45vw] w-[calc(50vw-1rem)]",
|
// 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 === 2 && "grid-cols-2 2xl:w-[90vw] w-[calc(100vw-2rem)]",
|
||||||
gridColumns === 3 && "grid-cols-3 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 === 4 && "grid-cols-4 2xl:w-[90vw] w-[calc(100vw-2rem)]",
|
||||||
gridColumns === 5 && "grid-cols-5 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 === 1 && "auto-rows-[calc((86vh-1.111vw)/1)]",
|
||||||
gridRows === 2 && "auto-rows-[calc((86vh-1.667vw)/2)]",
|
// gridRows === 2 && "auto-rows-[calc((86vh-1.667vw)/2)]",
|
||||||
gridRows === 3 && "auto-rows-[calc((86vh-2.222vw)/3)]",
|
// gridRows === 3 && "auto-rows-[calc((86vh-2.222vw)/3)]",
|
||||||
gridRows === 4 && "auto-rows-[calc((86vh-2.778vw)/4)]",
|
// gridRows === 4 && "auto-rows-[calc((86vh-2.778vw)/4)]",
|
||||||
gridRows === 5 && "auto-rows-[calc((86vh-3.333vw)/5)]"
|
// gridRows === 5 && "auto-rows-[calc((86vh-3.333vw)/5)]"
|
||||||
)}
|
// )}
|
||||||
>
|
// >
|
||||||
{camerasContent}
|
// {camerasContent}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SessionUsersPanel;
|
export default SessionUsersPanel;
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
|
|||||||
|
|
||||||
function handleClickOpenSharePopup() {
|
function handleClickOpenSharePopup() {
|
||||||
setIsOpened(false);
|
setIsOpened(false);
|
||||||
setPopup(<SharePopup link="https://estate.stream/ahdy12jdco1" />);
|
setPopup(
|
||||||
|
<SharePopup link={`${window.location.origin}/sessions/${session?.id}`} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickOpenSettingsModal() {
|
function handleClickOpenSettingsModal() {
|
||||||
@@ -54,7 +56,7 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="order-3 2xl:hidden" ref={ref}>
|
<div className="order-3 2xl:hidden z-[9999]" ref={ref}>
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className={clsx(isOpened && "!bg-[#7B60F3]")}
|
className={clsx(isOpened && "!bg-[#7B60F3]")}
|
||||||
|
|||||||
@@ -75,9 +75,13 @@ export default function UserCamera({
|
|||||||
// Отправляем изменения состояния для локального пользователя
|
// Отправляем изменения состояния для локального пользователя
|
||||||
// Используем ref для отслеживания предыдущего состояния, чтобы избежать лишних вызовов
|
// Используем ref для отслеживания предыдущего состояния, чтобы избежать лишних вызовов
|
||||||
const prevLocalSpeakingRef = useRef<boolean>(localSpeaking);
|
const prevLocalSpeakingRef = useRef<boolean>(localSpeaking);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLocal && onSpeakingChange && prevLocalSpeakingRef.current !== localSpeaking) {
|
if (
|
||||||
|
isLocal &&
|
||||||
|
onSpeakingChange &&
|
||||||
|
prevLocalSpeakingRef.current !== localSpeaking
|
||||||
|
) {
|
||||||
prevLocalSpeakingRef.current = localSpeaking;
|
prevLocalSpeakingRef.current = localSpeaking;
|
||||||
onSpeakingChange(localSpeaking);
|
onSpeakingChange(localSpeaking);
|
||||||
}
|
}
|
||||||
@@ -276,7 +280,7 @@ export default function UserCamera({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
mode === "full"
|
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-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",
|
: "aspect-video 2xl:rounded-[2.222vw] rounded-[32px] overflow-hidden group relative pointer-events-auto max-w-full h-full self-center object-cover",
|
||||||
isLocal && "order-last",
|
isLocal && "order-last",
|
||||||
isVideoOff ? "bg-green-500" : "bg-yellow-500/10",
|
isVideoOff ? "bg-green-500" : "bg-yellow-500/10",
|
||||||
className
|
className
|
||||||
@@ -290,7 +294,9 @@ export default function UserCamera({
|
|||||||
window.innerWidth >= 1536 ? "0.069vw" : "1px"
|
window.innerWidth >= 1536 ? "0.069vw" : "1px"
|
||||||
} rgba(255, 255, 255, 0.3)`,
|
} rgba(255, 255, 255, 0.3)`,
|
||||||
transition:
|
transition:
|
||||||
"box-shadow 0.1s ease-out, width 0.3s, background-color 0.3s",
|
mode === "full"
|
||||||
|
? "box-shadow 0.1s ease-out, width 0.3s, background-color 0.3s"
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
onClick={handleVideoClick}
|
onClick={handleVideoClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ function SessionPage() {
|
|||||||
const session = sessionData?.session;
|
const session = sessionData?.session;
|
||||||
|
|
||||||
function handleChatOpen() {
|
function handleChatOpen() {
|
||||||
console.log("handleChatOpen");
|
|
||||||
setPopup(<ChatPopup sessionId={id} />);
|
setPopup(<ChatPopup sessionId={id} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +98,7 @@ function SessionPage() {
|
|||||||
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
|
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [mode, setMode] = useState<"full" | "mini">("full");
|
const [mode, setMode] = useState<"full" | "mini">("mini");
|
||||||
|
|
||||||
function toggleMode() {
|
function toggleMode() {
|
||||||
setMode(mode === "full" ? "mini" : "full");
|
setMode(mode === "full" ? "mini" : "full");
|
||||||
@@ -161,9 +160,8 @@ function SessionPage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
mode === "full"
|
"overflow-hidden relative order-3 w-screen h-dvh bg-black touch-none max-2xl:flex max-2xl:portrait:items-center",
|
||||||
? "flex overflow-hidden relative order-3 w-screen h-dvh bg-black justify-center_items-center touch-none"
|
mode === "full" && "flex"
|
||||||
: "2xl:px-[5vw] grid 2xl:gap-[0.556vw] gap-2 bg-black relative w-screen h-dvh overflow-hidden"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Pixel Streaming - показывается только когда сессия активна */}
|
{/* Pixel Streaming - показывается только когда сессия активна */}
|
||||||
@@ -206,7 +204,7 @@ function SessionPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ActionsSidebarWrapper className="z-[99]">
|
<ActionsSidebarWrapper className="z-[9999]">
|
||||||
<FloatingActionButton onClick={toggleMode}>
|
<FloatingActionButton onClick={toggleMode}>
|
||||||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||||||
{mode === "mini" ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
{mode === "mini" ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||||||
|
|||||||
Reference in New Issue
Block a user