259 lines
9.3 KiB
TypeScript
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,
|
|
};
|
|
};
|