Skip to content
AI Engineering
AI Engineering11 min0 views

Perfect Negotiation in WebRTC for AI Voice: Polite vs Impolite Peer (2026)

Perfect negotiation is the only sane way to handle SDP renegotiation when an AI agent and a browser both want to change tracks. Here is the 2026 pattern.

Perfect negotiation is the W3C's recommended way to make SDP renegotiation symmetric. For AI voice agents — where the agent might add a tool-call track or the user might enable video mid-call — it is the only pattern that does not deadlock.

Why renegotiation is hard

A WebRTC session is born from one offer/answer round trip. Anything after that — adding a video track, switching codecs, restarting ICE, hot-swapping the AI model — needs another round trip. If both peers try to start renegotiation at the same time you get glare: two offers in flight, two `signalingState` transitions racing each other, and a connection that ends up half-broken.

Pre-2020 the answer was "have the caller always renegotiate." That breaks the moment your AI agent decides to upgrade itself or the browser swaps cameras. Perfect negotiation, codified in the WebRTC spec and documented on MDN, makes negotiation symmetric so the same code runs on both sides.

For AI voice in 2026 the renegotiation triggers are constant:

  • The user changes input device (laptop mic to AirPods).
  • The agent decides to upgrade from `gpt-realtime` to a heavier model mid-call.
  • The browser hits a NAT timeout and triggers ICE restart.
  • The agent wants to add a screen-share track for visual handoff.
  • A simulcast layer reconfiguration kicks in because bandwidth dropped.

Without perfect negotiation each one of these is a potential deadlock.

Architecture pattern

```mermaid flowchart LR Polite[Polite peer - browser] -- offer --> Signal[(signalling)] Impolite[Impolite peer - AI gateway] -- offer --> Signal Signal -- on collision --> Polite Polite -- rollback + accept other --> Impolite ```

One peer is polite; the other is impolite. On collision the polite peer rolls back its own pending offer and accepts the other side's. The impolite peer ignores the colliding incoming offer. The roles are pre-assigned by your application — typically the server-side AI gateway is impolite, the browser is polite — and they never change for the life of the connection.

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →

CallSphere implementation

CallSphere assigns the role at `PeerConnection` creation:

  • Browser (/demo) — `polite = true`. The page can interrupt, change input device, or upgrade to video at any moment; the browser yields to the gateway on collision.
  • Pion Go gateway 1.23 — `polite = false`. The gateway controls model swaps (gpt-realtime to gpt-4o-realtime), tool registry hot-reloads, and bandwidth restarts. When it offers, it wins.

The pattern is the same in our Real Estate vertical (OneRoof, /industries/real-estate) and across the other five (healthcare, behavioral health, legal, salon, insurance). Across 37 agents, 90+ tools, and 115+ database tables we deduplicated negotiation logic into one helper that ships to every browser path. SOC 2 audit notes call out the deterministic role assignment as a control. NATS coordinates the gateway across the 6-container pod (CRM, MLS, calendar, SMS, audit, transcript) so a renegotiation does not lose in-flight tool calls. Pricing $149/$499/$1499; affiliates 22% — see /affiliate.

Code snippet (TypeScript, both ends)

```ts const polite = role === "browser"; let makingOffer = false; let ignoreOffer = false; let isSettingRemoteAnswerPending = false;

pc.onnegotiationneeded = async () => { try { makingOffer = true; await pc.setLocalDescription(); signaler.send({ description: pc.localDescription }); } finally { makingOffer = false; } };

signaler.onmessage = async ({ data: { description, candidate } }) => { if (description) { const readyForOffer = !makingOffer && (pc.signalingState === "stable" || isSettingRemoteAnswerPending); const offerCollision = description.type === "offer" && !readyForOffer; ignoreOffer = !polite && offerCollision; if (ignoreOffer) return;

isSettingRemoteAnswerPending = description.type === "answer";
await pc.setRemoteDescription(description);
isSettingRemoteAnswerPending = false;

if (description.type === "offer") {
  await pc.setLocalDescription();
  signaler.send({ description: pc.localDescription });
}

} else if (candidate) { try { await pc.addIceCandidate(candidate); } catch (err) { if (!ignoreOffer) throw err; } } }; ```

Build steps

  1. Pick the polite peer once, deterministically — usually the browser.
  2. Use the `makingOffer` Boolean instead of `signalingState`; the state changes asynchronously and is unsafe to compare against.
  3. Always set the local description without arguments when reacting to `negotiationneeded` — the modern API derives the offer for you.
  4. Wrap ICE candidate `addIceCandidate` failures in `if (!ignoreOffer) throw` — colliding candidates are normal noise, not errors.
  5. Add a `isSettingRemoteAnswerPending` flag for browsers that signal `have-remote-pranswer` differently.
  6. Test with both peers calling `addTransceiver` simultaneously; if it does not deadlock, your pattern works.
  7. Log role and `signalingState` transitions; they are gold during postmortems.

Common pitfalls

  • Both peers polite — produces deadlocks under bad-luck timing. Pick one.
  • Role-flipping mid-session — never change `polite` after the connection is open. Roles must be deterministic for the lifetime of the PC.
  • Ignoring ICE candidate errors — without the `ignoreOffer` guard you will see noisy "InvalidStateError: addIceCandidate failed" in production logs.
  • Skipping isSettingRemoteAnswerPending — Firefox sometimes fires events between `have-remote-pranswer` and `stable` that this flag handles.
  • Forgetting trickle ICE — perfect negotiation works just as well with trickled candidates; do not batch them.

FAQ

Should the AI agent be polite? No. Make the user-controlled side polite so the user wins on every collision-free offer.

Does this fix ICE restart too? Yes — `pc.restartIce()` triggers `negotiationneeded` and flows through the same path.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

Do I still need rollback? Implicit rollback is automatic when a polite peer calls `setRemoteDescription(offer)` with a colliding offer.

Can both peers be polite? No — that produces deadlocks under bad-luck timing.

Does Pion implement perfect negotiation? Pion exposes the primitives; the pattern is application-level. WebRTC.rs and libwebrtc do the same.

What about Safari? Safari has supported the modern signalingState transitions since 14; perfect negotiation works without polyfills.

Does it work over a third-party SFU? Yes — you negotiate with the SFU on each side; the SFU is just two peers from the perfect-negotiation point of view.

Should I version my signalling messages? Yes — add a `v` field. Renegotiation across a deploy boundary is exactly when version skew bites.

Production playbook for AI voice teams in 2026

Three rules from shipping perfect negotiation across all six verticals:

  1. One PeerConnection per session. Do not create a new PC for every renegotiation. The whole point is to reuse the existing one.
  2. Treat `negotiationneeded` as a hint. It can fire spuriously (Chromium has fixed many but not all of these). Always check whether you actually have changes before sending an offer.
  3. Have a kill-switch. If the connection's been "have-local-offer" for >10 s, force a teardown. Do not assume the network will recover.

The third rule is the controversial one. It produces about 0.4% spurious teardowns in our data. The trade-off is worth it: stuck-state sessions used to be our top-3 voice support ticket, and the kill-switch eliminated them.

Sources

Try a stable renegotiation flow on /demo or start a /trial.

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like