1

I have been trying to implement WebRTC "perfect negotiation" as explained in this blog post. Unfortunately, I'm very often triggering errors on the polite side of the conversation (in the following linked code, this is the last peer to join).

The two most frequent errors are InvalidStateError: Cannot rollback local description in stable caused by this line and ONN DOMException: "Cannot set local offer in state have-remote-offer" triggered here and caused by this line. That second error is quite inexplicable to me since the line before checks that we should be in stable state and it is the first async call following that.

If you want to try it for yourself, my code is hosted on GitHub. It can be deployed directly on Heroku:

heroku create
git push heroku master

or can be tested locally with a self-signed certificate by changing lines 1, 7-10 and 15 of server.js.

I have had those issues when testing with desktop Firefox at versions 76, 73, desktop Chromium 80 and their up-to-date mobile versions on Android. Any help on this would be very much appreciated. And I'd love to also have this reliably working on iPads (Safari >= 11) which I can't personally test.

jib
  • 40,579
  • 17
  • 100
  • 158
  • 1
    This has been [fixed](https://bugzilla.mozilla.org/show_bug.cgi?id=1628139) in Firefox 77 and in the [spec](https://github.com/w3c/webrtc-pc/pull/2509). In Chrome see [crbug 1085748](https://bugs.chromium.org/p/chromium/issues/detail?id=1085748). [adapter.js](https://github.com/webrtcHacks/adapter) may help with the latter. – jib May 23 '20 at 02:57
  • Ah thank you for the information @jib ! Maybe an update note on the blog post would be helpful? – Matthieu Pizenberg May 23 '20 at 09:34
  • 1
    Good idea. Also hoping to do a follow-up blog once Firefox 77 hits release and Chrome has been fixed! – jib May 23 '20 at 22:45

1 Answers1

4

TL;DR: Browser bugs fixed in Firefox 77 & the spec, as well as a separate bug in Chrome.

ONN DOMException: "Cannot set local offer in state have-remote-offer" ... That second error is quite inexplicable to me since the line before checks that we should be in stable state and it is the first async call following that.

WebRTC negotiation code has the tricky task of translating synchronous signaling messages into asynchronous operations on the RTCPeerConnection object. To ensure order, these asynchronous methods are chained internally, so only one runs at a time, because state machine.

Under signaling stress, it's possible for this internal chain to fill up, which complicates things, because methods you invoke end up running later, not immediately in the state you just observed synchronously.

The "second error" happened because the browser erroneously fired the negotiationneeded event while the RTCPeerConnection object's internal operations chain was non-empty. The spec has now been fixed so this won't happen.

Since the browser controls when the negotiationneeded event is fired, this takes care of it.

InvalidStateError: Cannot rollback local description in stable

The "first error" on the impolite side is similarly a symptom of an overloaded chain: synchronous state check that we're NOT in stable, followed by this:

if (offerCollision) { // pc.signalingState != "stable"
  await Promise.all([
    pc.setLocalDescription({ type: "rollback" }),
    pc.setRemoteDescription(description),
  ]);
}

I suspect other negotiation methods are on the chain at this time that take us back to "stable" before this setLocalDescription rollback gets to run.

To solve this, the spec added implicit rollback, so you can replace the above with:

await pc.setRemoteDescription(description); // implicit rollback only if needed

This works better because the decision of whether rollback is needed or not is made inside the chained method, looking at the correct signaling state at that (potentially later) time.

This unfortunately is not that easy to polyfill. You might attempt something like:

if (offerCollision) { // pc.signalingState != "stable"
  await Promise.all([
    pc.setLocalDescription({type: "rollback"}).catch(() => {}), // ignore failure
    pc.setRemoteDescription(description),
  ]);
}

Or wait for browsers to implement implicit rollback.

jib
  • 40,579
  • 17
  • 100
  • 158
  • 1
    Re. type: "rollback": unfortunately there's a bug in Safari which makes this tricky to get right :( (https://stackoverflow.com/questions/62331155/webrtc-with-perfect-negotiation-rollback-on-mobile-safari-does-not-work). Is there any way to teardown a peer connection and start afresh? Thanks for all your work on this! – Dom Christie Feb 19 '21 at 07:31