【问题标题】:Javascript WebRTC Failed to set remote answer sdp: Called in wrong state: kHaveRemoteOffer and Called in wrong state: kStableJavascript WebRTC 无法设置远程应答 sdp:在错误状态下调用:kHaveRemoteOffer 并在错误状态下调用:kStable
【发布时间】:2020-03-11 11:26:09
【问题描述】:

我无法让我的 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;

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

【问题讨论】:

    标签: javascript html5-video webrtc getusermedia rtcpeerconnection


    【解决方案1】:

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

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

    第二个问题是 ICE 候选人,您必须为双方创建一个队列/数组,其中包含所有传入的 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
    

    【讨论】:

    • 随机采摘无济于事,您需要双方保持一致。在群聊中,这意味着要么是加入的人创建要约,要么是首先进入房间的人。
    • 只有当我第一次运行网站时,远程流没有显示任何错误。但在第二次我重新连接时它工作正常,再也不会出现故障。我随机化提供者,因为他们都同时连接camspark.com 我的 Java WebSocket 和 SSL 有另一个问题,它挂了,但我希望明天能解决这个问题。
    • 非常感谢@SSpoke 发布您的实施和修复,我整天都在为 kStable 错误苦苦挣扎。出于好奇,您是否最终解决了远程流不向第一个用户显示的事实?我已经构建了一个类似的实现(使用 SignalR 作为信令服务器),不幸的是遇到了您在此处的评论中描述的相同问题。正如你所说,第二/第三/等用户可以看到两个视频,但不能看到第一个。
    • 不幸的是,我无法解决这个问题,只是放弃了这个项目,因为它很好,哈哈,因为它可能的用户没有摄像头哈哈。我确实添加了一个有效的临时修复程序。可能只是一个理论,因为我不会对其进行测试,但@PhilippHancke 提到您应该知道提供者是谁与故障流有关?谁知道.. 但是我添加了一个聊天命令/forcevid,它将提议者从当前提议者切换到对面用户,然后它调用一个reloadCameraStream() 函数,它再次启动loadMyCameraStream()loadStrangerCameraStream() /跨度>
    • 谢谢!我最终让它工作了哈哈。我确实让第二个人始终是提议者(信令服务器记录房间里的人,所以当第二个人加入时,对呼叫者的响应是向另一个人提出提议) - 这似乎有效:) 仅供任何其他偶然发现此问题的人参考——我不得不重写一些代码才能让它在 iOS/Mac 上的 Safari 上运行——但除此之外,它几乎可以在 Chrome 桌面和移动设备上开箱即用。
    猜你喜欢
    • 2018-08-04
    • 1970-01-01
    • 1970-01-01
    • 2021-05-09
    • 2016-09-21
    • 1970-01-01
    • 2016-10-13
    • 1970-01-01
    • 2022-10-01
    相关资源
    最近更新 更多