Search code examples
javascriptwebrtcrtcpeerconnection

addIceCandidate with argument null result in error


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

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)

After some reading, I learnt null is used to indicate ICE Candidate gathering finishes and example here: https://webrtc.github.io/samples/src/content/peerconnection/pc1/ 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.

What I had tried:

  1. I had tried to write a check such that if candidate is null, skip addIceCandidate.
  2. Place all connection logic in less buttons to reduce delay between function calls
  3. Add adapter-latest.js to each page

Result:

  1. Initiator connection state is "fail", receiver connection state is "new". Failed to stream to receiver page.
  2. Same error was thrown
  3. Error is gone, but connection still fails

initiator.html

<!doctype html>
<html lang="en">
  <head>
    <title>First WebRTC Project</title>
        <link href="common.css" rel="stylesheet" />
  </head>
  <body>
        <div class="log-display"></div>
        <div class="func-list">
            Initiating host
            <div class="func">
                <button onclick="onPrepareMedia(this)">Prepare media</button>
                <video class="dump"></video>
            </div>
            <div class="func">
                <button onclick="onCreatePeerConnection(this)">onCreatePeerConnection()</button>
                <textarea class="dump"></textarea>
            </div>
            <div class="func">
                <button onclick="onCreateOffer(this)">onCreateOffer()</button>
                <textarea class="dump"></textarea>
            </div>
            <div class="func">
                <button onclick="onSetLocalDescription(this)">onSetLocalDescription()</button>
                <textarea class="dump"></textarea>
            </div>
            <div class="func">
                <button onclick="onSetRemoteDescription(this, answerReceived)">onSetRemoteDescription() // set answerReceived variable manually</button>
                <textarea class="dump"></textarea>
            </div>
        </div>
        <script src="common.js"></script>
        <script>
            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 && await peerConn.addIceCandidate(e.candidate).catch(e => 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 => 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 => 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();
            }
        </script>
  </body>
</html>

receiver.html

<!doctype html>
<html lang="en">
  <head>
    <title>First WebRTC Project</title>
        <link href="common.css" rel="stylesheet" />
  </head>
  <body>
        <div class="log-display"></div>
        <div class="func-list">
            Receiving host
            <div class="func">
                <button >Received video</button>
                <video class="dump"></video>
            </div>
            <div class="func">
                <button onclick="onCreatePeerConnection(this)">onCreatePeerConnection()</button>
                <textarea class="dump"></textarea>
            </div>
            <div class="func">
                <button onclick="onSetRemoteDescription(this)">onSetRemoteDescription()</button>
                <textarea class="dump"></textarea>
            </div>
            <div class="func">
                <button onclick="onCreateAnswer(this)">onCreateAnswer()</button>
                <textarea class="dump"></textarea>
            </div>
            <div class="func">
                <button onclick="onSetLocalDescription(this)">onSetLocalDescription()</button>
                <textarea class="dump"></textarea>
            </div>
        </div>
        <script src="common.js"></script>
        <script>
            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 && await peerConn.addIceCandidate(e.candidate).catch(e => 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);
            }
        </script>
  </body>
</html>

common.js

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);
}

common.css

.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);
}

Solution

  • Why does supplying addIceCandidate with null result in error while the example code works fine?

    It's because your browser is not up to spec. addIceCandidate(null) is valid in the latest spec, and indistinguishable from addIceCandidate() and addIceCandidate({}). They all signal end-of-candidates from the remote end.

    The WebRTC samples work because they use adapter.js, which polyfills the correct spec behavior on older browsers.