Urgent.News

What's breaking now, across thousands of outlets.

Tech

What actually breaks when you build a WebRTC SFU in Go

Every Pion example I found stops at "hello world": two peer connections, one track, it works, the blog post ends. That is the easy 20%. The other 80% is what happens after — when the publisher's connection drops, when a viewer joins mid-stream, when a reverse proxy reaps your signaling socket, when the person streaming switches browser tabs. I built a low-latency streaming server: one Go binary…

Building a WebRTC Selector Forwarding Unit (SFU) in Go can be challenging, with numerous pitfalls that can cause issues for both publishers and viewers. These nine problems highlight the complexities involved and the need for careful implementation.

1. When upgrading to Pion v4, the TrackRemote.WriteRTCP function is no longer available. To solve this, keep the publisher's PeerConnection alongside the track and call pubPC.WriteRTCP(pkts), rewriting the target SSRC first for viewer feedback references. Failure to do so can result in RTCP flows without errors, causing publishers to never send keyframes.

2. Another common issue is an empty event.streams array in ontrack. The viewer's peer connection may report inbound-RTP bytes and resolution, with ICE connected and good stats, but the video element remains black. This is due to the browser applying heuristic caching, serving the script from memory instead of revalidating it. To fix this, wrap the static handler and send Cache-Control: no-cache.

3. JavaScript can become stale after editing viewer.js, causing old behavior to persist. A hard reload fixes it temporarily, but it returns later. This is due to the http.FileServer sending Last-Modified but no Cache-Control, causing the browser to serve the script from memory. Fix this by wrapping the static handler and sending Cache-Control: no-cache, ensuring the browser always revalidates.

4. Forwarding RTP without rewriting payload types can lead to viewers seeing black frames or Chrome dropping packets. The server's default MediaEngine may have its own payload-type table, and if the publisher sends VP8 on PT 96 while the viewer side re-offers it as PT 100, the RTP forwarded is garbage for the viewer. Build the viewer-side PeerConnection from a MediaEngine that registers the publisher's codec parameters, including its payload type, to prevent this issue.

5. ICE candidates arriving before the remote description can cause AddICECandidate: InvalidStateError. Candidates may land while the server still has no remote description to attach them to. Buffer candidates per client until SetRemoteDescription has been applied, then flush them to resolve this problem.

6. Republishing can stack state, causing viewers to see two video tracks or the picture to only update every other attempt. When the publisher's connection fails and it republishes, the server keeps its old PeerConnection, track list, and viewer senders. To fix this, treat a second offer as a republish, close the server-side PC, clear the room's tracks, tear down every viewer PC, broadcast publisher{live:false}, and negotiate from scratch.

Viewers must also drop their PeerConnection on live:false to avoid holding a dead one and autoplaying silently.

7. A backgrounded tab can stop the source, resulting in no frames, no bitrate, but only when switching tabs. The self-test page may report running indefinitely, leading to the impression of a media failure. This is caused by requestAnimationFrame being stopped for hidden documents. The source should be driven from a timer (setInterval) instead of rAF, and if the session is stopped, decide to stop the source accordingly.

Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.

Also reported by 1 other outlet

Read the original at dev.to →

More in Tech

More from Saturday 19 September →