addIceCandidate с аргументом null приведет к ошибке

Я пытаюсь изучить WebRTC, мне удалось подключить два RTCPeerConnection на одной странице, и теперь я пытаюсь разделить их на две отдельные страницы и соединить их. Однако после того, как код написан и обменен предложением и ответом, я заметил, что addIceCandidate () на initiator.html всегда будет выдавать это с нулевым аргументом

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/ Также выполняет addIceCandidate с нулевым аргументом после завершения сбора. Но я не понимаю, почему я вижу ту ошибку, которую вижу в данный момент.

Что я пробовал:

  1. Я пытался написать чек, чтобы, если кандидат равен нулю, пропустить addIceCandidate.
  2. Поместите всю логику подключения в меньшее количество кнопок, чтобы уменьшить задержку между вызовами функций
  3. Добавьте адаптер-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);
}

person 8749236    schedule 25.04.2019    source источник
comment
В чем именно заключается ваш вопрос?   -  person jib    schedule 25.04.2019
comment
@jib, почему добавление null для addIceCandidate приведет к ошибке, в то время как код примера работает нормально.   -  person 8749236    schedule 25.04.2019
comment
Из какого браузера (и версии) вы получаете эту ошибку? Обратите внимание, ваша функция onIceCandidate полностью остановлена, вызывая addIceCandidate на себя.   -  person jib    schedule 27.04.2019
comment
Ваш код также ставит в очередь кандидатов ICE, что сложно и не нужно.   -  person jib    schedule 27.04.2019


Ответы (2)


Почему предоставление addIceCandidate с нулевым значением приводит к ошибке, в то время как код примера работает нормально?

Это потому, что ваш браузер не соответствует спецификации. addIceCandidate(null) действителен в последней спецификации и неотличим от addIceCandidate() и addIceCandidate({}) . Все они сигнализируют о конце кандидатов с удаленного конца.

Примеры WebRTC работают, потому что они используют adapter.js, который обеспечивает правильное поведение спецификации в старых браузерах.

person jib    schedule 28.04.2019

После некоторого чтения я понял, почему мой код не работает. Он содержит фатальный недостаток, не связанный с названием этого вопроса.

Сначала ответьте на вопрос о названии. ВПочему предоставление addIceCandidate () значения null приведет к ошибке? О: Это потому, что я прочитал устаревшую статью о WebRTC, где когда-то в прошлом addIceCandidate () мог принимать нулевое значение, и он будет счастлив. Однако с 25 апреля 2019 года это уже не так. Вместо этого с текущей реализацией:

Если свойство кандидата события равно нулю, сборка ICE завершена.

MDN - событие: соединение RTCPeer .onicecandidate

Поэтому, чтобы правильно обработать этот случай, мне нужно проверить нулевого кандидата

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

Вот почему, когда я добавил адаптер-latest.js, ошибка исчезла; он заменяет addIceCandidate () для защиты от нулевого кандидата

Во-вторых, я упомянул, хотя ошибка исчезает, когда добавляется адаптер-latest.js. Это потому, что я сигнализировал неправильно.

Вот описание события icecandidate из MDN

Это происходит всякий раз, когда локальному агенту 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 с исправленным кодом, как только исправлю проблему.

person 8749236    schedule 26.04.2019
comment
Этот ответ неверен, поскольку addIceCandidate(null) действительно действителен в последней спецификации . Он неотличим от addIceCandidate() и addIceCandidate({}). - person jib; 27.04.2019