WebRTC Perfect Negotiation: Simplifying Peer Connection
Establishing a peer-to-peer connection in WebRTC has historically required complex, error-prone signaling logic to handle state synchronization and race conditions. The WebRTC Perfect Negotiation pattern solves these issues by decoupling negotiation logic from application state and assigning predictable roles to each peer. This article explains how the Perfect Negotiation pattern works, why it eliminates the notorious “glare” problem, and how it drastically simplifies modern WebRTC implementations.
The Problem: Glare and State Desynchronization
In traditional WebRTC setups, both peers can attempt to modify the connection simultaneously—such as adding a video track at the exact same moment. This creates a collision known as “glare.”
When glare occurs, both peers generate and send a Session Description Protocol (SDP) offer. Because the WebRTC state machine is highly state-sensitive, receiving an offer while you have an outstanding local offer causes a conflict. Without a strict resolution strategy, the connection fails, requiring developers to write complex, fragile state machines to catch errors, roll back descriptions, and renegotiate.
How Perfect Negotiation Solves It
Perfect Negotiation introduces a simple, asymmetrical framework to resolve glare automatically. Instead of treating peers identically, one peer is designated as polite and the other as impolite.
- The Polite Peer: If a collision occurs, the polite peer backs down. It discards its own pending local offer, accepts the incoming offer from the impolite peer, and responds with an answer.
- The Impolite Peer: If a collision occurs, the impolite peer ignores the incoming offer and insists on its own. It waits for the polite peer to back down and accept its offer.
By establishing this simple asymmetry, one peer’s offer is guaranteed to win in any collision scenario. The entire negotiation state is resolved deterministically without the application having to manually track who started the call or tear down the connection.
The Perfect Negotiation Implementation Loop
The beauty of this pattern lies in its implementation. Using modern WebRTC APIs, the logic can be consolidated into a single, reusable event-driven template. The pattern relies on three primary components:
1. Handling the
negotiationneeded Event
Whenever a change occurs that requires negotiation (like adding a
track), the browser fires a negotiationneeded event. The
application simply responds by creating and sending an offer:
peerConnection.onnegotiationneeded = async () => {
try {
makingOffer = true;
await peerConnection.setLocalDescription();
sendSignalingMessage({ description: peerConnection.localDescription });
} catch (err) {
console.error(err);
} finally {
makingOffer = false;
}
};Note: Calling setLocalDescription() without
arguments automatically creates the appropriate offer or answer based on
the current state.
2. Processing Incoming Signaling Messages
When a peer receives an incoming SDP description, it processes it using the polite/impolite logic:
async function handleSignalingMessage({ description, candidate }) {
try {
if (description) {
const offerCollision = (description.type === "offer") &&
(makingOffer || peerConnection.signalingState !== "stable");
// Ignore the offer if we are impolite and a collision occurs
ignoreOffer = !isPolite && offerCollision;
if (ignoreOffer) {
return;
}
// If we are polite and there is a collision, rollback our own offer
if (offerCollision) {
await Promise.all([
peerConnection.setLocalDescription({ type: "rollback" }),
peerConnection.setRemoteDescription(description)
]);
} else {
await peerConnection.setRemoteDescription(description);
}
// If it was an offer, send back an answer
if (description.type === "offer") {
await peerConnection.setLocalDescription();
sendSignalingMessage({ description: peerConnection.localDescription });
}
} else if (candidate) {
try {
await peerConnection.addIceCandidate(candidate);
} catch (err) {
if (!ignoreOffer) throw err; // Ignore candidates if we ignored the offer
}
}
} catch (err) {
console.error(err);
}
}Key Benefits of Perfect Negotiation
By adopting this pattern, developers gain several immediate advantages:
- No Manual State Tracking: You no longer need to
track variables like
isCalleror manually manage peer connection states. - Seamless Dynamic Media: Users can add or remove video, audio, or data channels at any time without fear of breaking the connection.
- Shorter Codebases: Perfect Negotiation reduces hundreds of lines of conditional signaling logic into a single, predictable block of code.
- Automatic Rollbacks: WebRTC’s native
rollbackmechanism handles the heavy lifting of restoring connection states during collisions.