使用 null 作为参数调用 addIceCandidate 会导致错误。

3

我正在尝试学习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”方法。但是我不明白为什么此时会出现我看到的错误。

以下是我的尝试:

  1. 我曾尝试编写一个检查函数,如果候选者为空,则跳过addIceCandidate方法。
  2. 将所有连接逻辑放在更少的按钮中,以减少函数调用之间的延迟。
  3. 在每个页面中添加adapter-latest.js文件。

结果如下:

  1. 发起连接状态显示为“失败”,接收者连接状态显示为“新”。无法将流式传输到接收者页面。
  2. 仍然抛出相同的错误。
  3. 错误已经消失,但是连接仍然失败。

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

你的问题是什么? - jib
@jib 为什么使用 null 作为 addIceCandidate 的参数会导致错误,而示例代码却可以正常工作? - 8749236
你正在使用哪个浏览器(及版本)遇到这个错误?请注意,你的 onIceCandidate 函数完全失败了,它在调用自身的 addIceCandidate - jib
你的代码还在排队ICE候选者,这很复杂且不必要。 - jib
2个回答

5
为什么使用null调用addIceCandidate会导致错误,而示例代码却可以正常工作?
这是因为您的浏览器不符合规范。在最新规范中,addIceCandidate(null)是有效的,并且与addIceCandidate()addIceCandidate({})无法区分。它们都表示远程端候选项结束。 WebRTC样例可以正常工作,因为它们使用adapter.js,该文件在旧版浏览器上模拟了正确的规范行为。

-1

经过一些阅读,我已经找出了为什么我的代码不起作用的原因。它包含了一个与这个问题标题无关的致命缺陷。

首先,回答标题的问题。 问:"为什么使用 null 提供 addIceCandidate() 会导致错误?" 答:这是因为我读了一篇关于 WebRTC 的过时文章,在过去的某个时间里,addIceCandidate() 能够接受 null 值并且能够正常工作。然而,截至2019年4月25日,情况已经发生了改变。现在的实现方式如下:

如果事件的 candidate 属性为 null,则 ICE 收集已完成。

MDN - Event: RTCPeer​Connection​.onicecandidate

因此,为了正确处理这种情况,我需要测试 null 候选者。

onIceCandidateHandler(e)
    if e.candidate is not null
        signalingMedium.sendToRemote(e.candidate)
    else
        do nothing

这就是为什么当我添加了adapter-latest.js时,错误消失了;它将addIceCandidate()替换为防止空候选项。

其次,我提到尽管添加了adapter-latest.js,错误消失了。这是因为我使用了错误的信令方式。

以下是来自MDN的icecandidate事件描述:

每当本地ICE代理需要通过信令服务器向其他对等方传递消息时,就会发生这种情况。

...

只需实现此方法即可使用您选择的任何消息传递技术将ICE候选项发送到远程对等方。

而在我的代码中,我将候选项添加到本地对等连接中(这是错误的)。

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

因此连接总是失败,因为我实际上是在连接自己。

一旦我解决了这个问题,我将提供一个带有更正代码的jsFiddle。


2
这个答案是不正确的,因为在最新规范中,addIceCandidate(null)实际上是有效的。它和addIceCandidate()以及addIceCandidate({})没有区别。 - jib

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接