This episode covers WebSocket's role in WebRTC: signaling for SDP and ICE candidate exchange, the offer and answer flow, room management, and STUN and TURN server configuration.

WebRTC lets two browsers share video and audio directly, without a server relaying the data. But there is one part that actually needs WebSocket: signaling. Before two peers can connect, they must get to know each other — and that introduction exchange is signaling.
Episode 25 covers WebSocket's role as the signaling channel for WebRTC: SDP offer and answer exchange, the ICE candidate flow, room management, and STUN and TURN configuration to get through NAT.
WebRTC flows media directly between browsers, but the initial exchange cannot happen out of the blue — peers do not know each other's addresses.
peer A -> signaling (WebSocket) -> peer B
media -> langsung antara peer A dan BWebSocket only carries control messages: SDP (the media session description) and ICE candidates (candidate network addresses). Once the two peers agree, media flows directly without the server.
WebSocket's job is to shuttle SDP and ICE candidates back and forth until both sides connect.
A signaling server is very simple: receive a message from one peer, forward it to the target peer.
const { Server } = require("socket.io");
const io = new Server(server);
io.on("connection", (socket) => {
socket.on("signal", (data) => {
io.to(data.tujuan).emit("signal", {
dari: socket.id,
tipe: data.tipe,
payload: data.payload,
});
});
});io.to(data.tujuan).emit("signal", ...) forwards the message to the target peer. The signaling server does not need to understand the SDP or ICE content — it is just a reliable courier.
The complete connection establishment flow:
const pc = new RTCPeerConnection(config);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit("signal", {
tujuan: peerId,
tipe: "offer",
payload: offer,
});pc.createOffer() creates an SDP offer, which is then sent over WebSocket. The receiving peer answers with pc.createAnswer(), and both sides accept each other's remote description.
During connection establishment, both peers discover address candidates gradually.
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit("signal", {
tujuan: peerId,
tipe: "candidate",
payload: event.candidate,
});
}
};
socket.on("signal", async (data) => {
if (data.tipe === "candidate") {
await pc.addIceCandidate(data.payload);
}
});pc.onicecandidate triggers the sending of each new candidate, and pc.addIceCandidate(data.payload) adds it to the connection. ICE trickling lets the connection start forming before all candidates are found.
A WebSocket room (episode 9) acts as a meeting room: everyone who joins the same room can signal each other.
socket.on("room:join", (room) => {
socket.join(room);
const hadir = io.sockets.adapter.rooms.get(room) || new Set();
const lainnya = [...hadir].filter((id) => id !== socket.id);
socket.emit("room:peer", lainnya);
socket.to(room).emit("room:new-peer", socket.id);
});hadir gives the list of existing peers, and socket.to(room).emit("room:new-peer", ...) tells the old ones a newcomer arrived. Each newcomer creates a WebRTC connection to all old peers — the mesh pattern.
In a 3-4 person conference, mesh (everyone connected to everyone) is still fine. Above that, video and audio multiply and overwhelm bandwidth — that is when an SFU (Selective Forwarding Unit) like MediaSoup is needed, which is beyond this episode's scope.
Browsers need STUN and TURN servers to get through NAT.
const config = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{
urls: "turn:turn.example.com:3478",
username: "user",
credential: "secret",
},
],
};
const pc = new RTCPeerConnection(config);stun:stun.l.google.com:19302 is a free public STUN. turn:turn.example.com:3478 provides a relay for connections that fail directly. TURN requires credentials — the signaling server shares them securely.
Symmetric NATs and strict firewalls often block direct connections. ICE then chooses the TURN route: media is relayed through the server, at the cost of extra latency and bandwidth. For production, run your own TURN because public services are not meant for application scale.
Episode 25 showed the collaboration of WebSocket and WebRTC: WebSocket as the courier delivering SDP and ICE, WebRTC as the direct peer-to-peer media carrier. One arranges, the other flows.
Key takeaways:
In the next episode we cover testing WebSocket applications: unit and integration testing, load testing with Artillery and k6, and chaos testing to simulate failures.