Javascript WebRTC Не удалось установить удаленный ответ sdp: вызов в неправильном состоянии: kHaveRemoteOffer и вызов в неправильном состоянии: kStable

Я не могу заставить свой код WebRTC работать должным образом. Я все сделал правильно, и он все еще не работает. Есть что-то странное, почему ontrack вызывается так рано, может быть, так и должно быть.

Веб-сайт использует код javascript, код сервера, который я не публиковал, но там, где соединение WebSockets является просто обменником, то, что вы отправляете на сервер, отправляет ту же информацию обратно другому партнеру (незнакомцу), с которым вы тоже подключены.

Код сервера выглядит как этот небольшой пример

    private void writeStranger(UserProfile you, String msg) {
        UserProfile stranger = you.stranger;
        if(stranger != null)
            sendMessage(stranger.getWebSocket(), msg);
    }

    public void sendMessage(WebSocket websocket, String msg) {
        try {
            websocket.send(msg);
        } catch ( WebsocketNotConnectedException e ) {
            disconnnectClient(websocket);
        }
    }

   //...

        case "ice_candidate":
            JSONObject candidatePackage = (JSONObject) packet.get(1);
            JSONObject candidate = (JSONObject) candidatePackage.get("candidate");

            obj = new JSONObject();
            list = new JSONArray();

            list.put("iceCandidate");
            obj.put("candidate", candidate);
            list.put(obj);

            System.out.println("Sent = " + list.toString());

            writeStranger(you, list.toString()); //send ice candidate to stranger

            break;
        case "send_answer":
            JSONObject sendAnswerPackage = (JSONObject) packet.get(1);
            JSONObject answer = (JSONObject) sendAnswerPackage.get("answer");

            obj = new JSONObject();
            list = new JSONArray();

            list.put("getAnswer");
            obj.put("answer", answer);
            list.put(obj);

            System.out.println("Sent = " + list.toString());

            writeStranger(you, list.toString()); //send answer to stranger

            break;
        case "send_offer":
            JSONObject offerPackage = (JSONObject) packet.get(1);
            JSONObject offer = (JSONObject) offerPackage.get("offer");

            obj = new JSONObject();
            list = new JSONArray();

            list.put("getOffer");
            obj.put("offer", offer);
            list.put(obj);

            System.out.println("Sent = " + list.toString());

            writeStranger(you, list.toString()); //send ice candidate to stranger

            break;

Вот мои выходы.
Текст RAW: https://pastebin.com/raw/FL8g29gG
цветной JSON : https://pastebin.com/FL8g29gG

Мой код javascript ниже

var ws;

var peerConnection, localStream;    
var rtc_server = {
  iceServers: [
                {urls: "stun:stun.l.google.com:19302"},
                {urls: "stun:stun.services.mozilla.com"},
                {urls: "stun:stun.stunprotocol.org:3478"},
                {url: "stun:stun.l.google.com:19302"},
                {url: "stun:stun.services.mozilla.com"},
                {url: "stun:stun.stunprotocol.org:3478"},
  ]
}

//offer SDP's tells other peers what you would like
var rtc_media_constraints = {
  mandatory: {
    OfferToReceiveAudio: true,
    OfferToReceiveVideo: true
  }
};

var rtc_peer_options = {
  optional: [
              {DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
  ]
}

var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

function hasSupportForVideoChat() {
   return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}

function loadMyCameraStream() {
    if (getUserMedia) {
      getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
        function(localMediaStream) {
          //Add my video
          $("div#videoBox video#you")[0].muted = true;
          $("div#videoBox video#you")[0].autoplay = true;
          $("div#videoBox video#you").attr('playsinline', '');
          $("div#videoBox video#you").attr('webkit-playsinline', '');
          $("div#videoBox video#you")[0].srcObject = localMediaStream;
          localStream = localMediaStream;
        },
        function(e) {
          addStatusMsg("Your Video has error : " + e);
        }
      );
    } else {
      addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
      return;
    }
}

function loadStrangerCameraStream() {
    if(!hasSupportForVideoChat())
      return;

    peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
    if (peerConnection.addTrack !== undefined)
      localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
    else
      peerConnection.addStream(localStream);

    peerConnection.onicecandidate = function(e) {
      if (!e || !e.candidate)
        return;
      ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
    };

    if (peerConnection.addTrack !== undefined) {
      //newer technology
      peerConnection.ontrack = function(e) {
        //e.streams.forEach(stream => doAddStream(stream));
        addStatusMsg("ontrack called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.streams[0];
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    } else {
      //older technology
      peerConnection.onaddstream = function(e) {
        addStatusMsg("onaddstream called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.stream;
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    }

    peerConnection.createOffer(
      function(offer) {
        peerConnection.setLocalDescription(offer, function () {
          //both offer and peerConnection.localDescription are the same.
          addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
          //addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
          ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
        },
        function(e) {
          addStatusMsg('createOffer, set description error' + e);
        });
      },
      function(e) {
        addStatusMsg("createOffer error: " + e);
      },
      rtc_media_constraints
    );
}

function closeStrangerCameraStream() {
    $('div#videoBox video#stranger')[0].srcObject = null
    if(peerConnection)
      peerConnection.close();
}     

function iceCandidate(candidate) {
  //ICE = Interactive Connectivity Establishment
  if(peerConnection)
    peerConnection.addIceCandidate(new IceCandidate(candidate));
  else
    addStatusMsg("peerConnection not created error");
  addStatusMsg("Peer Ice Candidate = " + JSON.stringify(candidate));
}

function getAnswer(answer) {    
    if(!hasSupportForVideoChat())
      return;

    if(peerConnection) {
  peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
    console.log("get answer ok");
    addStatusMsg("peerConnection, SessionDescription answer is ok");
  },
  function(e) {
    addStatusMsg("peerConnection, SessionDescription fail error: " + e);
  });
    }
}

function getOffer(offer) {
    if(!hasSupportForVideoChat())
      return;
    addStatusMsg("peerConnection, setRemoteDescription offer: " + JSON.stringify(offer));
    if(peerConnection) {
      peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
        peerConnection.createAnswer(
          function(answer) {
            peerConnection.setLocalDescription(answer);
            addStatusMsg("create answer sent: " + JSON.stringify(answer));
            ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
          },
          function(e) {
            addStatusMsg("peerConnection, setRemoteDescription create answer fail: " + e);
          }
        );
      });
    }
}

person SSpoke    schedule 11.03.2020    source источник


Ответы (1)


Мой веб-сайт, на котором я его использую: https://www.camspark.com/
Исправил сам, я понял out У меня было 2 проблемы с этим кодом.

Первая проблема заключалась в том, что createOffer() должен быть отправлен только 1 человеком, а не обоими людьми. Вам нужно случайным образом выбрать, кто делает createOffer().

Вторая проблема заключается в том, что для ICE Candidate вам необходимо создать очередь/массив для обеих сторон, которая содержит все входящие ice_candidates. Выполняйте peerConnection.addIceCandidate(new IceCandidate(candidate)); только тогда, когда получен ответ на createOffer() и настроен ответ setRemoteDescription от createOffer().

И getAnswer(), и getOffer() используют один и тот же код, но один из них принимается для одного клиента, а другой — для другого клиента. Оба должны очищать массив IceCandidates, когда любой из них срабатывает. Может быть, если кто-то хочет, вы можете объединить обе функции в 1 функцию, поскольку код тот же.

Окончательный рабочий код выглядит так

var ws;

var peerConnection, localStream;  
//STUN = (Session Traversal Utilities for NAT)  
var rtc_server = {
  iceServers: [
                {urls: "stun:stun.l.google.com:19302"},
                {urls: "stun:stun.services.mozilla.com"},
                {urls: "stun:stun.stunprotocol.org:3478"},
                {url: "stun:stun.l.google.com:19302"},
                {url: "stun:stun.services.mozilla.com"},
                {url: "stun:stun.stunprotocol.org:3478"},
  ]
}

//offer SDP = [Session Description Protocol] tells other peers what you would like
var rtc_media_constraints = {
  mandatory: {
    OfferToReceiveAudio: true,
    OfferToReceiveVideo: true
  }
};

var rtc_peer_options = {
  optional: [
              {DtlsSrtpKeyAgreement: true}, //To make Chrome and Firefox to interoperate.
  ]
}
var finishSDPVideoOffer = false;
var isOfferer = false;
var iceCandidates = [];
var PeerConnection = RTCPeerConnection || window.PeerConnection || window.webkitPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
var IceCandidate = RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate;
var SessionDescription = RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription;
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;

function hasSupportForVideoChat() {
   return window.RTCPeerConnection && window.RTCIceCandidate && window.RTCSessionDescription && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && (RTCPeerConnection.prototype.addStream || RTCPeerConnection.prototype.addTrack) ? true : false;
}

function loadMyCameraStream() {
    if (getUserMedia) {
      getUserMedia.call(navigator, { video: {facingMode: "user", aspectRatio: 4 / 3/*height: 272, width: 322*/}, audio: { echoCancellation : true } },
        function(localMediaStream) {
          //Add my video
          $("div#videoBox video#you")[0].muted = true;
          $("div#videoBox video#you")[0].autoplay = true;
          $("div#videoBox video#you").attr('playsinline', '');
          $("div#videoBox video#you").attr('webkit-playsinline', '');
          $("div#videoBox video#you")[0].srcObject = localMediaStream;
          localStream = localMediaStream;
        },
        function(e) {
          addStatusMsg("Your Video has error : " + e);
        }
      );
    } else {
      addStatusMsg("Your browser does not support WebRTC (Camera/Voice chat).");
      return;
    }
}

function loadStrangerCameraStream(isOfferer_) {
    if(!hasSupportForVideoChat())
      return;

    //Only add pending ICE Candidates when getOffer() is finished.
    finishSDPVideoOfferOrAnswer = false;
    iceCandidates = []; //clear ICE Candidates array.
    isOfferer = isOfferer_;

    peerConnection = new PeerConnection(rtc_server, rtc_peer_options);
    if (peerConnection.addTrack !== undefined)
      localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
    else
      peerConnection.addStream(localStream);

    peerConnection.onicecandidate = function(e) {
      if (!e || !e.candidate)
        return;    
      ws.send(JSON.stringify(['ice_candidate', {"candidate": e.candidate}]));
    };

    if (peerConnection.addTrack !== undefined) {
      //newer technology
      peerConnection.ontrack = function(e) {
        //e.streams.forEach(stream => doAddStream(stream));
        addStatusMsg("ontrack called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.streams[0];
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    } else {
      //older technology
      peerConnection.onaddstream = function(e) {
        addStatusMsg("onaddstream called");
        //Add stranger video
        $("div#videoBox video#stranger").attr('playsinline', '');
        $("div#videoBox video#stranger").attr('webkit-playsinline', '');
        $('div#videoBox video#stranger')[0].srcObject = e.stream;
        $("div#videoBox video#stranger")[0].autoplay = true;
      };
    }

    if(isOfferer) {
      peerConnection.createOffer(
        function(offer) {
          peerConnection.setLocalDescription(offer, function () {
            //both offer and peerConnection.localDescription are the same.
            addStatusMsg('createOffer, localDescription: ' + JSON.stringify(peerConnection.localDescription));
            //addStatusMsg('createOffer, offer: ' + JSON.stringify(offer));
            ws.send(JSON.stringify(['send_offer', {"offer": peerConnection.localDescription}]));
          },
          function(e) {
            addStatusMsg('createOffer, set description error' + e);
          });
        },
        function(e) {
          addStatusMsg("createOffer error: " + e);
        },
        rtc_media_constraints
      );
    }
}

function closeStrangerCameraStream() {
    $('div#videoBox video#stranger')[0].srcObject = null
    if(peerConnection)
      peerConnection.close();
}     

function iceCandidate(candidate) {
  //ICE = Interactive Connectivity Establishment
  if(!finishSDPVideoOfferOrAnswer) {
    iceCandidates.push(candidate);
    addStatusMsg("Queued iceCandidate");
    return;
  }

  if(!peerConnection) {
    addStatusMsg("iceCandidate peerConnection not created error.");
    return;
  }

  peerConnection.addIceCandidate(new IceCandidate(candidate));
  addStatusMsg("Added on time, Peer Ice Candidate = " + JSON.stringify(candidate));
}

function getAnswer(answer) {    
    if(!hasSupportForVideoChat())
      return;

    if(!peerConnection) {
      addStatusMsg("getAnswer peerConnection not created error.");
      return;
    }

    peerConnection.setRemoteDescription(new SessionDescription(answer), function() {
      addStatusMsg("getAnswer SessionDescription answer is ok");
      finishSDPVideoOfferOrAnswer = true;
      while (iceCandidates.length) {
        var candidate = iceCandidates.shift();
        try {
          peerConnection.addIceCandidate(new IceCandidate(candidate));
          addStatusMsg("Adding queued ICE Candidates");
        } catch(e) {
          addStatusMsg("Error adding queued ICE Candidates error:" + e);
        }
      }
      iceCandidates = [];
    },
    function(e) {
      addStatusMsg("getAnswer SessionDescription fail error: " + e);
    });
}

function getOffer(offer) {
    if(!hasSupportForVideoChat())
      return;

    if(!peerConnection) {
      addStatusMsg("getOffer peerConnection not created error.");
      return;
    }

    addStatusMsg("getOffer setRemoteDescription offer: " + JSON.stringify(offer));
    peerConnection.setRemoteDescription(new SessionDescription(offer), function() {
      finishSDPVideoOfferOrAnswer = true;
      while (iceCandidates.length) {
        var candidate = iceCandidates.shift();
        try {
          peerConnection.addIceCandidate(new IceCandidate(candidate));
          addStatusMsg("Adding queued ICE Candidates");
        } catch(e) {
          addStatusMsg("Error adding queued ICE Candidates error:" + e);
        }
      }
      iceCandidates = [];
      if(!isOfferer) {
        peerConnection.createAnswer(
          function(answer) {
            peerConnection.setLocalDescription(answer);
            addStatusMsg("getOffer create answer sent: " + JSON.stringify(answer));
            ws.send(JSON.stringify(['send_answer', {"answer": answer}]));
          },
          function(e) {
            addStatusMsg("getOffer setRemoteDescription create answer fail: " + e);
          }
        );
      }
    });
}

Вот патч, который я сделал на сервере WebSocket (Java) на стороне сервера.

//JSON
 //["connected", {videoChatOfferer: true}]
 //["connected", {videoChatOfferer: false}]
 JSONObject obj = new JSONObject();
 JSONArray list = new JSONArray();
 list.put("loadStrangerCameraStream");
 obj.put("videoChatOfferer", true); //first guy offerer for WebRTC.
 list.put(obj);
 server.sendMessage(websocket, list.toString()); //connected to chat partner
 obj.put("videoChatOfferer", false); //second guy isn't offerer.
 list.put(obj);
 server.sendMessage(stranger.getWebSocket(), list.toString()); //connected to chat partner
person SSpoke    schedule 12.03.2020
comment
случайный выбор не поможет, вы должны быть последовательны с обеих сторон. В групповом чате это означает, что либо человек, который присоединяется, создает предложение, либо человек, который был в комнате первым. - person Philipp Hancke; 12.03.2020
comment
Только когда я запускаю веб-сайт в первый раз, удаленный поток не показывает никаких ошибок, ничего ... но во второй раз, когда я снова подключаюсь, он работает нормально и больше никогда не дает сбоев. Я рандомизирую предлагающего, потому что они оба подключаются в в то же время camspark.com У меня есть еще одна проблема с моим Java WebSocket и SSL, он зависает, но я исправлю что, надеюсь, к завтрашнему дню. - person SSpoke; 12.03.2020
comment
Огромное спасибо @SSpoke за публикацию вашей реализации и исправления, я весь день боролся с ошибкой kStable. Из любопытства вы исправили тот факт, что удаленный поток не отображается для первого пользователя? Я построил аналогичную реализацию (используя SignalR в качестве сервера сигнализации) и, к сожалению, столкнулся с той же проблемой, которую вы описали в своем комментарии здесь. Как вы сказали, 2-й/3-й/и т. д. пользователь может видеть оба видео, но не первое. - person Corey Thompson; 30.03.2020
comment
к сожалению, я не смог это исправить и просто забросил проект, потому что он хорош как есть, лол, поскольку у его возможных пользователей нет камер, ха-ха. Я добавил временное исправление, которое действительно работает. Возможно, это просто теория, так как я не буду ее проверять, но что @PhilippHancke упомянул, что вы должны знать, кто делает предложение, связанное с потоками с ошибками? кто знает.. но я добавил команду чата /forcevid, которая переключает офертера с текущего оферента на противоположного пользователя, а затем вызывает функцию reloadCameraStream(), которая просто инициирует loadMyCameraStream() и loadStrangerCameraStream() снова - person SSpoke; 31.03.2020
comment
P.S.› если вы посмотрите на WebRTC Omegle, здесь я сделал скриншот i.imgur.com/TVsy0B7.png он использует setTimeout() для ga(), который является setLocalDescription() с обратным вызовом для sendPeerDescription(), который используется для CreateAnswer() и CreateOffer(). Думаю, он объединил обе функции в одну. Бьюсь об заклад, setTimeout() должен исправить сбой, когда поток не загружается, у него была такая же проблема, и вот как он ее решил. Я понятия не имею, как setTimeout с интервалом 0 что-то делает, поскольку он выполняется мгновенно. Источник: omegle .com/static/omegle.js - person SSpoke; 31.03.2020
comment
Спасибо! Я закончил тем, что заработал, ха-ха. У меня есть 2-й человек, который всегда делает предложение (сервер сигнализации ведет учет людей в комнате, поэтому, когда 2-й человек присоединяется, ответ звонящему должен сделать предложение другому человеку) - и это, кажется, работает :) К сведению всех, кто наткнулся на это - мне пришлось немного переписать, чтобы заставить его работать в Safari на iOS / Mac, но в остальном он работал почти из коробки для настольного и мобильного Chrome. - person Corey Thompson; 01.04.2020