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