Javascript WebRTC 无法设置远程应答 sdp:在错误状态下调用:kHaveRemoteOffer 并在错误状态下调用:kStable

Javascript WebRTC Failed to set remote answer sdp: Called in wrong state: kHaveRemoteOffer and Called in wrong state: kStable

我无法让我的 WebRTC 代码正常工作。我相信我做的一切都是正确的,但它仍然无法正常工作。为什么这么早就调用 ontrack 有点奇怪,也许它应该是这样的。

该网站使用 javascript 代码,我没有使用服务器代码 post 但那是 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;

这是我的输出。
原始文本: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);
          }
        );
      });
    }
}

我使用它的网站:https://www.camspark.com/
修复了我自己,我发现这段代码有 2 个问题。

第一个问题是 createOffer() 只能由一个人发送,不能由两个人发送。您必须随机选择执行 createOffer() 的人。

第二个问题是 ICE 候选人你必须为双方创建一个 queue/array,它包含所有传入的 ice_candidates。仅当收到对 createOffer() 的响应并设置来自 createOffer() 响应的 setRemoteDescription 时才执行 peerConnection.addIceCandidate(new IceCandidate(candidate));

getAnswer() 和 getOffer() 使用完全相同的代码,但一个是为一个客户接收的,另一个是为另一个客户接收的。当其中一个被触发时,两者都需要刷新 IceCandidates 数组。也许如果有人愿意,你可以将两个函数组合成一个函数,因为代码是相同的。

最终的工作代码如下所示

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