10.7 Perfect Negotiation Example

Perfect negotiation is a recommended pattern to manage negotiation transparently, abstracting this asymmetric task away from the rest of an application. This pattern has advantages over one side always being the offerer, as it lets applications operate on both peer connection objects simultaneously without risk of glare (an offer coming in outside of “stable“ state). The rest of the application may use any and all modification methods and attributes, without worrying about signaling state races.

It designates different roles to the two peers, with behavior to resolve signaling collisions between them:

  1. The polite peer uses rollback to avoid collision with an incoming offer.

  2. The impolite peer ignores an incoming offer when this would collide with its own.

Together, they manage signaling for the rest of the application in a manner that doesn’t deadlock. The example assumes a polite boolean variable indicating the designated role:

Example 18

  1. const signaling = new SignalingChannel(); // handles JSON.stringify/parse
  2. const constraints = {audio: true, video: true};
  3. const configuration = {iceServers: [{urls: 'stun:stun.example.org'}]};
  4. const pc = new RTCPeerConnection(configuration);
  5. // call start() anytime on either end to add camera and microphone to connection
  6. async function start() {
  7. try {
  8. const stream = await navigator.mediaDevices.getUserMedia(constraints);
  9. for (const track of stream.getTracks()) {
  10. pc.addTrack(track, stream);
  11. }
  12. selfView.srcObject = stream;
  13. } catch (err) {
  14. console.error(err);
  15. }
  16. }
  17. pc.ontrack = ({track, streams}) => {
  18. // once media for a remote track arrives, show it in the remote video element
  19. track.onunmute = () => {
  20. // don't set srcObject again if it is already set.
  21. if (remoteView.srcObject) return;
  22. remoteView.srcObject = streams[0];
  23. };
  24. };
  25. // - The perfect negotiation logic, separated from the rest of the application ---
  26. // keep track of some negotiation state to prevent races and errors
  27. let makingOffer = false;
  28. let ignoreOffer = false;
  29. let isSettingRemoteAnswerPending = false;
  30. // send any ice candidates to the other peer
  31. pc.onicecandidate = ({candidate}) => signaling.send({candidate});
  32. // let the "negotiationneeded" event trigger offer generation
  33. pc.onnegotiationneeded = async () => {
  34. try {
  35. makingOffer = true;
  36. await pc.setLocalDescription();
  37. signaling.send({description: pc.localDescription});
  38. } catch (err) {
  39. console.error(err);
  40. } finally {
  41. makingOffer = false;
  42. }
  43. };
  44. signaling.onmessage = async ({data: {description, candidate}}) => {
  45. try {
  46. if (description) {
  47. // An offer may come in while we are busy processing SRD(answer).
  48. // In this case, we will be in "stable" by the time the offer is processed
  49. // so it is safe to chain it on our Operations Chain now.
  50. const readyForOffer =
  51. !makingOffer &&
  52. (pc.signalingState == "stable" || isSettingRemoteAnswerPending);
  53. const offerCollision = description.type == "offer" && !readyForOffer;
  54. ignoreOffer = !polite && offerCollision;
  55. if (ignoreOffer) {
  56. return;
  57. }
  58. isSettingRemoteAnswerPending = description.type == "answer";
  59. await pc.setRemoteDescription(description); // SRD rolls back as needed
  60. isSettingRemoteAnswerPending = false;
  61. if (description.type == "offer") {
  62. await pc.setLocalDescription();
  63. signaling.send({description: pc.localDescription});
  64. }
  65. } else if (candidate) {
  66. try {
  67. await pc.addIceCandidate(candidate);
  68. } catch (err) {
  69. if (!ignoreOffer) throw err; // Suppress ignored offer's candidates
  70. }
  71. }
  72. } catch (err) {
  73. console.error(err);
  74. }
  75. }

Note that this is timing sensitive, and deliberately uses versions of setLocalDescription (without arguments) and setRemoteDescription (with implicit rollback) to avoid races with other signaling messages being serviced.

The ignoreOffer variable is needed, because the RTCPeerConnection object on the impolite side is never told about ignored offers. We must therefore suppress errors from incoming candidates belonging to such offers.