© Ujjawal Solanki | All Rights Reserved
My first WebRTC demo failed in the most educational way possible: live, in front of other people, while I tried to look calm next to a black remote video box.
The local camera preview worked, which made the failure worse. It gave me enough hope to act confident. The remote stream never arrived.
That awkward silence taught me something important. WebRTC is not hard because the API is evil. It is hard because several moving parts must all line up at the same time.
You hear new words immediately: signaling, offer, answer, ICE, STUN, TURN, tracks, streams. For beginners, the vocabulary itself can create panic before any code even runs.
My mistake was trying to understand every detail before building anything. The better path was learning the flow one stage at a time.
Most WebRTC pain is not random magic. It is usually a missing step in signaling, permissions, or network traversal.
If camera and microphone access are unstable, nothing else matters yet. I now treat local media as its own milestone.
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
localVideoRef.current.srcObject = localStream;WebRTC does not replace signaling. You still need a server path to carry offers, answers, and ICE candidates between peers. Socket.io is a friendly place to start.
socket.on('call:offer', async ({ offer, from }) => {
await peer.setRemoteDescription(offer);
const answer = await peer.createAnswer();
await peer.setLocalDescription(answer);
socket.emit('call:answer', { answer, to: from });
});I lost time here because I created an offer too early. If the peer connection does not already know about local tracks, the negotiation can look oddly incomplete.
localStream.getTracks().forEach((track) => {
peer.addTrack(track, localStream);
});
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);Receiving media is not just a networking event. The UI must know where to render the stream.
peer.ontrack = (event) => {
const [remoteStream] = event.streams;
remoteVideoRef.current.srcObject = remoteStream;
};This is the stage many demos avoid because it works on friendly networks and local testing. Real users eventually join from restrictive networks and the call mysteriously fails.
const peer = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.yourserver.com:3478',
username: 'user',
credential: 'pass'
}
]
});If the browser blocks mic or camera access, everything downstream becomes confusing. Check permissions first.
This is one of the most common causes of false hope. Signaling looks alive, but the peers still cannot find a usable route.
Many media APIs behave best or only on secure origins. Localhost can hide this for a while.
I once ended a call in the UI but left media tracks running. The app felt haunted because the camera light kept glowing.
When debugging WebRTC, log each stage separately: media access, offer creation, answer receipt, ICE candidate flow, remote track event.
WebRTC taught me patience and sequencing. It punished rushing in a very honest way. Each missing step surfaced as a real symptom.
Once I respected the order of operations, the system felt less magical and more mechanical. That is when I started trusting myself with it.
If this helped you, save it for later. And if you are stuck on a MERN feature, you can always reach out to me.
© Ujjawal Solanki | All Rights Reserved