WhisperNet / hooks /useWebRTC.ts
bonesmasher's picture
Upload 56 files
abc1805 verified
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<MediaStream | null>(null);
const [remoteStreams, setRemoteStreams] = useState<Map<string, MediaStream>>(new Map());
const peersRef = useRef<Map<string, RTCPeerConnection>>(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 };
}