import { useState, useRef, useCallback, useEffect } from "react"; import { ICreateConversationResponse } from "../interfaces/conversation"; import { createConnection, createConversation } from "../services/api/chatService"; import { conversationWebSocket } from "../services/websockets/conversation"; export const useWebRTC = () => { const [isConnected, setIsConnected] = useState(false); const [transcript, setTranscript] = useState(""); const [remoteStream, setRemoteStream] = useState(null); const [callStatus, setCallStatus] = useState(""); const peerConnectionRef = useRef(null); const dataChannelRef = useRef(null); const socketRef = useRef(null); const [error, setError] = useState(null); const mediaStreamRef = useRef(null); const isConnectedRef = useRef(isConnected); useEffect(() => { isConnectedRef.current = isConnected; }, [isConnected]); const endCall = useCallback(() => { if (peerConnectionRef.current) { peerConnectionRef.current.close(); peerConnectionRef.current = null; } if (socketRef.current) { socketRef.current.close(); socketRef.current = null; } if (mediaStreamRef.current) { mediaStreamRef.current.getTracks().forEach((track) => track.stop()); mediaStreamRef.current = null; } if (dataChannelRef.current) { dataChannelRef.current.close(); dataChannelRef.current = null; } setIsConnected(false); setCallStatus("Disconnected"); setRemoteStream(null); setTranscript(""); }, []); const startCall = useCallback(async () => { try { const peerConnection = new RTCPeerConnection(); peerConnectionRef.current = peerConnection; peerConnection.onconnectionstatechange = () => { switch (peerConnection.connectionState) { case "connected": setIsConnected(true); setCallStatus("Connected"); break; case "disconnected": case "failed": case "closed": endCall(); break; default: break; } }; peerConnection.ontrack = (event) => { setRemoteStream(event.streams[0]); }; const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaStreamRef.current = mediaStream; peerConnection.addTrack(mediaStream.getTracks()[0], mediaStream); const dataChannel = peerConnection.createDataChannel("response"); dataChannelRef.current = dataChannel; dataChannel.onmessage = (event) => { if (socketRef.current?.readyState === WebSocket.OPEN) { socketRef.current.send(event.data); } }; const sessionResponse = await createConversation({ modality: "voice" }); const sessionData: ICreateConversationResponse = await sessionResponse.data; const conversationId = sessionData.conversation_id; const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); const webrtcResponse = await createConnection(conversationId, { conversation_id: conversationId, offer: { sdp: offer.sdp, type: offer.type }, }); const responseData = await webrtcResponse.data; const ephemeralKey = responseData.ephemeral_key; const socket = conversationWebSocket({ conversationId, modality: "voice", }); socketRef.current = socket; socket.onopen = () => { socket.send( JSON.stringify({ type: "headers", headers: { Authorization: `Bearer ${ephemeralKey}` }, }) ); }; socket.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.transcript) { setTranscript((prev) => `${prev}\n${data.transcript}`); } else if (dataChannelRef.current?.readyState === "open") { dataChannelRef.current.send(JSON.stringify(data)); } } catch { setError("Error handling message"); } }; socket.onclose = (_event) => { if (isConnectedRef.current) endCall(); }; await peerConnection.setRemoteDescription(new RTCSessionDescription(responseData.answer)); } catch { setError("Call setup failed"); endCall(); } }, [endCall]); return { startCall, endCall, isConnected, transcript, remoteStream, callStatus, error, }; };