Webrtc远程视频流无法工作

5
在设置remoteDescription时,我在Firefox中遇到以下错误:
DOMException [InvalidStateError:“无法在状态have-local-offer中设置远程提供” 代码:11 nsresult:0x8053000b 位置:http://localhost:8080/resources/assets/js/test-online.js:111] 请查看下面的test-online.js代码:
var localVideo;
var remoteVideo;
var peerConnection;
var serverConnection;
var peerConnectionConfig = {'iceServers': [{'url': 'stun:stun.services.mozilla.com'}, {'url': 'stun:stun.l.google.com:19302'}]};
pageReady();
var offerOptions = {
          offerToReceiveAudio: 1,
          offerToReceiveVideo: 1
        };

var sdpConstraints = {'mandatory': {
      'OfferToReceiveAudio':true,
      'OfferToReceiveVideo':true }};

function pageReady() {
    localVideo = document.getElementById('localVideo');
    remoteVideo = document.getElementById('remoteVideo');

    localVideo.addEventListener('loadedmetadata', function() {
          trace('Local video videoWidth: ' + this.videoWidth +
            'px,  videoHeight: ' + this.videoHeight + 'px');
    });

    remoteVideo.addEventListener('loadedmetadata', function() {
          trace('Remote video videoWidth: ' + this.videoWidth +
            'px,  videoHeight: ' + this.videoHeight + 'px');
    });

    remoteVideo.onresize = function() {
          trace('Remote video size changed to ' +
            remoteVideo.videoWidth + 'x' + remoteVideo.videoHeight);
          // We'll use the first onsize callback as an indication that video has started
          // playing out.
          if (startTime) {
            var elapsedTime = window.performance.now() - startTime;
            trace('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
            startTime = null;
          }
    };

    serverConnection = new SockJS("/onlineHandler");

    serverConnection.onopen = function() {
        console.log("Opening server connection");
     };
     serverConnection.onmessage = gotMessageFromServer;
     serverConnection.onclose = function() {
         console.log("Closing server connection");
     };
   //serverConnection.onmessage = gotMessageFromServer;

    var constraints = {
        video: true,
        audio: true,
    };

    navigator.mediaDevices.getUserMedia(constraints)
      .then(getUserMediaSuccess)
      .catch(function(e) {
        alert('getUserMedia() error: ' + e.name);
      });
}

function getUserMediaSuccess(stream) {
      trace('Received local stream');
      localVideo.srcObject = stream;
      localStream = stream;
}

function start(isCaller) {
    trace('Starting call');
      startTime = window.performance.now();
      var videoTracks = localStream.getVideoTracks();
      var audioTracks = localStream.getAudioTracks();
      if (videoTracks.length > 0) {
        trace('Using video device: ' + videoTracks[0].label);
      }
      if (audioTracks.length > 0) {
        trace('Using audio device: ' + audioTracks[0].label);
      }

    peerConnection = new RTCPeerConnection(peerConnectionConfig);
    peerConnection.onicecandidate = gotIceCandidate;
    peerConnection.oniceconnectionstatechange = onIceStateChange;
    peerConnection.onaddStream = gotRemoteStream;
    peerConnection.addStream(localStream);

    if(isCaller) {
        peerConnection.createOffer(gotDescription, errorHandler , offerOptions);
    }
}

function gotMessageFromServer(message) {
   /* if(!peerConnection) start(false);

    var signal = JSON.parse(message.data);
//    console.log("Got Message from server :" + message.data);
    if(signal.sdp) {;
        console.log("hi in sdp" + message.data);
        peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp), function() {
            console.log("Creating answer :");
        if (peerConnection.remoteDescription.type == 'offer')
            peerConnection.createAnswer(gotDescription, errorHandler);
        }, errorHandler);
    } else if(signal.ice) {
        peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice));
    }*/
    var signal = JSON.parse(message.data);
    if (signal.type === 'offer') {
        peerConnection.setRemoteDescription(new RTCSessionDescription(signal),doAnswer,errorHandler);
      } else if (signal.type === 'answer') {
          peerConnection.setRemoteDescription(new RTCSessionDescription(signal),doNothing, errorHandler);
      } else if (signal.type === 'candidate') {
        var candidate = new RTCIceCandidate({
          sdpMLineIndex:signal.label,
          candidate: signal.candidate
        });
        peerConnection.addIceCandidate(candidate);
      } else if (signal === 'bye' && isStarted) {
        handleRemoteHangup();
      }
}
function doNothing(){

}

function doAnswer() {
      console.log('Sending answer to peer.');
      peerConnection.createAnswer(gotDescription, errorHandler, sdpConstraints);
    }

function handleRemoteHangup() {
//  console.log('Session terminated.');
  // stop();
  // isInitiator = false;
}

function gotIceCandidate(event) {
    if(event.candidate != null) {

        var message ={
                  type: 'candidate',
                  label: event.candidate.sdpMLineIndex,
                  id: event.candidate.sdpMid,
                  candidate: event.candidate.candidate};

       // serverConnection.send(JSON.stringify({'ice': event.candidate}));
         serverConnection.send(JSON.stringify(message));
    }
}

function onIceStateChange(event) {
      if (peerConnection) {
        trace(' ICE state: ' + peerConnection.iceConnectionState);
        console.log('ICE state change event: ', event);
      }
    }

function gotDescription(description) {
   // trace('Offer from peerConnection\n' + description.sdp);
      description.sdp = preferOpus(description.sdp);
     // pc.setLocalDescription(description);
      console.log('setLocalAndSendMessage sending message' , description);
   // trace('peerConnection setLocalDescription start');
    peerConnection.setLocalDescription(
            description, 
            function () {
                serverConnection.send(JSON.stringify(description));
            }, 
            onSetSessionDescriptionError
    );
}

function preferOpus(sdp) {
      var sdpLines = sdp.split('\r\n');
      var mLineIndex;
      // Search for m line.
      for (var i = 0; i < sdpLines.length; i++) {
          if (sdpLines[i].search('m=audio') !== -1) {
            mLineIndex = i;
            break;
          }
      }
      if (mLineIndex === null) {
        return sdp;
      }

      // If Opus is available, set it as the default in m line.
      for (i = 0; i < sdpLines.length; i++) {
        if (sdpLines[i].search('opus/48000') !== -1) {
          var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
          if (opusPayload) {
            sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload);
          }
          break;
        }
      }

      // Remove CN in m line and sdp.
      sdpLines = removeCN(sdpLines, mLineIndex);

      sdp = sdpLines.join('\r\n');
      return sdp;
    }

    function extractSdp(sdpLine, pattern) {
      var result = sdpLine.match(pattern);
      return result && result.length === 2 ? result[1] : null;
    }

    // Set the selected codec to the first in m line.
    function setDefaultCodec(mLine, payload) {
      var elements = mLine.split(' ');
      var newLine = [];
      var index = 0;
      for (var i = 0; i < elements.length; i++) {
        if (index === 3) { // Format of media starts from the fourth.
          newLine[index++] = payload; // Put target payload to the first.
        }
        if (elements[i] !== payload) {
          newLine[index++] = elements[i];
        }
      }
      return newLine.join(' ');
    }

    // Strip CN from sdp before CN constraints is ready.
    function removeCN(sdpLines, mLineIndex) {
      var mLineElements = sdpLines[mLineIndex].split(' ');
      // Scan from end for the convenience of removing an item.
      for (var i = sdpLines.length-1; i >= 0; i--) {
        var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
        if (payload) {
          var cnPos = mLineElements.indexOf(payload);
          if (cnPos !== -1) {
            // Remove CN payload from m line.
            mLineElements.splice(cnPos, 1);
          }
          // Remove CN line in sdp
          sdpLines.splice(i, 1);
        }
      }

      sdpLines[mLineIndex] = mLineElements.join(' ');
      return sdpLines;
    }


function onSetSessionDescriptionError(error) {
  trace('Failed to set session description: ' + error.toString());
}

function gotRemoteStream(event) {
     remoteVideo.srcObject = event.stream;
     trace('Received remote stream');
}

function errorHandler(error) {
    console.log(error);
}

以下是我的HTML代码:

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
    <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<html lang="en">
<head>
<link
    href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
    rel="stylesheet">
    <!-- Meta tag to not followed by search engine. -->
    <meta name="robots" content="noindex,nofollow,nosnippet,noodp,noarchive">
    <meta name="keywords" content="JavaScript, WebRTC" />
    <meta name="description" content="WebRTC codelab" />
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1">

    <style>
        video {
            filter: hue-rotate(180deg) saturate(200%);
            -moz-filter:  hue-rotate(180deg) saturate(200%);
            -webkit-filter: hue-rotate(180deg) saturate(200%); 
            /*-webkit-filter: grayscale(0.3) hue-rotate(360deg) saturate(10) opacity(0.7) sepia(0.5); */
        }
    </style>
</head>
<body>


    <div id='videos'>
                <video id='localVideo' autoplay muted></video>
                <video id='remoteVideo' autoplay></video>
            </div>

       <input type="button" id="start" onclick="start(true)" value="Start Video"></input>
    <script src="//cdn.jsdelivr.net/sockjs/1.0.0/sockjs.min.js"></script>
     <script
        src="${pageContext.request.contextPath}/resources/assets/js/jquery-2.1.1.min.js"></script>
     <script
        src="${pageContext.request.contextPath}/resources/assets/js/bootstrap.min.js"></script>
     <script src ="${pageContext.request.contextPath}/resources/assets/js/adapter-0.2.10.js"></script>
     <script src="${pageContext.request.contextPath}/resources/assets/js/test-online.js"></script>
</body>
</html>

我不太明白我在这里做错了什么。我在webrtc领域还是个新手,只想在我的代码中运行这个基本的东西。


@NallaSrinivas 我的 Firefox 版本是 46.0a2 (2016-02-12),我非常确定 webrtc 支持在早期版本的 Firefox 中就已经开始了。你有没有机会查看上面的代码?你是否发现了代码中的不寻常之处? - Varun
你已经将start(false)的调用注释掉了,所以我不明白接收方如何创建点对点连接,除非你的信令服务器在传递消息,否则我看不出它能够进展到足以产生该错误的地步。另外,第111行是哪一行?当我数行时,代码示例似乎有些不对。 - jib
但是接收方在接收电话之前也必须按开始键吗?这听起来不太对。两端都需要一个对等连接对象(特别是,被注释掉的代码似乎很重要)。 - jib
由于接收端没有对等连接的迹象,我推测你正在调用带有自己offer的发送端peerConnection上的setRemoteDescription方法,这符合你收到的“状态错误”,因为此时该peerConnection已经有一个相同的offer。这表明你的信令服务器可能在将消息弹回消息的发送方,而这并不是你想要的。另外,当你说“所有客户端”时,请注意WebRTC不是一对多的,连接中需要恰好有另一个对等点。 - jib
感谢@jib,是的,你说得对,我需要取消注释peerConnection代码,并且我的服务器端似乎有一个bug,我已经修复了,现在它可以工作了。虽然,现在我已经更改了这段代码,并开始使用此处提到的示例代码[链接](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling)。而且问题出在我点击两次发送按钮时,这不是我的问题。现在一切都开始正常工作 :) - Varun
显示剩余3条评论
1个回答

3
报价-应答交换是一个状态机,某些方法在特定状态下是不允许的。
调用setLocalDescription(或setRemoteDescription)将信令状态更改为"have-local-offer"(或"have-remote-offer")。
此时,应用程序的工作是将状态带回到stableclosed,如规范所述
例如,应用程序有责任处理闪烁(即双方同时发送报价的情况)。
Firefox中还存在一个错误,即一旦处于have-local-offer状态,就不允许再次调用createOffersetLocalDescription,反之亦然对于答案(在上面链接的规范中的状态图中的小跳跃)。但从您的错误消息中看起来,似乎并非如此。

感谢Jib提供更深入的见解,由于我在使用不同的浏览器(Chrome、Firefox Dev Edition、Firefox普通版)时遇到了许多困惑,因此在设置webrtc呼叫时出现了很多混淆。在webrtc中,Offer状态是一个相当令人困惑的问题。 - Varun
我建议使用 adapter.js 来消除规范上的差异,尽管在提供/答复方面,现在你提到的浏览器应该是一致的。如果我的回答有帮助,请不要忘记点赞。 :) - jib
谢谢Jib,是的,它帮助我认识到了我的错误 :) - Varun

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