我正在尝试学习WebRTC技术。我已经成功地在同一页上连接了两个RTCPeerConnection,并且现在正在尝试将它们分开放在两个不同的页面中并连接它们。
然而,在代码编写完成后并交换offer和answer之后,我发现initiator.html文件中的addIceCandidate()会始终抛出空参数的错误:
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)
经过一些阅读,我了解到null是用来表示ICE候选者收集完成的,并且这里有一个例子:https://webrtc.github.io/samples/src/content/peerconnection/pc1/。当候选者收集完成时,还会执行带有null参数的“addIceCandidate”方法。但是我不明白为什么此时会出现我看到的错误。
以下是我的尝试:
- 我曾尝试编写一个检查函数,如果候选者为空,则跳过addIceCandidate方法。
- 将所有连接逻辑放在更少的按钮中,以减少函数调用之间的延迟。
- 在每个页面中添加adapter-latest.js文件。
结果如下:
- 发起连接状态显示为“失败”,接收者连接状态显示为“新”。无法将流式传输到接收者页面。
- 仍然抛出相同的错误。
- 错误已经消失,但是连接仍然失败。
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);
}
onIceCandidate
函数完全失败了,它在调用自身的addIceCandidate
。 - jib