§ 1.5 Module 1 — When Voice Became Data
Feedback is the property of being able to adjust future conduct by past performance.— Norbert Wiener, The Human Use of Human Beings, 1950
By the end of this lesson
Historical note
In March 1992 the IETF meeting in San Diego was audiocast live over the experimental Multicast Backbone — the MBone — using vat, the audio conferencing tool Van Jacobson and Steven McCanne built at Lawrence Berkeley Laboratory. It worked, mostly. What it could not do was tell the speaker what hundreds of scattered listeners were experiencing: a sender multicasting into the void had no idea whether a receiver in Helsinki was hearing speech or confetti. Danny Cohen's NVP had run into the same wall on the ARPANET in 1973–74 — packet voice with no report channel is a broadcast, not a conversation. So when Henning Schulzrinne, Stephen Casner of USC's Information Sciences Institute, Ron Frederick of Xerox PARC (author of the nv video tool), and Jacobson standardized RTP in RFC 1889 (January 1996), they shipped a full companion protocol in the same document: RTCP, the RTP Control Protocol — a low-rate back-channel in which every participant periodically reports what it is sending and, more importantly, what it is actually receiving. The design was revised as RFC 3550 (July 2003, Internet Standard 64) and survives unchanged inside every WebRTC session: NACK, PLI, and transport-wide congestion control are all extensions bolted onto this 1996 feedback loop.
RTCP defines five packet types. Two of them carry the actual feedback: the Sender Report (SR, packet type 200) and the Receiver Report (RR, packet type 201). The other three are bookkeeping: SDES (202) carries source descriptions, BYE (203) announces departure, and APP (204) is an escape hatch for experiments.
A Sender Report is sent by anyone who has recently transmitted media, and it opens with a block of sender information: a 64-bit NTP-format wall-clock timestamp, the RTP timestamp corresponding to that same instant, and running counts of packets and octets sent. That NTP↔RTP pairing looks unremarkable and is in fact the entire basis of lip synchronization — we return to it in §1.5.4.
Both SRs and RRs then carry zero or more report blocks, one per remote source the reporter has heard from. Each block is a 24-byte verdict on one incoming stream (RFC 3550 §6.4.1):
LSR and DLSR let the original sender compute round-trip time without any clock agreement: when a report arrives at time A (in the same middle-32-bits NTP format), the RTT is A − LSR − DLSR — total elapsed time minus the time the reporter sat on the reply. This is how WebRTC's roundTripTime statistic is measured, and the SR/RR pair as a whole is how a sender with a one-way media stream learns anything at all about the far end.
The jitter field answers a question the loss counters cannot: is the network varying the delay it imposes? A receiver's jitter buffer must hold packets long enough to smooth that variance, so a rising jitter estimate is an early warning that either the buffer must grow or quality will drop — often before a single packet is lost.
RFC 3550 §6.4.1 defines it precisely. For two packets i and j, compare the spacing at arrival against the spacing at the source, both measured in RTP timestamp ticks:
D(i,j) = (R_j − R_i) − (S_j − S_i)
// R = arrival time converted to timestamp units
// S = the packet's RTP timestamp
J += (|D(i−1, i)| − J) / 16; // on every packet, in arrival order
Listing 1.5 — The interarrival jitter estimator of RFC 3550 §6.4.1 and Appendix A.8.
Three details carry all the weight. First, units: R is a wall-clock arrival time converted to the media clock — for 48 kHz audio, milliseconds × 48. The estimate lives entirely in timestamp ticks; a value of 720 at 48 kHz means 15 ms. Second, the absolute value: jitter is a magnitude of variation, so early and late deviations both push it up. Third, the gain of 1/16: each new sample moves the estimate one-sixteenth of the way toward the instantaneous deviation, an exponentially weighted average that RFC 3550 chose to give "a good noise reduction ratio while maintaining a reasonable rate of convergence." The estimator runs on every packet in arrival order — including out-of-order ones, whose negative timestamp deltas produce large |D| spikes that then decay by 1/16 per packet.
A common error
“getStats() reports jitter in milliseconds.” It does not — and neither does RTCP. On the wire, the RR jitter field is in RTP timestamp units (ticks of the 48 kHz or 90 kHz media clock). The W3C stats API, populated from these very reports, converts to seconds: jitter: 0.015, not 15. Dashboards that assume milliseconds display every audio stream as a thousand times better — or worse — than it is, and the bug is silent because the numbers still look plausible. Check units against the getStats dictionary definitions, never against intuition.
RFC 3550 §6.4.1 defines cumulative packets lost as a signed 24-bit value precisely so that duplicates can drive it below zero, and browsers faithfully surface negative values in getStats(). Duplicates arrive routinely: retransmissions that race the original, and some middleboxes that duplicate UDP. A loss rate computed as packetsLost / (packetsLost + packetsReceived) then goes negative, and a dashboard that clamps at zero hides the fact that something is duplicating your traffic.
Compute loss from deltas, allow negatives, and alert on them — sustained negative loss means duplication, which wastes bandwidth and confuses the bandwidth estimator. Separately, remember the RTCP fraction lost field is 8-bit fixed point, so its resolution is 1/256, or about 0.4 %.
A report block carries two loss numbers with different clocks. Cumulative packets lost is a lifetime figure: the number of packets expected minus the number actually received, since reception began. Expected is computed from sequence numbers — the extended highest sequence number seen, minus the first sequence number, plus one. Fraction lost is an interval figure: the loss during the period since the previous report, expressed as an 8-bit fixed-point fraction — the lost count times 256, divided by the expected count, truncated. A value of 64 means 64/256 = 25 percent.
The reason for carrying both is diagnostic. A high cumulative count with a fraction of zero describes an outage that is already over; a low cumulative count with a fraction of 64 describes trouble that started seconds ago. Senders that adapt — and every congestion controller descended from these reports, up to and including Google Congestion Control, does — care about the fraction; operators reading logs after the fact care about the cumulative.
Reordering is where naïve implementations lie. A packet that arrives after a higher sequence number has already been seen is late, not lost: it does not raise the extended highest sequence number, but it does increment the received count, so the computed loss (expected − received) drops back down. An implementation that instead marks a gap as permanently lost the moment it appears will report phantom loss on every reordered packet — and the network reorders more than you think. Two consequences of the arithmetic are worth memorizing: duplicated packets inflate the received count, which is why cumulative lost is a signed 24-bit field that can legitimately go negative; and fraction lost is defined to clamp at zero rather than wrap when duplicates outnumber losses in an interval.
Check your intuition before implementing this in the problem set.
Check your understanding
Auto-graded check
3 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m1-l5-check1)
Interactive 3D instrument
The feedback channel — what the sender is allowed to know
A 3D instrument you drive yourself, one variable at a time. It needs JavaScript and WebGL, so it is not shown in this static copy of the page.
Recall from §1.4 that RTP timestamps are media-clock ticks starting from a deliberately random offset — 48,000 per second for Opus audio, 90,000 per second for video, and never comparable across streams. Your microphone track's timestamp 1,843,200 and your camera track's timestamp 2,914,560,187 may describe the same instant. Nothing inside the RTP packets themselves can align them.
The Sender Report is the missing bridge. Its sender-information block is a single calibration point: at NTP wall-clock time X, my RTP clock for this stream read Y. Given one SR per stream, a receiver maps any RTP timestamp t to wall-clock time as ntp_ref + (t − rtp_ref) / clockRate, placing audio and video on one shared timeline. Playout then delays whichever stream is ahead — almost always audio, since video pipelines are slower — until the timelines agree. One question remains: which audio SSRC belongs with which video SSRC? That binding comes from SDES in the next section, via the CNAME both streams share.
Two practical notes. The sender's NTP clock does not need to be correct — synchronization only requires that both of a participant's streams are stamped from the same clock. And until the first SR arrives, a receiver simply cannot lip-sync; this is why fresh WebRTC calls occasionally show a flash of unsynchronized media in their opening second, and why SRs are sent early and often.
remote-inbound-rtp is synthesised from Receiver Reports the far end sends you, so it does not exist on the first getStats() poll and typically appears 1–5 s into the call. On a send-only connection you never get remote-outbound-rtp; on a receive-only connection you never get remote-inbound-rtp and therefore never get an RTT. Code that reads stats.roundTripTime.toFixed(1) on the first poll throws a TypeError, usually inside a setInterval where nobody sees the exception.
Guard for absence, and for a smoother figure use totalRoundTripTime / roundTripTimeMeasurements rather than the instantaneous value. If you need an RTT on a receive-only path, read candidate-pair.currentRoundTripTime, which comes from STUN consent checks and updates about every 5 s.
RTCP packets never travel alone. RFC 3550 §6.1 requires every transmission to be a compound packet: it must begin with an SR (if the participant sent media recently) or an RR, and it must contain an SDES packet carrying a CNAME — the canonical name that identifies a participant stably across its SSRCs. SDES defines other item types (NAME, EMAIL, PHONE, LOC, TOOL, NOTE), relics of the MBone's open-directory culture; WebRTC sends only CNAME, generated as an opaque random string per RFC 7022 precisely so it identifies nothing but the session. CNAME is what lets the receiver in §1.5.4 know that audio SSRC 0x9A3F17C2 and video SSRC 0x517B2E90 are the same person, and therefore must be lip-synced to each other. A participant leaving cleanly appends a BYE packet — RTCPeerConnection.close() does this for you — so the far side can tear down state immediately instead of waiting for a timeout.
How often do reports flow? RFC 3550 §6.2 fixes the control-traffic budget at 5 percent of the session bandwidth, splitting it one quarter to active senders and three quarters to receivers, on the theory that a few senders matter to many receivers. Each participant computes its share, derives a reporting interval, randomizes it between 0.5× and 1.5× to avoid synchronized bursts, and respects a 5-second minimum (halved for a participant's first report). The elegance is in what this does at scale: the rule was written for MBone multicast sessions with thousands of listeners, and the interval stretches automatically as membership grows — a session of ten thousand still spends only 5 percent of its bandwidth on gossip, with each member reporting rarely. "Timer reconsideration," added in RFC 3550, recomputes the interval when membership changes so joins and leaves cannot stampede the channel.
For a two-party WebRTC call the arithmetic is gentler, but the 5-second minimum was still far too slow for one thing the modern web demands: reacting to loss now. The extended profile AVPF (RFC 4585, July 2006 — Ott, Wenger, Sato, Burmeister, Rey) relaxed the timing rules so a receiver may send early feedback — a NACK or a picture-loss indication — almost immediately, within the same average-rate budget. Reduced-size RTCP (RFC 5506, April 2009) then dropped the compound-packet requirement for such feedback, negotiated in SDP as a=rtcp-rsize, so a two-word NACK no longer drags a full SR and SDES along with it. Every WebRTC feedback mechanism you will meet in Module 5 — NACK, PLI, FIR, TWCC — is an RTCP feedback message riding on this pair of amendments. The 1996 design took its own advice: it adjusted future conduct by past performance.
Implement the receiver-side arithmetic of a report block over a canned arrival trace, then read a decoded SR/RR pair the way a troubleshooter would.
Lab 1
Implement the Jitter Estimator
Graded in the browser against 4 assertions; the editor and harness require JavaScript.
The quiz below refers to this decoded report pair, captured mid-call (Opus audio, media clock 48,000 Hz). Alice is sending; Bob is reporting on what he receives.
SR from Alice (sender SSRC 0x9A3F17C2)
NTP timestamp = 3,992,179,200.500 s (since 1900)
RTP timestamp = 1,843,200 (audio, 48 kHz)
packets sent = 2,450 octets sent = 392,000
[Alice's video SR, same NTP instant: RTP = 2,914,560,187 @ 90 kHz]
RR from Bob, report block on SSRC 0x9A3F17C2
fraction lost = 64 / 256
cumulative lost = 118
ext. highest seq = 67,890
jitter = 720 (timestamp units)
LSR = 0x5F2A8000 DLSR = 131,072 (= 2.000 s)
Check your understanding
Auto-graded check
4 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m1-l5-ex2)
What the specifications say, and what the browsers actually do, are two different documents. These are the divergences that cost production teams their weekends.
RFC 3550 §A.8 computes interarrival jitter in RTP timestamp units; getStats() converts to seconds, so a healthy value looks like 0.004. Half the dashboards in the world multiply by 1000 twice or not at all. Worse, the algorithm is an exponentially weighted moving average with a 1/16 gain, so it takes roughly 16 packets to react and it systematically understates bursty delay — the 400 ms Wi-Fi stall that ruined the call barely moves it.
Treat jitter as a coarse trend line only. For actual delay pathology use jitterBufferDelay/jitterBufferEmittedCount (the buffer's own view, in seconds) and, for audio, concealmentEvents, which counts discrete concealment episodes and does spike on a stall.
WebRTC muxes RTCP onto the media port (RFC 8858), and browsers will not negotiate a separate RTCP port. That is a net win — one NAT mapping instead of two — but it means network teams cannot classify or prioritise RTCP separately, and legacy SBCs that offer only non-muxed RTCP simply fail to interoperate with no browser-side flag to relax it. The 5 % RTCP bandwidth cap and 5 s minimum report interval from RFC 3550 §6.2 also do not describe WebRTC, which uses a=rtcp-fb immediate feedback at far higher rates.
Require a=rtcp-mux in your gateway configuration and validate it in integration tests. When someone reports "RTCP is not being sent", check whether they are looking for a second port that no longer exists.
WebRTC: From First Principles · Module 1, Lesson 5