Files
stream.graff.tech-new/client/src/hooks/useWebRTC.ts
T

259 lines
9.3 KiB
TypeScript

import { useEffect, useState, useRef } from "react";
import {
createWebRTCService,
type Participant,
type ChatMessage,
} from "../lib/webrtc";
let webrtcServiceInstance: ReturnType<typeof createWebRTCService> | null = null;
let isInitializing = false;
export const useWebRTC = (roomId?: string, autoJoin = false) => {
const callbacksRegisteredRef = useRef(false);
const hasJoinedRoomRef = useRef(false);
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [participants, setParticipants] = useState<Participant[]>([]);
const [isAudioMuted, setIsAudioMuted] = useState(false);
const [isVideoMuted, setIsVideoMuted] = useState(false);
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const [isConnected, setIsConnected] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
// Создаем сервис только один раз (синглтон)
if (!webrtcServiceInstance) {
webrtcServiceInstance = createWebRTCService({});
}
// Инициализируем состояние из существующего сервиса
const existingStream = webrtcServiceInstance.getLocalStream();
if (existingStream) {
console.log("[useWebRTC] Initializing with existing local stream");
setLocalStream(existingStream);
setIsInitialized(true);
}
const existingParticipants = webrtcServiceInstance.getParticipants();
console.log("[useWebRTC] Component mounted, existing participants:", existingParticipants.length);
if (existingParticipants.length > 0) {
console.log("[useWebRTC] Initializing with participants:", existingParticipants.map(p => p.id));
setParticipants(existingParticipants);
}
const existingMessages = webrtcServiceInstance.getChatMessages();
if (existingMessages.length > 0) {
console.log("[useWebRTC] Initializing with existing messages:", existingMessages.length);
setChatMessages(existingMessages);
}
// Добавляем коллбэки только один раз для этого компонента
if (callbacksRegisteredRef.current) {
return;
}
callbacksRegisteredRef.current = true;
const removeCallbacks = webrtcServiceInstance.addCallbacks({
onLocalStreamReady: (stream) => {
console.log("[useWebRTC] Local stream ready");
setLocalStream(stream);
setIsInitialized(true);
},
onRemoteStreamReady: (participantId, stream) => {
console.log("[useWebRTC] onRemoteStreamReady called for:", participantId);
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
);
} else {
console.log("[useWebRTC] Adding new participant with stream:", participantId);
return [...prev, { id: participantId, stream }];
}
});
},
onRoomParticipants: () => {
setIsConnected(true);
},
onParticipantJoined: (participant) => {
console.log("[useWebRTC] onParticipantJoined called for:", participant.id);
setParticipants((prev) => {
if (prev.find((p) => p.id === participant.id)) {
console.log("[useWebRTC] Participant already in list, skipping");
return prev;
}
console.log("[useWebRTC] Adding participant to state");
return [...prev, participant];
});
},
onParticipantLeft: (participantId) => {
setParticipants((prev) => prev.filter((p) => p.id !== participantId));
},
onParticipantAudioToggle: (participantId, isEnabled) => {
console.log(`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`);
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) => {
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) => {
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);
setChatMessages((prev) => [...prev, message]);
},
onError: (error) => {
console.error("[useWebRTC] Error:", error);
},
});
const initWebRTC = async () => {
if (!webrtcServiceInstance || isInitializing) {
return;
}
// Проверяем, есть ли уже localStream
if (webrtcServiceInstance.getLocalStream()) {
setIsInitialized(true);
return;
}
try {
isInitializing = true;
const stream = await webrtcServiceInstance.initializeLocalStream();
// Даже если stream === null (пользователь отказался от разрешений),
// считаем инициализацию завершенной
if (stream === null) {
console.log("[useWebRTC] Initialized without local stream (user denied permissions)");
setIsInitialized(true);
}
} catch (error) {
console.error("[useWebRTC] Initialization error:", error);
// Даже при ошибке разрешаем продолжить
setIsInitialized(true);
} finally {
isInitializing = false;
}
};
initWebRTC();
// Cleanup при размонтировании компонента
return () => {
callbacksRegisteredRef.current = false;
removeCallbacks();
};
}, []); // Пустой массив зависимостей - эффект срабатывает только при монтировании
// Отдельный эффект для присоединения к комнате
// ВАЖНО: Присоединяемся только ПОСЛЕ инициализации localStream!
useEffect(() => {
if (
!webrtcServiceInstance ||
!autoJoin ||
!roomId ||
hasJoinedRoomRef.current ||
!isInitialized
) {
return;
}
const joinRoomAsync = async () => {
await webrtcServiceInstance!.joinRoom(roomId);
hasJoinedRoomRef.current = true;
};
joinRoomAsync();
}, [roomId, autoJoin, isInitialized]);
const toggleAudio = () => {
if (!webrtcServiceInstance) return;
const newState = webrtcServiceInstance.toggleAudio();
setIsAudioMuted(!newState);
};
const toggleVideo = () => {
if (!webrtcServiceInstance) return;
const newState = webrtcServiceInstance.toggleVideo();
setIsVideoMuted(!newState);
};
const sendMessage = (content: string) => {
if (!webrtcServiceInstance) return;
webrtcServiceInstance.sendChatMessage(content);
};
const joinRoom = async (roomId: string) => {
if (!webrtcServiceInstance) return;
await webrtcServiceInstance.joinRoom(roomId);
setIsConnected(true);
};
const leaveRoom = () => {
if (!webrtcServiceInstance) return;
webrtcServiceInstance.leaveRoom();
setIsConnected(false);
setParticipants([]);
};
const updateSpeakingState = (isSpeaking: boolean) => {
if (!webrtcServiceInstance) return;
webrtcServiceInstance.updateSpeakingState(isSpeaking);
};
return {
localStream,
participants,
isAudioMuted,
isVideoMuted,
chatMessages,
isConnected,
isInitialized,
currentUserId: webrtcServiceInstance?.getCurrentUserId() || "",
toggleAudio,
toggleVideo,
sendMessage,
updateSpeakingState,
joinRoom,
leaveRoom,
};
};