title: addIceCandidate with argument null result in error
body: <p>I am trying to learn WebRTC, I had achieved connecting two RTCPeerConnection in same page and I am now attempting to separate them into two separate pages and connects them.
  However, after code are written and exchanged offer and answer, I noticed addIceCandidate() on initiator.html will always throw this with null argument</p>
  
  <p><code>Error at addIceCandidate from queue:  TypeError: Failed to execute 'addIceCandidate' on 'RTCPeerConnection': Candidate missing values for both sdpMid and sdpMLineIndex
      at processCandidateQueue (initiator.html:69)</code></p>
  
  <p>After some reading, I learnt null is used to indicate ICE Candidate gathering finishes and example here: <a href="https://webrtc.github.io/samples/src/content/peerconnection/pc1/" rel="nofollow noreferrer">https://webrtc.github.io/samples/src/content/peerconnection/pc1/</a>
  Also executes "addIceCandidate" with argument null when gathering finishes.
  But I do not understand why I am seeing the error I see at this moment.</p>
  
  <p>What I had tried:</p>
  
  <ol>
  <li>I had tried to write a check such that if candidate is null, skip addIceCandidate.</li>
  <li>Place all connection logic in less buttons to reduce delay between function calls</li>
  <li>Add adapter-latest.js to each page</li>
  </ol>
  
  <p>Result:</p>
  
  <ol>
  <li>Initiator connection state is "fail", receiver connection state is "new". Failed to stream to receiver page.</li>
  <li>Same error was thrown</li>
  <li>Error is gone, but connection still fails</li>
  </ol>
  
  <p>initiator.html</p>
  
  <pre><code>&lt;!doctype html&gt;
  &lt;html lang="en"&gt;
    &lt;head&gt;
      &lt;title&gt;First WebRTC Project&lt;/title&gt;
          &lt;link href="common.css" rel="stylesheet" /&gt;
    &lt;/head&gt;
    &lt;body&gt;
          &lt;div class="log-display"&gt;&lt;/div&gt;
          &lt;div class="func-list"&gt;
              Initiating host
              &lt;div class="func"&gt;
                  &lt;button onclick="onPrepareMedia(this)"&gt;Prepare media&lt;/button&gt;
                  &lt;video class="dump"&gt;&lt;/video&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onCreatePeerConnection(this)"&gt;onCreatePeerConnection()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onCreateOffer(this)"&gt;onCreateOffer()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onSetLocalDescription(this)"&gt;onSetLocalDescription()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onSetRemoteDescription(this, answerReceived)"&gt;onSetRemoteDescription() // set answerReceived variable manually&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
          &lt;/div&gt;
          &lt;script src="common.js"&gt;&lt;/script&gt;
          &lt;script&gt;
              localStorage.removeItem("FirstWebRTC_offer");
              localStorage.removeItem("FirstWebRTC_answer");
              var constraints = { video: true, audio: true };
              var stream = null;
              var peerConn = null;
              var offer = null, offerReceived = null;
              var answer = null, answerReceived = null;
              const offerOptions = {
                  offerToReceiveAudio: 1,
                  offerToReceiveVideo: 1
              };
  
              candidateQueue = [];
              var onIceCandidate = async function(e) {
                  window.log("onIceCandidate", e);
                  if(peerConn.remoteDescription) {
                      var rslt = e.candidate &amp;&amp; await peerConn.addIceCandidate(e.candidate).catch(e =&gt; onError("addIceCandidate", e));
                  } else {
                      candidateQueue.push(e.candidate);
                  }
                  window.log(JSON.stringify(rslt));
              };
              var onIceConnectionStateChange = function(e) {
                  window.log("onIceConnectionStateChange", e);
              };
              var onNegotiationNeeded = function(e) {
                  console.log("-----", e);
              }
  
              var processCandidateQueue = async function() {
                  for(var i in candidateQueue) {
                      var candidate = candidateQueue[i];
                      await peerConn.addIceCandidate(candidate).catch(e =&gt; onError("addIceCandidate from queue", e));
                  }
              }
  
              async function onPrepareMedia(e) {
                  stream = await navigator.mediaDevices.getUserMedia(constraints);
                  e.parentElement.children[1].value = dumpProperty(stream)
                  video = e.parentElement.children[1];
                  video.srcObject = stream;
                  video.play();
              }
  
              function onCreatePeerConnection(e) {
                  peerConn = new RTCPeerConnection({});
  
                  // Setup ICE event handlers
                  peerConn.onicecandidate = onIceCandidate;
                  peerConn.oniceconnectionstatechange = onIceConnectionStateChange;
                  peerConn.onnegotiationneeded = onNegotiationNeeded
  
                  // Add tracks to be transmitted
                  stream.getTracks().forEach(track =&gt; peerConn.addTrack(track, stream));
  
                  e.parentElement.children[1].value = dumpProperty(peerConn)
              }
  
              async function onCreateOffer(e) {
                  offer = await peerConn.createOffer(offerOptions)
                  localStorage.setItem("FirstWebRTC_offer", JSON.stringify(offer))
                  e.parentElement.children[1].value = dumpProperty(offer)
              }
  
              async function onSetLocalDescription(e) {
                  var rslt = await peerConn.setLocalDescription(offer)
                  e.parentElement.children[1].value = dumpProperty(rslt)
              }
  
              async function onSetRemoteDescription(e) {
                  answerReceived = JSON.parse(localStorage.getItem("FirstWebRTC_answer"));
                  rslt = await peerConn.setRemoteDescription(answerReceived)
                  e.parentElement.children[1].value = dumpProperty(rslt)
                  processCandidateQueue();
              }
          &lt;/script&gt;
    &lt;/body&gt;
  &lt;/html&gt;
  </code></pre>
  
  <p>receiver.html</p>
  
  <pre><code>&lt;!doctype html&gt;
  &lt;html lang="en"&gt;
    &lt;head&gt;
      &lt;title&gt;First WebRTC Project&lt;/title&gt;
          &lt;link href="common.css" rel="stylesheet" /&gt;
    &lt;/head&gt;
    &lt;body&gt;
          &lt;div class="log-display"&gt;&lt;/div&gt;
          &lt;div class="func-list"&gt;
              Receiving host
              &lt;div class="func"&gt;
                  &lt;button &gt;Received video&lt;/button&gt;
                  &lt;video class="dump"&gt;&lt;/video&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onCreatePeerConnection(this)"&gt;onCreatePeerConnection()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onSetRemoteDescription(this)"&gt;onSetRemoteDescription()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onCreateAnswer(this)"&gt;onCreateAnswer()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
              &lt;div class="func"&gt;
                  &lt;button onclick="onSetLocalDescription(this)"&gt;onSetLocalDescription()&lt;/button&gt;
                  &lt;textarea class="dump"&gt;&lt;/textarea&gt;
              &lt;/div&gt;
          &lt;/div&gt;
          &lt;script src="common.js"&gt;&lt;/script&gt;
          &lt;script&gt;
              localStorage.removeItem("FirstWebRTC_offer");
              localStorage.removeItem("FirstWebRTC_answer");
              var constraints = { video: true, audio: true };
              var stream = null;
              var peerConn = null;
              var offer = null, offerReceived = null;
              var answer = null, answerReceived = null;
              const offerOptions = {
                  offerToReceiveAudio: 1,
                  offerToReceiveVideo: 1
              };
  
              var onTrack = function(e) {
                  console.log(e);
                  video = document.querySelector("video")
                  if (video.srcObject !== e.streams[0]) {
                      video.srcObject = e.streams[0];
                      video.play();
                      console.log('received and playing remote stream');
                  }
              }
  
              var onIceCandidate = async function(e) {
                  window.log("onIceCandidate", e);
                  var rslt = e.candidate &amp;&amp; await peerConn.addIceCandidate(e.candidate).catch(e =&gt; onError("addIceCandidate", e));
                  window.log(JSON.stringify(rslt));
              };
              var onIceConnectionStateChange = function(e) {
                  window.log("onIceConnectionStateChange", e);
              };
  
              function onCreatePeerConnection(e) {
                  peerConn = new RTCPeerConnection({});
  
                  // Setup ICE event handlers
                  peerConn.onicecandidate = onIceCandidate;
                  peerConn.oniceconnectionstatechange = onIceConnectionStateChange;
                  peerConn.ontrack = onTrack;
  
                  e.parentElement.children[1].value = dumpProperty(peerConn);
              }
  
              async function onSetRemoteDescription(e) {
                  offerReceived = JSON.parse(localStorage.getItem("FirstWebRTC_offer"));
                  rslt = await peerConn.setRemoteDescription(offerReceived);
                  e.parentElement.children[1].value = dumpProperty(rslt);
              }
  
              async function onCreateAnswer(e) {
                  answer = await peerConn.createAnswer(offerReceived);
                  localStorage.setItem("FirstWebRTC_answer", JSON.stringify(answer));
                  e.parentElement.children[1].value = dumpProperty(answer);
              }
  
              async function onSetLocalDescription(e) {
                  var rslt = await peerConn.setLocalDescription(answer);
                  e.parentElement.children[1].value = dumpProperty(rslt);
              }
          &lt;/script&gt;
    &lt;/body&gt;
  &lt;/html&gt;
  </code></pre>
  
  <p>common.js</p>
  
  <pre><code>function dumpProperty(obj, noJSON) {
      var output = JSON.stringify(obj);
      if(output == "{}" || noJSON) {
          output = ""
          for (var property in obj) {
              output += property + ': ' + obj[property]+';\n';
          }
      }
      return output;
  }
  
  function onError(name, e) {
      console.warn("Error at " + name + ": ", e);
  }
  
  window.log = function(str, obj) {
      var logDisplay = document.getElementsByClassName('log-display')[0];
      if(logDisplay) {
          var newLog = document.createElement("div");
          newLog.innerText = str + " : " + dumpProperty(obj);
          logDisplay.appendChild(newLog);
      }
      console.log(str, obj);
  }
  </code></pre>
  
  <p>common.css</p>
  
  <pre><code>.connection-flow-diagram {
      display: flex;
      text-align: center;
  }
  .func-list {
      display: flex;
      flex-direction: column;
      flex-wrap: wrap;
      justify-content: space-around;
      width: 50%;
      margin-left: auto;
      margin-right: auto;
      text-align: center;
  }
  .func {
      padding: 1rem;
      display: flex;
      flex-direction: column;
      border: 1px dashed black;
  }
  .func button {
  
  }
  .func .dump {
      height: 180px;
  }
  .log-display {
      position: fixed;
      left: 0;
      top: 0;
      width: 100vw;
      height: 100vh;
      pointer-events: none;
      color: rgba(0,0,0,0.4);
  }
  </code></pre>