| 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<MediaStream | null>(null); | |
| const [callStatus, setCallStatus] = useState(""); | |
| const peerConnectionRef = useRef<RTCPeerConnection | null>(null); | |
| const dataChannelRef = useRef<RTCDataChannel | null>(null); | |
| const socketRef = useRef<WebSocket | null>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const mediaStreamRef = useRef<MediaStream | null>(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, | |
| }; | |
| }; | |