import { io, Socket } from "socket.io-client";
class WebRTCSignaling {
private socket: Socket;
private peerConnections: Map<string, RTCPeerConnection> = new Map();
constructor(wsUrl: string, displayName: string) {
this.socket = io(wsUrl, {
auth: { displayName },
});
this.setupSignalingHandlers();
}
private setupSignalingHandlers() {
// Room events
this.socket.on("room-joined", (data) => {
console.log("Joined room:", data.roomId);
// Create offers to existing participants
data.participants.forEach((p) => this.createOffer(p.socketId));
});
this.socket.on("user-joined", (data) => {
console.log("User joined:", data.participant.displayName);
// Wait for offer from new user
});
this.socket.on("user-left", (data) => {
this.closePeerConnection(data.socketId);
});
// WebRTC signaling
this.socket.on("offer", async (data) => {
const pc = this.getOrCreatePeerConnection(data.senderId);
await pc.setRemoteDescription(data.sdp);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
this.socket.emit("answer", {
targetId: data.senderId,
sdp: answer,
});
});
this.socket.on("answer", async (data) => {
const pc = this.peerConnections.get(data.senderId);
if (pc) {
await pc.setRemoteDescription(data.sdp);
}
});
this.socket.on("ice-candidate", async (data) => {
const pc = this.peerConnections.get(data.senderId);
if (pc) {
await pc.addIceCandidate(data.candidate);
}
});
// Media events
this.socket.on("user-toggle-audio", (data) => {
console.log(`User ${data.socketId} ${data.enabled ? 'unmuted' : 'muted'}`);
});
this.socket.on("user-toggle-video", (data) => {
console.log(`User ${data.socketId} video ${data.enabled ? 'on' : 'off'}`);
});
}
private getOrCreatePeerConnection(peerId: string): RTCPeerConnection {
if (this.peerConnections.has(peerId)) {
return this.peerConnections.get(peerId)!;
}
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
],
});
// Send ICE candidates
pc.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit("ice-candidate", {
targetId: peerId,
candidate: event.candidate.toJSON(),
});
}
};
// Handle remote stream
pc.ontrack = (event) => {
console.log("Received remote track from:", peerId);
// Display remote stream
};
this.peerConnections.set(peerId, pc);
return pc;
}
private async createOffer(peerId: string) {
const pc = this.getOrCreatePeerConnection(peerId);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
this.socket.emit("offer", {
targetId: peerId,
sdp: offer,
});
}
private closePeerConnection(peerId: string) {
const pc = this.peerConnections.get(peerId);
if (pc) {
pc.close();
this.peerConnections.delete(peerId);
}
}
joinRoom(roomCode: string) {
return new Promise((resolve, reject) => {
this.socket.emit("join-room", {
roomCode,
displayName: "John Doe",
});
this.socket.once("room-joined", resolve);
this.socket.once("join-error", (err) => reject(new Error(err.message)));
setTimeout(() => reject(new Error("Join timeout")), 15000);
});
}
toggleAudio(enabled: boolean) {
const roomId = "current-room-id"; // Get from state
this.socket.emit("toggle-audio", { roomId, enabled });
}
toggleVideo(enabled: boolean) {
const roomId = "current-room-id"; // Get from state
this.socket.emit("toggle-video", { roomId, enabled });
}
}