§ 2.1 Module 2 — Naming the Session: SDP, SIP, and Offer/Answer
SDP was a flyer stapled to a telephone pole. Thirty years later, the flyer is a load-bearing wall.
By the end of this lesson
Historical note
In March 1992, the 23rd IETF meeting in San Diego was audiocast live to about twenty sites worldwide over an experimental overlay network that Steve Casner and Steve Deering had stitched together from tunnels: the Mbone, the multicast backbone. For the first time, one packet stream could reach the whole planet. What the Mbone could not do was tell anyone the stream existed. Discovery was word-of-mouth — a mailing list, a hallway conversation. Van Jacobson's sd tool, and later Mark Handley's sdr, fixed this by broadcasting little text flyers over the network itself: a handful of single-letter lines — v=, o=, s=, m= — saying here is a session, here is where to tune in. That flyer format became the Session Description Protocol (SDP), standardized as RFC 2327 in April 1998. It was born as an announcement — one writer, many readers, take it or leave it. Which is precisely why pressing it into two-party negotiation, as every WebRTC call still does, has hurt ever since.
Ordinary IP delivers a packet to one address. Steve Deering's doctoral work at Stanford, published as RFC 1112 in August 1989, added a third mode alongside unicast and link-local broadcast: IP multicast, in which a sender transmits once to a group address in the 224.0.0.0/4 range and the network replicates the packet along a distribution tree to every host that has joined the group. The economics were seductive. A concert, a seminar, a space-shuttle downlink could be sent once and heard by ten thousand receivers, with no server farm, no per-viewer cost — the routers themselves would do the fan-out, carrying audio in RTP over UDP.
The catch was deployment: in 1992 almost no production router spoke multicast routing. The workaround, proposed in RFC 1075 (1988, Waitzman, Partridge, and Deering) and implemented in the mrouted daemon, was to tunnel multicast traffic inside unicast packets between islands of multicast-capable workstations. The resulting overlay — the Mbone — went live for that March 1992 San Diego audiocast and grew from around forty subnets to thousands within a few years. A remarkable toolchain grew with it, much of it from Van Jacobson and Steve McCanne's group at Lawrence Berkeley Laboratory: vat for audio, vic for video, wb for a shared whiteboard.
The culture produced spectacles. On June 24, 1993, the Palo Alto garage band Severe Tire Damage — its members drawn from Xerox PARC, DEC, and Apple — became the first band to play live on the Internet. On November 18, 1994, the Rolling Stones multicast roughly twenty minutes of their Cotton Bowl show in Dallas over the Mbone (Severe Tire Damage cheekily played an unauthorized opening set on the same channel). NASA multicast shuttle missions. The IETF multicast itself, twice a year. The broadcast dream appeared to be working.
But every one of these events shared an embarrassing bootstrap problem. A multicast session is identified by a group address and a set of ports, chosen by the sender. A listener who does not know those numbers cannot join. There was no dial tone, no directory, no URL. The whole planet could receive the concert; almost nobody could find it.
The Mbone's answer was characteristically recursive: use multicast to advertise multicast. Van Jacobson's sd (session directory) tool, and its successor sdr, written by Mark Handley at University College London in the mid-1990s, listened on a well-known multicast group for periodic session announcements and displayed them as a scrollable programme guide — a TV listing for the Internet. Click a session and sdr launched vat and vic with the right addresses and ports.
The wire mechanism was the Session Announcement Protocol, SAP. Its design tells you everything about the era. There is no request and no response: an announcer simply multicasts its session description, over and over, to the well-known group, and every directory tool within scope caches whatever it hears. Because all announcers share one fixed bandwidth budget for the announcement channel, the repeat interval stretches as the number of advertised sessions grows — with hundreds of sessions, your flyer might be re-stapled to the pole only every ten or fifteen minutes. Announcements carry no acknowledgement, no versioning handshake, no addressee. A SAP packet is a broadcast flyer in the most literal sense.
And the payload of that flyer — the text that actually describes the session — is SDP.
Interactive 3D instrument
Flyers on a telephone pole — SAP, and the announcement that never asks
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.
SDP was designed to describe a multicast session on a flyer, not to negotiate between two parties; offer/answer was retrofitted five years later in RFC 3264. That history explains the parts that make no sense: identity is positional (the m-line index is the stream's name), direction is expressed as four ad-hoc attributes with an a=inactive escape hatch, and there is no way to say "I do not support this" other than zeroing a port. Nearly every SDP bug you will ever debug is a consequence of positional identity.
Never key application state on m-line index. Use a=mid (exposed as transceiver.mid) as the stable identifier, and carry your own semantic labels out-of-band in your signaling messages rather than trying to encode them in SDP.
Handley and Jacobson wrote SDP down as RFC 2327 in April 1998, after years of deployment in sd and sdr. The format is aggressively simple: a sequence of lines, each of the form <type>=<value>, where the type is exactly one case-significant lowercase letter and no whitespace is permitted around the equals sign. Here is the canonical example, essentially as it appears in RFC 2327 itself:
v=0
o=mhandley 2890844526 2890842807 IN IP4 126.16.64.4
s=SDP Seminar
i=A Seminar on the session description protocol
u=http://www.ucl.ac.uk/staff/M.Handley/sdp.03.ps
e=mjh@isi.edu (Mark Handley)
c=IN IP4 224.2.17.12/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 31
Listing 2.1 — A 1990s Mbone session announcement: Mark Handley's own seminar about SDP, described in SDP.
Read it as the flyer it is. v=0 is the format version — still 0 today, twenty-eight years later. The origin line o= carries six fields: a username, a session ID, a session version (bump it when you re-announce a changed session), a network type, an address type, and the originator's address — together a globally unique name for the announcement. s= is the human-readable title, i= a longer description, u= and e= where to read more and whom to email. c= gives the connection address — here a multicast group with a TTL of 127 — and t= the start and stop times as NTP timestamps, because a flyer for a seminar must say when the seminar is. Then the media lines: m=audio 49170 RTP/AVP 0 means audio on port 49170, RTP under the audio/video profile, payload type 0 (PCM µ-law at 8 kHz — the same G.711 coding the telephone network had used since the 1970s). Every fact is declarative. The announcement says what is, never asks what you can do.
The specification has been revised twice, each time by people who were there at the start. RFC 4566 (July 2006, Handley, Jacobson, and Colin Perkins) tightened the grammar with full ABNF and reflected a decade of VoIP deployment. RFC 8866 (January 2021, Begen, Kyzivat, Perkins, and Handley) is the current definition; it clarified parsing rules, deprecated the k= key line, and — tellingly — changed almost nothing structural. The SDP that a 2026 browser emits from createOffer() would be substantially legible to sdr in 1996. The flyer's grammar fossilized early, and everything since has been built on the fossil.
SDP's most distinctive rule is that line order is fixed. A session description is not a bag of key–value pairs: the session-level lines must appear, when present, in exactly this order —
v= o= s= i= u= e= p= c= b= t= r= z= k= a=
Listing 2.2 — Session-level type letters in their one permitted order (RFC 8866 §5). After the first m= line, only i= c= b= k= a= — and further m= lines — may appear.
— followed by zero or more media descriptions, each opened by an m= line that scopes every line beneath it until the next m=. A c= before the first m= applies to the whole session; a c= after an m= overrides it for that one media stream. Order is not a style preference; o= before v= is not merely unusual, it is not SDP.
RFC 8866 states the reason plainly: the fixed order was chosen to allow simple, strict parsers and to make errors immediately detectable. Remember the deployment target. An sdr instance on a 1995 SPARCstation was parsing a continuous drizzle of announcements from strangers on a broadcast channel, with no way to ask a malformed sender to clarify. A fixed-order, single-letter grammar can be validated in one pass with a pocket-sized state machine: track where you are, reject anything that steps backward. The type alphabet is closed, too — extension happens through named a= attributes (which parsers ignore when unrecognized), never through new type letters, so an unknown letter marks a description the parser cannot safely process at all. Strictness where structure lives, tolerance where vocabulary lives: that split is why thirty years of codecs, transports, and security schemes have been bolted onto SDP without ever changing v=0.
Chrome emits o=- <session-id> <version> IN IP4 127.0.0.1. The address is a placeholder, not the peer; the session id is a random 64-bit-ish number; the version is supposed to increment on modification. Logging the o= address as "the remote IP" is a rite of passage. Meanwhile some SIP stacks fail to increment the version on a re-INVITE, and a strict peer will treat the second offer as a duplicate and ignore it.
Get peer addresses from ICE candidate stats (local-candidate/remote-candidate joined through the selected pair), never from o= or c=. If you build SDP by hand for a gateway, increment the o= version on every modification even though browsers do not check.
Everything above describes a one-to-many, take-it-or-leave-it medium. The announcer declares the codec; you either have a PCM decoder or you do not. There is no channel — and no vocabulary — for the receiver to answer I would prefer Opus, because on the Mbone ten thousand receivers cannot each prefer something different.
Then the VoIP gold rush arrived, and two-party calls needed exactly what announcements never did: agreement. Both sides must converge on codecs from the intersection of their capabilities, exchange each other's transport addresses, and settle directionality — and both must be able to change their minds mid-call. When SIP (RFC 2543, March 1999; rewritten as RFC 3261, June 2002) beat H.323 for the signaling crown — the war of the next lesson — it carried SDP bodies inside its messages, because SDP was the description format the Internet community already had. Negotiation semantics were then retrofitted in RFC 3264 (June 2002, Jonathan Rosenberg and Henning Schulzrinne): the offer/answer model, a set of rules for interpreting one SDP blob as a proposal and a second as a response — the answer mirroring the offer's media lines one for one, pruning what the answerer cannot do.
Note carefully what RFC 3264 is: rules about SDP, not in SDP. The format itself gained no question mark. An offer is structurally indistinguishable from a 1994 concert flyer; only the surrounding protocol state machine makes it an offer. That mismatch — declarative text conscripted as negotiation algebra — is the original sin this module keeps returning to. It is why WebRTC's RTCPeerConnection hands you opaque SDP blobs and a fragile state machine instead of a capabilities API; why the Plan B versus Unified Plan schism (dueling 2013 drafts by Justin Uberti and Adam Roach on how to cram multiple video streams into one description) could break interop between browsers for years; and why ORTC (2013, championed by Microsoft and Hookflash's Robin Raymond) tried to abolish SDP from the API surface entirely before JSEP (RFC 8829, January 2021) cemented it. When you meet those fights later in this module, trace them back here: every one is the flyer being asked to be a contract.
A common error
“SDP is a protocol WebRTC sends over the network.” It is not, despite the P in the name. SDP is inert text — a description with no transport, no message exchange, no state machine of its own. SAP multicast it; SIP carries it in message bodies; WebRTC does not transmit it at all. Your application must ferry offers and answers between peers by whatever means you choose — WebSocket, HTTP, QR code, carrier pigeon. When a WebRTC call fails to set up, the SDP did not “fail to arrive” because of WebRTC; your signaling code dropped the flyer.
Build the strict one-pass validator sdr needed in 1995 — and prove you know which rules are real and which are folklore.
Lab 1
The SDP Grammar Cop
Graded in the browser against 5 assertions; the editor and harness require JavaScript.
Check your understanding
Auto-graded check
4 auto-graded questions with an explanation for every wrong answer. Requires JavaScript. (m2-l1-quiz)
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 4566 §5 specifies \r\n line endings. The idiomatic munge — sdp.split('\n').map(...).join('\n') — leaves the trailing \r on every untouched line and drops it on the joined ones, producing a mixed-ending document. Chrome's parser is forgiving and accepts it, which is exactly the problem: your munge appears to work in development and then a SIP gateway, a Firefox code path, or an SFU's stricter parser rejects it in production.
Split on /\r\n/ and join on '\r\n', and assert that the final string contains no bare LF. Better: do not munge. Most historical reasons to munge (codec order, bitrate caps, simulcast) now have real APIs in setCodecPreferences, setParameters, and addTransceiver.
v=0 has never changed and never will; SDP carries no version negotiation for the format itself. What varies is which attributes a given browser build writes, and the only way to learn that is to generate an offer and read it — which is why so much code calls createOffer() purely to sniff capabilities. That pattern is fragile because generating an offer has side effects on the peer connection's state machine.
Use RTCRtpSender.getCapabilities(kind) and RTCRtpReceiver.getCapabilities(kind) — both static, both side-effect-free, and both correctly report per-device differences that a hard-coded codec list cannot. Note that send and receive capabilities genuinely differ on real hardware.
The Impossible Call · Module 2, Lesson 1