WebRTC doesn't have to be a nightmare. Here's how I actually got it running.
I spent about six months building a peer-to-peer video system for a logistics company, and honestly, the first three weeks were pure frustration. NAT traversal alone could eat your entire week. The good news is that once you understand the basic flow, WebRTC isn't that complicated. It's mostly just signaling, ICE candidates, and SDP exchange. The hard part is making sure all those pieces actually talk to each other reliably. If you're looking for a solid starting point, Getting Started With Webrtc Rob Manson is one of the better practical guides I've found. It walks through the fundamentals without getting lost in browser compatibility debates. The main GitHub repo is at rob-manson/webrtc-getting-started. I'd clone that first before reading anything else.
Getting Started With Webrtc Rob Manson
The basic pipeline works like this. You create a RTCPeerConnection object, attach your media stream from getUserMedia, generate an offer or answer SDP, signal it to the other peer through whatever mechanism you have (WebSocket, Firebase, whatever), and then the other side creates their connection and applies the remote description. Once ICE candidates trickle in on both sides, the direct media path opens up. That's it. The theory is simple. The implementation is where things fall apart. Here's the thing most tutorials don't tell you. The signaling server is totally up to you. WebRTC does not specify how you exchange SDPs. You can use anything. I've seen people use Firebase real-time database, Socket.IO, even plain HTTP POSTs to a Node endpoint. Don't overcomplicate this. A simple WebSocket relay with a room-based message queue will handle dozens of concurrent connections without breaking a sweat. If you're building something larger than a demo, set up a proper STUN/TURN configuration. Google's public STUN servers are fine for testing but they fail behind corporate firewalls. Your users will sit there watching a connection hang for 30 seconds while the ICE agent times out on each candidate. I ran into a specific issue once where half my users in rural Europe were getting audio but no video. The RTCPeerConnection was establishing fine. Data channels worked. Everything pointed to the media path being open. After about four hours of debugging, I realized the problem was STUN. The candidates from those users were all coming through as relay candidates instead of host candidates because their upstream ISP was doing weird CGNAT stuff. The WebRTC spec says you should prefer host candidates over relay candidates for latency reasons, but some browser implementations get this wrong under heavy load. The fix was adding a second STUN server and two TURN servers on a real VPS in Amsterdam. Once I did that, the connection preferences sorted themselves out and video started flowing. That's the kind of thing you learn the hard way.
Setting up the environment
Start with a basic Node project. Install the dependencies. The Rob Manson repo already has a package.json configured correctly, so just run npm install inside it. You'll need express for the signaling endpoint, ws for WebSocket handling, and optionally a TURN server binary if you want to self-host that piece. I'd recommend staying away from coturn for now unless you need it. The public STUN servers from Google work fine for development and most casual use. Set NODE_ENV to development and run the server. Navigate to http://localhost:3000 in two separate browser tabs. You should see two video elements showing your webcam feed. If nothing appears, check the browser console for signaling errors first. Then check that getUserMedia actually succeeded by logging the stream object. Most failed setups are failing at that step, not at the peer connection step. Browsers will block camera access on HTTP sites. You need HTTPS or localhost. That's a hard requirement, not a suggestion. Even for local testing, if you're using ngrok or similar tunneling, make sure it's HTTPS.
Get the Full Details

Common pitfalls that waste days
The biggest mistake I see people make is trying to reuse the same RTCPeerConnection for multiple exchanges. Once a connection is closed or fails, create a new one. Don't try to recycle it. Also, make sure you're adding icecandidate events to both connections immediately after creation. If you miss one, candidates silently drop and the connection never establishes. No error will tell you this happened. It just sits in a waiting state forever. Another issue is SDP size limits. Some naive signaling implementations use simple GET requests to exchange descriptions. SDPs can get large, especially when you have many ICE candidates. Use POST with JSON bodies instead. I learned this when a user on a slow mobile connection sent an SDP with 47 candidates and the GET request got truncated by the proxy server. The connection stalled at checking state. Switched to POST and it worked immediately. Also, track the ICE connection state explicitly. The default readyState behavior in some browsers around version 70 had edge cases where the connection reported connected but media wasn't actually flowing. Add an interval check on iceConnectionState and log it. If it hits disconnected, restart the ICE gatherer. It sounds crude but it resolved the issue for about 5 percent of our users who were on flaky WiFi.
Scaling beyond a demo
WebRTC is peer-to-peer by design, which means it doesn't scale well past two participants without a mesh or SFU architecture. For a small group call, a mesh works. Three people, six connections. Five people, twenty connections. Each participant's upload bandwidth doubles with every person added. Beyond four or five, you need a Selective Forwarding Unit. mediasoup and Janus are the two most common choices. I'd recommend mediasoup if you're comfortable with Node. Janus is more flexible but has a steeper learning curve. The Rob Manson guide covers the peer-to-peer case well, so once you're past that, look at the mediasoup docs for the next step. The GitHub repo for Getting Started With Webrtc Rob Manson is straightforward and the code runs as-is on modern Chrome and Firefox. Safari has some quirks with simulcast support but works for basic calls. Edge is now Chromium-based so it behaves the same. Keep your dependencies updated. WebRTC APIs change occasionally between browser versions and the signal handling code in older examples sometimes breaks silently. Download the repo, run it locally, break it, fix it. That's honestly the fastest way to learn. The documentation is sparse by design because WebRTC itself is still somewhat raw at the edges. But once you understand the flow, everything else builds on top of it naturally.