import { useEffect, useRef, useState } from "react"; import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon"; import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon"; import VideoOffFilledIcon from "../icons/VideoOffFilledIcon"; import VideoFilledIcon from "../icons/VideoFilledIcon"; import Admin from "../indicators/Admin"; import clsx from "clsx"; import VolumeIcon from "../icons/VolumeIcon"; import VolumeOffIcon from "../icons/VolumeOffIcon"; import { useVoiceActivity } from "../../hooks/useVoiceActivity"; import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon"; interface UserCameraControlsProps { isMuted: boolean; isVideoOff: boolean; hasControl: boolean; } interface UserCameraProps { isMuted: boolean; isVideoOff: boolean; hasControl?: boolean; onMute: () => void; onVideoOff: () => void; onCanControl: () => void; isAdmin?: boolean; name?: string; mediaStream?: MediaStream | null; isLocal?: boolean; isSpeaking?: boolean; // Для удаленных участников - получаем по Socket.IO onSpeakingChange?: (isSpeaking: boolean) => void; // Для локального - отправляем изменения hasLocalMediaPermission?: boolean; // Есть ли у локального пользователя разрешение на медиа mode: "full" | "mini"; className?: string; } export default function UserCamera({ isMuted, isVideoOff, hasControl = false, // onMute, // onVideoOff, // onCanControl, isAdmin = false, name = "Гость", mediaStream = null, isLocal = false, isSpeaking: remoteSpeaking, onSpeakingChange, hasLocalMediaPermission = false, mode = "full", className, }: UserCameraProps) { const ref = useRef(null); // Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay) const [isAudioMuted, setIsAudioMuted] = useState(!hasLocalMediaPermission); // Обновляем состояние muted при изменении hasLocalMediaPermission useEffect(() => { if (!isLocal) { // Для удаленных участников: если у локального нет разрешения - mute для autoplay setIsAudioMuted(!hasLocalMediaPermission); console.log( `[UserCamera] ${name} audio muted state updated to ${!hasLocalMediaPermission} (hasLocalMediaPermission: ${hasLocalMediaPermission})` ); } }, [hasLocalMediaPermission, isLocal, name]); // Явно обновляем атрибут muted у video элемента при изменении isAudioMuted useEffect(() => { if (ref.current && !isLocal) { ref.current.muted = isAudioMuted; console.log( `[UserCamera] ${name} video element muted attribute set to ${isAudioMuted}` ); } }, [isAudioMuted, isLocal, name]); // Детекция голосовой активности (только для локального пользователя) const { isSpeaking: isVoiceActive } = useVoiceActivity( isLocal ? mediaStream : null ); // Для локального - используем локальную детекцию // Для удаленных - используем полученное состояние через Socket.IO const localSpeaking = !isMuted && isVoiceActive; const isSpeaking = isLocal ? localSpeaking : remoteSpeaking || false; // Отправляем изменения состояния для локального пользователя // Используем ref для отслеживания предыдущего состояния, чтобы избежать лишних вызовов const prevLocalSpeakingRef = useRef(localSpeaking); useEffect(() => { if ( isLocal && onSpeakingChange && prevLocalSpeakingRef.current !== localSpeaking ) { prevLocalSpeakingRef.current = localSpeaking; onSpeakingChange(localSpeaking); } }, [isLocal, localSpeaking, onSpeakingChange]); // Рамка либо горит на 100%, либо выключена // 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(() => { if (ref.current && mediaStream) { console.log( `[UserCamera] Setting srcObject for ${name}, isLocal: ${isLocal}, stream:`, mediaStream ); ref.current.srcObject = mediaStream; // Принудительно запускаем воспроизведение ref.current.play().catch((error) => { console.error(`[UserCamera] Failed to play video for ${name}:`, error); }); // Дополнительная попытка воспроизведения с задержкой для Firefox if (!isLocal) { // Попытка через 500ms setTimeout(() => { if (ref.current) { console.log( `[UserCamera] First retry for ${name}, paused: ${ref.current.paused}, readyState: ${ref.current.readyState}` ); if (ref.current.paused) { ref.current.play().catch((error) => { console.error( `[UserCamera] First retry play failed for ${name}:`, error ); }); } } }, 500); // Попытка через 1 секунду setTimeout(() => { if (ref.current && ref.current.paused) { console.log(`[UserCamera] Second retry for ${name} after timeout`); ref.current.play().catch((error) => { console.error( `[UserCamera] Second retry play failed for ${name}:`, error ); }); } }, 1000); // Еще одна попытка через 3 секунды setTimeout(() => { if (ref.current) { console.log( `[UserCamera] Final retry for ${name}, paused: ${ref.current.paused}, readyState: ${ref.current.readyState}` ); if (ref.current.paused) { ref.current.play().catch((error) => { console.error( `[UserCamera] Final retry play failed for ${name}:`, error ); }); } } }, 3000); } } else if (ref.current && !mediaStream) { console.log(`[UserCamera] Clearing srcObject for ${name}`); ref.current.srcObject = null; } }, [mediaStream, name, isLocal]); // Добавляем обработчики событий для отладки useEffect(() => { const videoElement = ref.current; if (!videoElement) return; const handleLoadStart = () => { console.log(`[UserCamera] ${name} video loadstart`); }; const handleLoadedData = () => { console.log(`[UserCamera] ${name} video loadeddata`); }; const handleCanPlay = () => { console.log(`[UserCamera] ${name} video canplay`); }; const handleLoadedMetadata = () => { console.log(`[UserCamera] ${name} video loadedmetadata`); }; const handleCanPlayThrough = () => { console.log(`[UserCamera] ${name} video canplaythrough`); }; const handlePlay = () => { console.log(`[UserCamera] ${name} video play event`); }; const handlePlaying = () => { console.log(`[UserCamera] ${name} video playing event`); }; const handleWaiting = () => { console.log( `[UserCamera] ${name} video waiting event, paused: ${videoElement.paused}, readyState: ${videoElement.readyState}` ); }; const handleStalled = () => { console.log( `[UserCamera] ${name} video stalled event, paused: ${videoElement.paused}, readyState: ${videoElement.readyState}` ); }; const handlePause = () => { console.log(`[UserCamera] ${name} video pause event`); }; const handleError = (e: Event) => { console.error(`[UserCamera] ${name} video error:`, e); }; videoElement.addEventListener("loadstart", handleLoadStart); videoElement.addEventListener("loadeddata", handleLoadedData); videoElement.addEventListener("loadedmetadata", handleLoadedMetadata); videoElement.addEventListener("canplay", handleCanPlay); videoElement.addEventListener("canplaythrough", handleCanPlayThrough); videoElement.addEventListener("play", handlePlay); videoElement.addEventListener("playing", handlePlaying); videoElement.addEventListener("waiting", handleWaiting); videoElement.addEventListener("stalled", handleStalled); videoElement.addEventListener("pause", handlePause); videoElement.addEventListener("error", handleError); return () => { videoElement.removeEventListener("loadstart", handleLoadStart); videoElement.removeEventListener("loadeddata", handleLoadedData); videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata); videoElement.removeEventListener("canplay", handleCanPlay); videoElement.removeEventListener("canplaythrough", handleCanPlayThrough); videoElement.removeEventListener("play", handlePlay); videoElement.removeEventListener("playing", handlePlaying); videoElement.removeEventListener("waiting", handleWaiting); videoElement.removeEventListener("stalled", handleStalled); videoElement.removeEventListener("pause", handlePause); videoElement.removeEventListener("error", handleError); }; }, [name]); const toggleRemoteAudio = () => { if (!isLocal) { const newMutedState = !isAudioMuted; setIsAudioMuted(newMutedState); console.log( `[UserCamera] ${name} audio ${newMutedState ? "muted" : "unmuted"}` ); } }; const handleVideoClick = () => { if (!isLocal && ref.current) { console.log( `[UserCamera] User clicked on ${name} video, paused: ${ref.current.paused}, readyState: ${ref.current.readyState}, muted: ${ref.current.muted}` ); if (ref.current.paused) { ref.current.play().catch((error) => { console.error(`[UserCamera] Click play failed for ${name}:`, error); }); } else { console.log(`[UserCamera] Video ${name} is already playing`); } } }; return (
= 1536 ? "0.139vw" : "2px" // } rgba(34, 197, 94, ${ringOpacity})` // : `0 4px 40px 0 rgba(15,16,17,0.1), 0 2px 2px 0 rgba(0,0,0,0.06), 0 0 0 ${ // window.innerWidth >= 1536 ? "0.069vw" : "1px" // } rgba(255, 255, 255, 0.3)`, transition: mode === "mini" ? "box-shadow 0.1s ease-out, width 0.3s, background-color 0.3s" : undefined, }} onClick={handleVideoClick} >