import { useState, useEffect, useRef, useCallback } from "react"; import { socket } from "@/lib/socket"; const STUN_SERVERS = { iceServers: [ { urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }, ], }; export function useWebRTC(roomId: string) { const [localStream, setLocalStream] = useState(null); const [remoteStreams, setRemoteStreams] = useState>(new Map()); const peersRef = useRef>(new Map()); const createPeer = useCallback((targetSocketId: string, initiator: boolean, stream: MediaStream) => { const peer = new RTCPeerConnection(STUN_SERVERS); peersRef.current.set(targetSocketId, peer); // Add local tracks stream.getTracks().forEach(track => peer.addTrack(track, stream)); // Handle ICE candidates peer.onicecandidate = (event) => { if (event.candidate) { socket.emit("signal", { target: targetSocketId, signal: { type: "ice-candidate", candidate: event.candidate } }); } }; // Handle remote stream peer.ontrack = (event) => { console.log("Received remote track from:", targetSocketId); setRemoteStreams(prev => { const newMap = new Map(prev); newMap.set(targetSocketId, event.streams[0]); return newMap; }); }; // Create Offer if initiator if (initiator) { peer.createOffer() .then(offer => peer.setLocalDescription(offer)) .then(() => { socket.emit("signal", { target: targetSocketId, signal: { type: "offer", sdp: peer.localDescription } }); }) .catch(err => console.error("Error creating offer:", err)); } return peer; }, []); const joinCall = useCallback(async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); setLocalStream(stream); socket.emit("join-call", { roomId }); return stream; } catch (err) { console.error("Error accessing media devices:", err); alert("Could not access camera/microphone"); return null; } }, [roomId]); const leaveCall = useCallback(() => { // Stop local stream if (localStream) { localStream.getTracks().forEach(track => track.stop()); setLocalStream(null); } // Close all peers peersRef.current.forEach(peer => peer.close()); peersRef.current.clear(); setRemoteStreams(new Map()); socket.emit("leave-call", { roomId }); }, [localStream, roomId]); useEffect(() => { if (!localStream) return; const handleUserConnected = ({ socketId }: { socketId: string }) => { console.log("User connected to call:", socketId); // Initiate connection to new user createPeer(socketId, true, localStream); }; const handleUserDisconnected = ({ socketId }: { socketId: string }) => { console.log("User disconnected from call:", socketId); if (peersRef.current.has(socketId)) { peersRef.current.get(socketId)!.close(); peersRef.current.delete(socketId); } setRemoteStreams(prev => { const newMap = new Map(prev); newMap.delete(socketId); return newMap; }); }; const handleSignal = async (data: { sender: string; signal: any }) => { const { sender, signal } = data; // Ignore key exchange signals (offer-key, answer-key) if (signal.type === "offer-key" || signal.type === "answer-key") return; let peer = peersRef.current.get(sender); if (!peer) { // If receiving offer, create peer (not initiator) if (signal.type === "offer") { peer = createPeer(sender, false, localStream); } else { console.warn("Received signal for unknown peer:", sender); return; } } try { if (signal.type === "offer") { await peer.setRemoteDescription(new RTCSessionDescription(signal.sdp)); const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); socket.emit("signal", { target: sender, signal: { type: "answer", sdp: peer.localDescription } }); } else if (signal.type === "answer") { await peer.setRemoteDescription(new RTCSessionDescription(signal.sdp)); } else if (signal.type === "ice-candidate") { await peer.addIceCandidate(new RTCIceCandidate(signal.candidate)); } } catch (err) { console.error("Error handling signal:", err); } }; socket.on("user-connected-to-call", handleUserConnected); socket.on("user-disconnected-from-call", handleUserDisconnected); socket.on("signal", handleSignal); return () => { socket.off("user-connected-to-call", handleUserConnected); socket.off("user-disconnected-from-call", handleUserDisconnected); socket.off("signal", handleSignal); }; }, [localStream, createPeer]); return { localStream, remoteStreams, joinCall, leaveCall }; }