Skip to content

Build1 publisher3 min readPublished

A Pion v4 SFU needs the SSRC rewritten before RTCP goes back upstream

Pion v4 moved RTCP writing onto the PeerConnection, so a Go SFU now has to rewrite the MediaSSRC on every PLI, FIR and NACK it forwards. Skip that step and RTCP keeps flowing while the keyframe never arrives.

The Engineer · Build desk

Illustration accompanying A Pion v4 SFU needs the SSRC rewritten before RTCP goes back upstream

What happened

  • A Go developer published the nine failures that cost time while building a low-latency streaming server: one binary doing WebSocket signaling and SFU forwarding, with a browser publisher, viewer and diagnostics panel.
  • The working fix keeps the publisher's PeerConnection beside the track and rewrites MediaSSRC on incoming PLI, FIR and NACK packets to the publisher's SSRC, because viewer feedback names the viewer's SSRC.
  • A viewer-side PeerConnection built from the default MediaEngine can renumber VP8 from payload type 96 to 100, so forwarded RTP is garbage for the viewer even though the SDP negotiated cleanly.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Signing off a v3-to-v4 upgrade takes more than a clean build. The only signal for the SSRC step is a viewer who joins mid-stream and gets a picture, so the upgrade needs a keyframe test in the acceptance path.
  • exposure Monitoring built on getStats and inbound-rtp counters will report healthy sessions during both the black-frame and the missing-keyframe failures, so the first report comes from a viewer.
  • cost Pinning each viewer connection to the publisher's codec parameters means a publisher adding screen share after camera forces a PeerConnection rebuild, and every viewer on that connection has to reconnect.
  • constraint Publisher state has to be keyed on something other than the signaling socket. A websocket that outlives its PeerConnection leaves the server forwarding a stale track list, which is what puts two video tracks in front of a viewer.

An RTCP feedback packet is addressed by SSRC. When a viewer's browser loses a frame it sends a Picture Loss Indication naming the SSRC it is receiving, which belongs to the track the SFU created for that viewer, not to the publisher [5]. Forward the packet as it arrived and, according to the post, RTCP flows, nothing errors, and the publisher never sends a keyframe [6]. The switch statement in the post sets MediaSSRC on PictureLossIndication, FullIntraRequest and TransportLayerNack to the publisher track's SSRC before the call to pubPC.WriteRTCP [5].

The API change shows up as a build error. remote.WriteRTCP stops compiling, and the error names TrackRemote directly [2], because v4 moved RTCP writing onto the PeerConnection and left tracks able only to read [3]. The repair is to keep the publisher's *webrtc.PeerConnection beside the track and call pubPC.WriteRTCP(pkts) [4]. The compiler does not check the SSRC field, because MediaSSRC is a uint32 and type-checks whatever you assign to it [14]. The test that catches the omission is a viewer joining mid-stream and getting a picture [15].

The payload-type item is the same mistake one layer down. A payload type is a number that means whatever the two ends of one connection agreed it means. The server's default MediaEngine keeps its own table, so a publisher sending VP8 on 96 can be re-offered to the viewer as 100, and RTP forwarded byte-for-byte is garbage at the far end [7]. The post's fix is to build the viewer's PeerConnection from a MediaEngine that registers the publisher's own codec parameters, then record which codecs that connection knows and rebuild it when the publisher adds another [8]. I would take the rebuild over in-place renegotiation in any SFU I owned, and the bug it avoids only appears when someone screen-shares after publishing camera [8].

All six failures set out in the text lie outside decoding and transcoding [16]. Two are ordering and lifecycle problems in signaling. Trickled ICE candidates arrive before SetRemoteDescription has been applied, and the post fixes that with about fifteen lines of per-client buffering [9]. The other is a publisher whose websocket outlives its PeerConnection, so the server keeps the old track list and viewers get two video tracks [12]. One is HTTP caching, where http.FileServer sends Last-Modified and no Cache-Control and the browser serves the old script from memory [10]. One is a DOM assignment: e.streams[0] on an empty array puts undefined into srcObject, nothing throws, and getStats keeps reporting inbound bytes and a resolution [11]. The post's own summary is that most of these "look like a media problem and are not one" [13].

The caching entry is the one I would not have filed as a bug at all. The post is right about why it belongs: you verify a change, see the old behaviour, and go looking for a bug that is not there [10].

The post's opening also names a reverse proxy reaping the signaling socket and a publisher switching browser tabs [15]. The text supplied breaks off inside the republish item, so those cases are described only as symptoms.

What to watch

  • Whether Pion's v3-to-v4 migration notes document the SSRC rewrite alongside the WriteRTCP move, since only the second one produces a compile error.
  • The rest of the post: the reverse-proxy and tab-switch cases named in its opening are not in the text supplied.
  • Whether the republish fix is tearing down the stale PeerConnection on a surviving websocket, or keying publisher state on the connection instead of the socket.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories