Why This Matters
If you are building anything in WebRTC — a meeting app, a telemedicine console, a live auction with two-way audio, a remote-classroom platform — the offer/answer exchange is the single most common place where the call either connects or fails to connect. When a doctor hears "the patient's video isn't coming through", four times out of five the root cause is somewhere in the SDP exchange: a codec that wasn't offered, a BUNDLE group that broke, an offer issued before the previous one was answered, an ICE candidate that arrived too early, a track added after the answer without a renegotiation. The good news is that the rules are written down with unusual precision in a single primary document, RFC 9429, and they are not long: the entire JSEP specification fits in roughly 130 pages, and the part that matters for a product team fits in this article. Read it once, and the rest of WebRTC becomes much easier to scope, debug, and explain to the engineers building it.
What SDP Actually Is
The Session Description Protocol, abbreviated SDP, is a small text format for describing a media session. It was first defined for telephony in 1998, updated for the modern internet in RFC 4566, and replaced by RFC 8866 in January 2021 (IETF, RFC 8866, SDP: Session Description Protocol, January 2021). An SDP document looks like a flat list of one-letter keys and their values, one per line:
v=0
o=- 4611732742 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 63
c=IN IP4 0.0.0.0
a=rtcp-mux
a=mid:0
a=sendrecv
a=rtpmap:111 opus/48000/2
a=ice-ufrag:F7gI
a=ice-pwd:x9cml/YzichV2+XlhiMu8g
a=fingerprint:sha-256 1A:2B:3C:...:F9
a=setup:actpass
That, in 14 lines, is a one-audio-track WebRTC offer with the most important fields. The reader who has never opened an SDP document often asks why it is so cryptic. The answer is that SDP predates JSON, predates XML by a decade, and was designed when 28.8 kilobit modems were normal — so brevity beat readability, and the format never changed because the install base never changed. WebRTC inherited it because every existing telephony stack already knew how to read it.
Each line is one field. v=0 is the protocol version. The o= line identifies the originator. s= is the session name (- in WebRTC, where the name has no meaning). t=0 0 is the session timing (zeros mean "for as long as both sides are interested"). Each m= line opens a media section — one for audio, one for video, one for the data channel if the connection has one. Inside each media section, every line starting with a= is an attribute that refines what the media will look like: which codecs, which RTP feedback mechanisms, which encryption keys, which ICE candidates, and which direction the media flows.
The point worth keeping is that SDP is descriptive, not active. The document does not move bytes. It tells both sides what bytes will look like once they start moving. The actual transport — UDP packets carrying encrypted RTP — happens entirely outside SDP, on the ports the SDP described. SDP is, in software terms, a configuration file two browsers swap.
The Offer/Answer Model — A 2002 Idea That Still Works
The model itself is older than WebRTC by a decade. RFC 3264, published in June 2002, defined a simple rule: one participant in a session creates an SDP that describes what it wants to send and receive — that's the offer — and sends it to the other participant; the other side replies with an SDP that describes what it agrees to — that's the answer — and the exchange is complete (IETF, RFC 3264, An Offer/Answer Model with the Session Description Protocol, §1, June 2002). Both sides apply both documents to their local state machines, and from then on, the media can flow.
Two rules from RFC 3264 still govern modern WebRTC. First, the answer must contain the same number of media sections as the offer, in the same order (RFC 3264, §6.1). The answerer cannot drop a section, but it can refuse to participate in one by setting its port to zero. Second, only one offer can be outstanding at a time: an agent must not generate a new offer while it has not yet received an answer to a previous one, or while it has not yet replied to an offer it has received (RFC 3264, §4). This second rule is the source of the "glare" condition we discuss further down — when both peers create offers at the same time, the rule is violated by accident.
The original document was written for Session Initiation Protocol, the telephony signalling standard. WebRTC inherited the rules and layered a browser-specific state machine on top.
JSEP — The Browser-Side State Machine
The browser-side machinery sits in a single document: RFC 9429, the JavaScript Session Establishment Protocol, abbreviated JSEP, published in April 2024 (IETF, RFC 9429, JavaScript Session Establishment Protocol, April 2024). It obsoletes the earlier RFC 8829 from January 2021. JSEP describes exactly what createOffer(), createAnswer(), setLocalDescription() and setRemoteDescription() are supposed to do inside the browser, including which states are legal at which time and what counts as an error.
A correct mental model is this: JSEP is a five-state state machine that two endpoints walk through together, with each transition driven by a JavaScript call. The states are listed below, with what each means, copied from the canonical state diagram in RFC 9429, §3.2:
| State | What it means |
|---|---|
stable | No offer/answer exchange is in progress. The connection is either freshly created or has finished a previous round of negotiation. Either side may issue an offer from this state. |
have-local-offer | The local side has called setLocalDescription(offer) but has not yet received the remote answer. |
have-remote-offer | The local side has called setRemoteDescription(offer) but has not yet sent its own answer. |
have-local-pranswer | The local side has sent a "provisional answer" (a partial commitment used in some SIP scenarios; rare in pure WebRTC). |
have-remote-pranswer | The local side has received a provisional answer from the remote. |
stable. The first offer moves the offerer to have-local-offer and the answerer to have-remote-offer. When both sides apply the matching answer, both return to stable. From there, either side can start the next round — and the next round is just the same five-state walk, applied to whatever changed (a new camera turned on, a screen share added, a network restart). The state diagram is closed: every legal call either advances the state or stays in it, and every illegal call (for example, calling createAnswer before receiving an offer) is required to throw an InvalidStateError.
The rollback option in the bottom of the diagram is the lever that makes Perfect Negotiation work — more on that later.
The Life Of One Offer, Step By Step
The cleanest way to understand the whole exchange is to walk one offer through from the moment the application decides to add a track. The numbered list below is what actually happens inside the browser. The application code calls are in monospace; everything else is the browser's job:
- A reason to negotiate arises. The application calls
pc.addTrack(localCameraTrack, localStream)or it changes a transceiver's direction or it adds a data channel. The browser flags the peer connection as "negotiation needed" and fires anegotiationneededevent (W3C WebRTC, §4.4, Recommendation 13 March 2025). - The application calls
await pc.createOffer(). The browser walks its internal list of transceivers, codec preferences, ICE credentials, DTLS fingerprints, BUNDLE groups, RTP header extensions, and msids, and produces an SDP blob describing all of it. At this point nothing has actually started: ICE has not begun gathering candidates, DTLS has not started, no media flows. The browser is offering a plan; no commitment yet (RFC 9429, §4.1.8). - The application calls
await pc.setLocalDescription(offer). The signaling state moves fromstabletohave-local-offer. The browser starts gathering ICE candidates and may immediately begin firingicecandidateevents for the application to relay to the remote peer (RFC 9429, §4.1.11; RFC 8838, §3, Trickle ICE, January 2021). DTLS endpoints are configured; the SRTP keys are not yet derived. - The application ships the offer over its signaling channel — a WebSocket, a long-poll HTTP endpoint, a Matrix room, anything the product team chose. JSEP deliberately does not standardize how the offer travels (RFC 9429, §3.1: "addressing, retransmission, forking, and glare handling … entirely up to the application").
- The remote peer calls
await pc.setRemoteDescription(offer). State moves fromstabletohave-remote-offer. The browser creates RTCRtpTransceiver objects mirroring the offerer's media sections, prepares decoders for the offered codecs, and configures its ICE and DTLS endpoints to match (RFC 9429, §4.1.12). - The remote peer calls
await pc.createAnswer(). The browser builds an SDP whose media-section count and order match the offer exactly (RFC 3264, §6.1), picks one codec per section out of those the offerer listed, sets each section's direction (sendrecv, sendonly, recvonly, inactive), and signs its own DTLS certificate fingerprint into the document (RFC 9429, §4.1.9). - The remote peer calls
await pc.setLocalDescription(answer). State moves tostable. The commitments are now locked in. SRTP keys can be derived as soon as DTLS finishes; media can begin flowing as soon as ICE finds a working candidate pair (RFC 9429, §5.11). - The remote peer sends the answer back over the signaling channel.
- The original peer calls
await pc.setRemoteDescription(answer). State moves tostableon this side too. Resources held during negotiation — extra ICE components, candidate pools — are released (RFC 9429, §5.11). - Trickle ICE has been running in parallel the whole time. Both sides have been gathering and exchanging candidates since step 3 on the offerer's side and step 5 on the answerer's. As soon as one valid candidate pair passes its connectivity check, the ICE state moves to
connectedand media starts moving — often before the answer has even been received (RFC 8838, §3).
The whole exchange typically takes 50 to 300 milliseconds end to end, dominated by signaling-channel latency. ICE gathering and DTLS run in parallel and add 100 to 500 milliseconds on top before media actually starts. On a same-LAN test it can finish in under 200 ms; on a corporate-firewall traversal with TURN allocation it can stretch to 2 seconds.
The Fields A WebRTC Offer Actually Contains
The SDP fields shown earlier deserve a closer look, because most production failures map to one specific line getting it wrong. The list below is the working set every WebRTC engineer must recognize; we group by purpose rather than by RFC.
Session-level lines are the small block at the top of the document:
v=0— protocol version. Always zero (RFC 8866, §5.1).o=— originator and session id (RFC 8866, §5.2).IN IP4 s=-— session name. The dash is WebRTC's convention for "no name needed".t=0 0— session timing. Both zeros mean an unbounded session.a=group:BUNDLE 0 1 2— the BUNDLE group, listing the media-section ids that will share a single network transport (RFC 9143, Negotiating Media Multiplexing Using SDP, §7, February 2022).
Media sections repeat once per audio/video/data stream:
m=audio 9 UDP/TLS/RTP/SAVPF 111 63— opens an audio section over UDP-with-DTLS-SRTP-and-feedback, listing the codec payload-type numbers the offerer accepts (RFC 8866, §5.14; RFC 5764, DTLS Extension to Establish Keys for SRTP, May 2010).c=IN IP4 0.0.0.0— placeholder connection address. With BUNDLE plus ICE, the actual address arrives via candidate lines (RFC 8866, §5.7).a=rtcp-mux— multiplex RTP and RTCP on the same port. Mandatory in modern WebRTC (RFC 5761, Multiplexing RTP Data and Control Packets on a Single Port, April 2010).a=mid:0— the media-id used as the BUNDLE key (RFC 5888; required by RFC 9143).a=sendrecv(orsendonly,recvonly,inactive) — direction of the media flow (RFC 3264, §5.1).a=rtpmap:111 opus/48000/2— codec name, sample rate, and channels for the payload type. Repeated once per codec offered (RFC 8866, §6.6).a=fmtp:111 minptime=10;useinbandfec=1— format-specific parameters for the codec (RFC 8866, §6.15).a=rtcp-fb:111 transport-cc— RTCP feedback messages the codec supports (RFC 4585).a=ice-ufrag:F7gIanda=ice-pwd:x9cml/...— ICE short-term credentials, used to authenticate connectivity-check packets (RFC 8839, SDP Offer/Answer Procedures for ICE, §5.4, January 2021).a=candidate:0 1 udp 2113929471 192.0.2.1 51772 typ host— one ICE candidate per address the peer might be reachable at; many such lines (RFC 8839, §5.1).a=fingerprint:sha-256 1A:2B:...:F9— the SHA-256 fingerprint of the DTLS certificate the peer will present. The signaling channel becomes the trust anchor for the media channel (RFC 8122, March 2017).a=setup:actpass— who initiates the DTLS handshake.actpassin offers,activeorpassivein answers (RFC 4145, §4).a=msid:— binds the section to aMediaStreamandMediaStreamTrackin the JavaScript API (RFC 8830, January 2021).a=simulcast:send 1;2;3— opens simulcast: the sender will publish three encoded layers (RFC 8853, January 2021).a=ssrc:1234567890 cname:abc...— synchronization sources and their canonical names (RFC 5576).
That is the working set. In practice you will see additional a=extmap lines for RTP header extensions (transport-wide congestion control, absolute send time, mid extension), additional a=rtcp-fb entries (nack, nack pli, ccm fir, goog-remb), and a m=application line for the SCTP-over-DTLS data channel when present (RFC 8841). A complete production offer with audio, video, and a data channel runs 70 to 120 lines; an SFU's fan-out offer to a 20-participant meeting can reach 600 lines and 25 KB.
A worked example. Count the lines in a typical SDP offer for a 1-to-1 video call with audio and video and one data channel, using the per-section line counts from the example offers in RFC 9429, §7.3:
| Block | Lines |
|---|---|
| Session-level (v=, o=, s=, t=, BUNDLE group, ice-options:trickle) | 8 |
| Audio m-section (Opus + headers + ICE creds + 2 candidates + fingerprint + setup) | 26 |
| Video m-section (VP8 + VP9 + H264 + AV1 + headers + ICE creds + 2 candidates + fingerprint + setup) | 32 |
| Data-channel m-section (SCTP) | 8 |
| Total | 74 |
Glare — The Failure Mode That Has A Name
Glare is the term RFC 3264 uses for the case where both peers generate an offer at the same time, neither has received the other's offer yet, and the offer-uniqueness rule from §4 is now broken on both sides simultaneously. The word comes from circuit-switched telephony — when two operators seized the same trunk at the same instant, the line "glared" — and the analogy is exact: two endpoints reach for the negotiation channel together and neither knows what to do.
In a hand-rolled WebRTC app, glare is the most common cause of intermittent two-way call failures. It happens whenever both sides legitimately decide to renegotiate within the round-trip-time window: both clients show a "share screen" button, both users press it within 200 ms of each other, both browsers fire negotiationneeded, both applications call createOffer and setLocalDescription, both ship their offer onto the signalling channel, and both arrive on the other side in the have-local-offer state — which is not a legal state to receive an offer into. The default behavior is that one or both ends throw an InvalidStateError and the call breaks.
The fix has a name: Perfect Negotiation. The pattern is documented in the W3C WebRTC Recommendation (§10.7, Perfect negotiation example, 13 March 2025) and explained in plain prose on MDN. The idea is to assign each peer a fixed role at connection setup — one is polite, the other is impolite — and to give the polite peer the lever called rollback that RFC 9429 added to the state machine specifically for this purpose.
The implementation is short enough to print:
let makingOffer = false;
let ignoreOffer = false;
const polite = /* assigned at connection setup, e.g. by who joined first */;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription(); // calls createOffer internally
signaling.send({ description: pc.localDescription });
} finally {
makingOffer = false;
}
};
signaling.onmessage = async ({ description, candidate }) => {
if (description) {
const collision = description.type === "offer"
&& (makingOffer || pc.signalingState !== "stable");
ignoreOffer = !polite && collision;
if (ignoreOffer) return;
await pc.setRemoteDescription(description); // implicit rollback if polite
if (description.type === "offer") {
await pc.setLocalDescription();
signaling.send({ description: pc.localDescription });
}
}
};
Two rules carry all the weight. The impolite peer ignores incoming offers when it has one of its own outstanding — its own offer wins by default. The polite peer rolls back its own offer and applies the incoming one — it yields the round. The polite peer's setRemoteDescription triggers an implicit rollback of the local offer because the W3C algorithm (§4.4.1) detects the collision and treats it as setLocalDescription(rollback) followed by setRemoteDescription(offer). The roles are symmetric: the call never deadlocks, the call never throws, the only cost is that whichever side was polite throws away its own offer and accepts the other side's. After the round completes, the polite peer's negotiationneeded fires again and it re-emits its own changes on top of the stable connection — exactly the same outcome as if there had been no collision, just one extra round-trip.
The pattern is the canonical answer to a class of problem WebRTC engineers used to write hundreds of lines of custom code to handle. If you are writing new WebRTC code in 2026, Perfect Negotiation is the default — anything else is a relic from the era before rollback existed.
BUNDLE And Trickle ICE — Why 2026 WebRTC Uses One Port
Two extensions to the offer/answer model defined the shape of modern WebRTC and are worth understanding because both touch the SDP.
BUNDLE (RFC 9143, February 2022) lets every media section in the SDP share a single network transport — one ICE component, one DTLS session, one set of UDP ports. The session-level a=group:BUNDLE 0 1 2 line names the media-section ids that participate. Without BUNDLE, a call with audio plus video plus data would open three separate UDP flows, do three ICE checks, do three DTLS handshakes, and traverse three holes in the NAT. With BUNDLE, all three rides on one transport. Set up time drops from 2-3 seconds to under 1 second on a difficult network; firewall traversal becomes proportional to the number of peers rather than the number of media tracks. Modern browsers default to bundlePolicy: "balanced", which gathers candidates only for the first media section of each type; passing bundlePolicy: "max-bundle" forces strict single-transport behavior, which is what production SFUs expect.
Trickle ICE (RFC 8838, January 2021) decouples ICE candidate exchange from offer/answer. Without trickle, the offerer would have to wait until ICE finished gathering every candidate before producing an offer — and "every candidate" includes a STUN round-trip and a TURN allocation, each of which can take 100 to 500 ms. With trickle, the offer goes out immediately with whatever candidates the local agent has gathered so far, and additional candidates flow through the signaling channel as separate messages over the next second or two. The signaling-state machine and the ICE-state machine are now independent: the call can be in have-local-offer for ICE purposes while the application is still trickling candidates, and the call can move to iceConnectionState=connected before the answer arrives. The single line a=ice-options:trickle in the SDP tells the remote side it is safe to receive an offer before candidate gathering completes.
Together, BUNDLE plus Trickle ICE plus rtcp-mux are the reason a 2026 WebRTC connection feels fast where the same connection in 2018 felt slow. The application-side cost is small — three small SDP changes — and the network-side win is large.
The Six Mistakes That Break Production WebRTC
After fifteen years of building WebRTC products, the same six SDP-related mistakes ship over and over. We have seen all of them in audits of other teams' code; we have written each one ourselves at least once. Naming them is the cheapest way to avoid them.
Mistake 1: Forgetting to renegotiate after adding a track. The application calls pc.addTrack(newCameraTrack) after the initial connection is up and assumes the new track will start flowing. It will not. addTrack fires negotiationneeded; if the application does not listen for that event and run the offer/answer dance again, the new track has no m= line and no media. The fix is to register one negotiationneeded handler at peer-connection construction time and let it drive every subsequent renegotiation (W3C WebRTC, §4.4, March 2025).
Mistake 2: Calling createAnswer before setRemoteDescription. The answerer cannot build an answer without seeing the offer first — there is nothing to answer. RFC 9429, §4.1.9-1 makes the requirement explicit: "setRemoteDescription MUST have been called prior to calling createAnswer." The browser will throw InvalidStateError, but it is easy to miss when the signaling layer is asynchronous and a createAnswer() runs in the wrong promise chain.
Mistake 3: Manually editing SDP between createOffer and setLocalDescription. Many old WebRTC tutorials show "SDP munging" — string-replacing the codec preference order, removing video formats the developer dislikes, hard-coding bitrate caps. RFC 9429, §5.4 forbids it outright: "The SDP returned from createOffer or createAnswer MUST NOT be changed before passing it to setLocalDescription." Modifications between setLocalDescription and the network are allowed only to reduce offered capabilities, never to add. Munge instead by using the modern APIs that exist for the purpose: RTCRtpTransceiver.setCodecPreferences() for codec ordering, RTCRtpSender.setParameters() for bitrate, addTransceiver({direction}) for direction.
Mistake 4: Misusing ICE restart. Setting iceRestart: true in createOffer regenerates the ICE credentials and forces both sides to re-run connectivity checks; it interrupts media briefly. It is meant for the case where the ICE connection has gone to failed or disconnected (network change, mobile handoff). Calling it on every offer — which a few tutorials suggest — causes pointless 1-2 second media stalls on every renegotiation (RFC 9429, §4.1.18).
Mistake 5: Treating glare as rare. The two-person 1-to-1 call almost never glares; the 4-person mesh, the SFU-fanned-out screen-share session, the conference where the moderator can mute participants — these glare regularly. Build Perfect Negotiation from day one. The pattern adds maybe twenty lines of code; the alternative is intermittent call drops that defy reproduction.
Mistake 6: Forgetting that BUNDLE-only SFUs reject non-bundled offers. mediasoup, Janus, LiveKit, Jitsi Videobridge, and Pion all default to strict BUNDLE in 2026. An offer that lists separate ICE credentials for the audio and video sections — common in old code copied from pre-2020 tutorials — will be rejected at setRemoteDescription time on the SFU side. The fix is to use RTCConfiguration.bundlePolicy: "max-bundle" (which RFC 9429, §1.3 documents under its newer name "must-bundle" for the next round of revisions) so the browser never produces a non-bundled offer.
A Common Mistake Worth Calling Out Separately
Pitfall — never strip ICE candidates from SDP to hide a public IP. This shows up regularly in code reviews: someone trying to protect a user's IP address by deletinga=candidate:lines containing public-IPv4 addresses before passing the SDP tosetLocalDescription. It does nothing useful. The ICE agent in the browser will still gather those candidates locally, still emit them asicecandidateevents, and still send them through the application's signaling channel — they just never appear in the SDP itself. To genuinely hide the public address, setRTCConfiguration.iceTransportPolicy: "relay", which forces every flow through a TURN server and prevents the host and server-reflexive candidates from being gathered at all (RFC 9429, §3.5.1; W3C WebRTC, §4.2.1.4). The TURN bill goes up; the privacy property holds.
Where Fora Soft Fits In
We have shipped WebRTC products since the standard was a 2012 IETF draft — in video conferencing, telemedicine consultations, e-learning classrooms, OTT live shopping, video surveillance review systems, and AR/VR collaboration. Across more than two hundred shipped projects, the offer/answer state machine is the layer where we have spent the most debug time and the most engineering thought, because it is where the protocol's intent and the application's intent have to meet without leaking either upward. The teams that have asked us to audit their WebRTC stack almost always have at least one of the six mistakes above somewhere in their codebase; we usually find and fix them in days. If you are building a real-time product and the calls are dropping in a way you cannot reproduce, the SDP exchange is the right place to look first.
What To Read Next
- WebRTC Explained Without Arcana
- ICE, STUN, TURN in Depth
- SFU vs MCU vs Mesh: The Three WebRTC Topologies
Talk To Us / See Our Work / Download
- Talk to a WebRTC engineer — book a 30-minute scoping call about your call stack.
- See our case studies — fifteen years of conferencing, telemedicine, and live-classroom builds.
- Download the SDP offer/answer cheat sheet — single-page PDF with the state diagram, the field reference, and the Perfect Negotiation snippet.
References
- IETF, RFC 9429, JavaScript Session Establishment Protocol (JSEP), J. Uberti, C. Jennings, E. Rescorla (eds.), April 2024 — https://www.rfc-editor.org/rfc/rfc9429.html. Obsoletes RFC 8829. The canonical document for browser-side offer/answer.
- IETF, RFC 3264, An Offer/Answer Model with the Session Description Protocol (SDP), J. Rosenberg, H. Schulzrinne, June 2002 — https://www.rfc-editor.org/rfc/rfc3264.html. The original offer/answer rules.
- IETF, RFC 8866, SDP: Session Description Protocol, A. Begen, P. Kyzivat, C. Perkins, M. Handley, January 2021 — https://www.rfc-editor.org/rfc/rfc8866.html. Obsoletes RFC 4566.
- IETF, RFC 9143, Negotiating Media Multiplexing Using the Session Description Protocol (SDP) (BUNDLE), C. Holmberg, H. Alvestrand, C. Jennings, February 2022 — https://www.rfc-editor.org/rfc/rfc9143.html. Obsoletes RFC 8843.
- IETF, RFC 8838, Trickle ICE: Incremental Provisioning of Candidates for the Interactive Connectivity Establishment (ICE) Protocol, E. Ivov, J. Uberti, P. Saint-Andre, January 2021 — https://www.rfc-editor.org/rfc/rfc8838.html.
- IETF, RFC 8839, Session Description Protocol (SDP) Offer/Answer Procedures for Interactive Connectivity Establishment (ICE), M. Petit-Huguenin, S. Nandakumar, A. Keränen, January 2021 — https://www.rfc-editor.org/rfc/rfc8839.html.
- IETF, RFC 8853, Using Simulcast in Session Description Protocol (SDP) and RTP Sessions, B. Burman, M. Westerlund, S. Nandakumar, M. Zanaty, January 2021 — https://www.rfc-editor.org/rfc/rfc8853.html.
- IETF, RFC 5761, Multiplexing RTP Data and Control Packets on a Single Port, C. Perkins, M. Westerlund, April 2010 — https://www.rfc-editor.org/rfc/rfc5761.html.
- IETF, RFC 8122, Connection-Oriented Media Transport over the Transport Layer Security (TLS) Protocol in the Session Description Protocol (SDP), J. Lennox, C. Holmberg, March 2017 — https://www.rfc-editor.org/rfc/rfc8122.html.
- IETF, RFC 8830, WebRTC MediaStream Identification in the Session Description Protocol, H. Alvestrand, January 2021 — https://www.rfc-editor.org/rfc/rfc8830.html.
- W3C, WebRTC: Real-Time Communication in Browsers, Recommendation 13 March 2025 — https://www.w3.org/TR/webrtc/. See §4.4 (RTCPeerConnection algorithms) and §10.7 (Perfect Negotiation Example).
- MDN Web Docs, Perfect Negotiation Pattern — https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation_pattern. The most readable explanation of polite/impolite roles; tracks the W3C example.


