WebRTC错误:无法创建远程会话描述。被称为错误状态

时间:2016-09-24 13:47:06

标签: javascript ruby-on-rails websocket webrtc actioncable

我正在尝试实施实时音频/视频群组通话,但目前我想只为两位参与者提供此功能。

它不起作用,我不明白为什么:((实际上,当我同时用两个不同的帐户测试它时,我看到一些错误消息但无论如何它仍然有用,但是当我测试它时与朋友在真实不同的网络中,会出现相同的错误消息但在这种情况下我们无法听到或看到对方)。

我在Linux上使用Chrome 53(Ubuntu 16.04)。

错误消息如下,对于发送优惠的对等方,浏览器控制台中有6个错误。 第一:

Failed to create remote session description: OperationError: Failed to set remote offer sdp: Called in wrong state: STATE_SENTOFFER

第二,第三,第四和第五:

addIceCandidate error: OperationError: Error processing ICE candidate

6th:

Failed to set local session description: OperationError: CreateAnswer failed because remote_description is not an offer

对于收到要约并发送和回答的对等方,浏览器控制台中有1个错误:

Failed to create remote session description: OperationError: Failed to set remote answer sdp: Called in wrong state: STATE_INPROGRESS

如果您想要查看控制台中的所有消息,发送优惠的对等方中的消息都在此处:WebRTC error from offer peer。其他对等浏览器控制台中的那些是:WebRTC error from answer peer

HTML文件中重要的代码如下(还有其他带有Javascript代码的文件,我将在稍后展示):

<div class='row'>
  <div class='col-xs'>
    <div class='box center-xs middle xs'>
      <h1>Call CallNameExample</h1>
    </div>
  </div>
</div>
<div class='row'>
  <div class='col-xs'>
    <div class='box center-content'>
      <button class='btn btn-info btn-37 no-padding circle' id='btnChangeCamStatus'>
        <i class='material-icons' id='iconCamOff'>
          videocam_off
        </i>
        <i class='material-icons hidden' id='iconCamOn'>
          videocam
        </i>
      </button>
      <button class='btn btn-info btn-37 no-padding circle' id='btnChangeMicStatus'>
        <i aria-hidden='true' class='fa fa-microphone-slash' id='iconMicOff'></i>
        <i aria-hidden='true' class='fa fa-microphone hidden' id='iconMicOn'></i>
      </button>
    </div>
  </div>
</div>
<div class='row'>
  <div class='col-xs'>
    <div class='box center-xs middle xs'>
      <video autoplay height='200px' id='bigRemoteVideo' width='200px'></video>
    </div>
  </div>
</div>
<script>
  var room = "1"
  var localVideo = document.getElementById("localVideo")
  var bigRemoteVideo = document.getElementById("bigRemoteVideo")

  document.getElementById("btnChangeCamStatus").addEventListener("click", function() {
    if (localStream.getVideoTracks()[0].enabled) {
      disableCam()
      $("#iconCamOff").addClass("hidden")
      $("#iconCamOn").removeClass("hidden")
    } else {
      enableCam()
      $("#iconCamOff").removeClass("hidden")
      $("#iconCamOn").addClass("hidden")
    }
  }, false);
  document.getElementById("btnChangeMicStatus").addEventListener("click", function() {
    if (localStream.getAudioTracks()[0].enabled) {
      disableMic()
      $("#iconMicOff").addClass("hidden")
      $("#iconMicOn").removeClass("hidden")
    } else {
      enableMic()
      $("#iconMicOff").removeClass("hidden")
      $("#iconMicOn").addClass("hidden")
    }
  }, false);

  function setLocalVideo(stream) {
    localVideo.src = window.URL.createObjectURL(stream)
  }

  function setRemoteVideo(stream) {
    bigRemoteVideo.src = window.URL.createObjectURL(stream)
  }

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

  bigRemoteVideo.addEventListener('loadedmetadata', function() {
    console.log('Remote video videoWidth: ' + this.videoWidth +
      'px,  videoHeight: ' + this.videoHeight + 'px');
  });

  // Starts the party:
  (function(){
    enableUserMedia()

    window.createOrJoin(room)
    console.log("Attempted to create or join room: " + room)

  }())
</script>

其他Javascript文件包含下一个代码(此处所有文件一起):

var localStream
var mediaConstraints = {video: true, audio: true}

function enableUserMedia(){
  console.log('Getting user media with constraints', mediaConstraints);
  navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia

  if (navigator.getUserMedia) {
    navigator.getUserMedia(mediaConstraints, gotStream, gotError)
  }

  window.URL = window.URL || window.webkitURL

  function gotStream(stream) {
    console.log('Adding local stream.');
    setLocalVideo(stream)
    localStream = stream;
    //sendMessage('got user media');
    console.log('got user media');
    attachLocalMedia();
  }
  function gotError(error) {
    console.log("navigator.getUserMedia error: ", error);
  }
}

function disableCam(){
  localStream.getVideoTracks()[0].enabled = false
}

function disableMic(){
  localStream.getAudioTracks()[0].enabled = false
}

function enableCam(){
  localStream.getVideoTracks()[0].enabled = true
}

function enableMic(){
  localStream.getAudioTracks()[0].enabled = true
}

function disableUserMedia(){
  localStream.getVideoTracks()[0].stop();
  localStream.getAudioTracks()[0].stop();
}

window.onbeforeunload = function() {
  sendMessage("bye");
};

function hangup() {
  console.log("Hanging up.");
  stop();
  sendMessage("bye");
}

function handleRemoteHangup() {
  console.log("Session terminated.");
  stop();
}

function stop() {
  disableUserMedia();
  pc.close();
  console.log("PC STATE: " + pc.signalingState || pc.readyState);
  console.log("PC ICE STATE: " + pc.iceConnectionState)
  pc = null;
}

var isInitiator = false
var justJoinedRoom = false

var sdpConstraints = { // Set up audio and video regardless of what devices are present.
  'mandatory': {
    'OfferToReceiveAudio': true,
    'OfferToReceiveVideo': true
  }
}

function sendMessage(message){
  App.call.message(message);
}

function doCall() {
  console.log("Sending offer to peer");
  pc.createOffer(sdpConstraints)
    .then(setLocalAndSendMessage)
    .catch(handleCreateOfferError);
  //pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}

function doAnswer() {
  console.log("Sending answer to peer.");
  pc.createAnswer()
    .then(setLocalAndSendMessage)
    .catch(onSetLocalSessionDescriptionError);
}

function setLocalAndSendMessage(sessionDescription) {
  console.log("setLocalAndSendMessage sending message" + JSON.stringify(sessionDescription));
  pc.setLocalDescription(sessionDescription)
    .then(
      function(){
        onSetLocalSuccess();
        sendMessage(sessionDescription);
      }
    )
    .catch(onSetLocalSessionDescriptionError);
}

function onSetLocalSuccess() {
  console.log('setLocalDescription complete');
}

function onSetRemoteSuccess() {
  console.log('setRemoteDescription complete');
  doAnswer();
}

function onSetLocalSessionDescriptionError(error) {
  console.error('Failed to set local session description: ' + error.toString())
}

function handleCreateOfferError(event) {
  console.error("createOffer() error: " + JSON.stringify(event))
}

function onSetRemoteSessionDescriptionError(error) {
  console.error("Failed to create remote session description: " + error.toString())
}

function handleReceivedOffer(message) {
  console.log("handleReceivedOffer: " + JSON.stringify(message));
  pc.setRemoteDescription(new RTCSessionDescription(message))
    .then(onSetRemoteSuccess)
    .catch(onSetRemoteSessionDescriptionError)
}
function handleReceivedAnswer(message) {
  console.log("handleReceivedAnswer: " + JSON.stringify(message));
  pc.setRemoteDescription(new RTCSessionDescription(message))
    .then(onSetRemoteSuccess)
    .catch(onSetRemoteSessionDescriptionError)
}
function handleReceivedCandidate(label, candidate) {
  pc.addIceCandidate(
    new RTCIceCandidate({
      sdpMLineIndex: label,
      candidate: candidate
    })
  ).then(successAddingIceCandidate).catch(errorAddingIceCandidate)
}

function successAddingIceCandidate() { console.log("addIceCandidate successfully") }
function errorAddingIceCandidate(error) { console.error("addIceCandidate error: " +  error.toString()) }

var remoteStream
var pc
var pcConfig = {
  'iceServers': [{
    'url': 'stun:stun.l.google.com:19302'
  }, {
    'url': 'turn:192.158.29.39:3478?transport=udp',
    'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
    'username': '28224511:1379330808'
  }]
}

function connectionStateCallback(){
  var state;
  if (pc) {
    state = pc.connectionState
    console.log("PC CONNECTION state change callback, state: " + state)
  }
}

function signalingStateCallback() {
  var state;
  if (pc) {
    state = pc.signalingState || pc.readyState;
    console.log("PC SIGNALING state change callback, state: " + state);
  }
}
function iceStateCallback() {
  var iceState;
  if (pc) {
    iceState = pc.iceConnectionState;
    console.log('PC ICE connection state change callback, state: ' + iceState);
  }
}

function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(pcConfig);
    signalingStateCallback();
    pc.onsignalingstatechange = signalingStateCallback;
    console.log("PC ICE STATE: " + pc.iceConnectionState);
    pc.oniceconnectionstatechange = iceStateCallback;
    pc.onconnectionstatechange = connectionStateCallback;
    pc.onicecandidate = handleIceCandidate;
    pc.onaddstream = handleRemoteStreamAdded;
    pc.onremovestream = handleRemoteStreamRemoved;
    console.log('Created RTCPeerConnnection');
    attachLocalMedia();
  } catch (e) {
    console.error("Failed to create PeerConnection, exception: " + e.toString())
    return;
  }
}

function handleIceCandidate(event) {
  console.log("icecandidate event: " + JSON.stringify(event));
  if (event.candidate) {
    sendMessage({
      type: "candidate",
      label: event.candidate.sdpMLineIndex,
      id: event.candidate.sdpMid,
      candidate: event.candidate.candidate
    });
  } else {
    console.log("End of candidates.");
  }
}

function handleRemoteStreamAdded(event) {
  console.log("Remote stream added.");
  setRemoteVideo(event.stream);
  remoteStream = event.stream;
}

function handleRemoteStreamRemoved(event) { //In real life something should be done here but since the point of this website is to learn, this function is not a priority right now.
  console.log("Remote stream removed. Event: " + event);
}

function attachLocalMedia() {
  if (pc && localStream) {
    pc.addStream(localStream)
    console.log("Added localStream to pc")
    if (justJoinedRoom) {
      console.log("call to DOCALL() from attachLocalMedia()")
      doCall()
    }
  }
}

最后代码与信令有关。但首先我要澄清我正在使用Rails 5进行此网站以及通过ActionCable通过WebSockets发送信号,因此该频道的CoffeeScript文件(客户端)就是这个:

window.createOrJoin = (roomID) ->
  App.call = App.cable.subscriptions.create { channel: "CallChannel", room: roomID },
    connected: ->
      # Called when the subscription is ready for use on the server
      createPeerConnection()

    disconnected: ->
      # Called when the subscription has been terminated by the server

    received: (data) ->
      # Called when there's incoming data on the websocket for this channel
      if (data["kindOfData"] == "created")
        console.log('Created room ' +  data["room"])
        window.isInitiator = true # ESTO ME SIRVE SOLO PARA 2 PERSONAS!! # CREO QUE YA NI LO USO
        attachLocalMedia()
      else if (data["kindOfData"] == "full")
        console.log('Room ' + data["room"] + ' is full')
      else if (data["kindOfData"] == "join")
        console.log('Another peer made a request to join room ' + data["room"])
        console.log('This peer is the initiator of room ' + data["room"] + '!')
        window.justJoinedRoom = false
      else if (data["kindOfData"] == "joined")
        console.log('joined: ' + data["room"])
        window.justJoinedRoom = true
        attachLocalMedia()
      else if (data["kindOfData"] == "log")
        console.log(data["info"])
      else if (data["kindOfData"] == "message") # This client receives a message
        console.log("Client received message: " + JSON.stringify(data["message"]));
        if (data["message"] == "bye")
          handleRemoteHangup()
        else if (data["message"]["type"] == "offer")
          handleReceivedOffer(data["message"]) # obj with "type" and "sdp"
        else if (data["message"]["type"] == "answer")
          handleReceivedAnswer(data["message"]) # obj with "type" and "sdp"
        else if (data["message"]["type"] == "candidate")
          handleReceivedCandidate(data["message"]["label"], data["message"]["candidate"])


    message: (data) ->
      console.log("Client sending message: " + JSON.stringify(data));
      @perform "message", {message: data, room: roomID}

Ruby one(服务器端):

class CallChannel < ApplicationCable::Channel
  def subscribed # Action automatically called when a client is subscribed to the channel
    stream_from "calls" # calls is a channel in common for everyone # ONLY FOR TESTING!!!
    stream_from "calls_room#{params[:room]}_person#{current_user.id}"
    @@hashUsersByRoom ||= Hash.new() # { |h,k| h[k] = Set.new }
    @@hashRoomsByUser ||= Hash.new() # { |h,k| h[k] = Set.new }
    result = createOrJoin(params[:room])
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def message(data)
    if data["message"].eql? "bye"
      if @@hashUsersByRoom[ data["room"] ] && @@hashUsersByRoom[ data["room"] ].include?( current_user.id )
        @@hashUsersByRoom[ data["room"] ].delete( current_user.id )
        if @@hashUsersByRoom[ data["room"] ].length() == 0
          @@hashUsersByRoom.delete( data["room"] )
          Call.find( data["room"] ).update_column("active", false)
        end
      end
      if @@hashRoomsByUser[ current_user.id ] && @@hashRoomsByUser[ current_user.id ].include?( data["room"] )
        @@hashRoomsByUser[ current_user.id ].delete( data["room"] )
        if @@hashRoomsByUser[ current_user.id ].length() == 0
          @@hashRoomsByUser.delete( current_user.id )
        end
      end
    end
    ActionCable.server.broadcast "calls_room#{data["room"]}", kindOfData: "log", info: "Client #{current_user.id} said: #{data["message"]}"
    ActionCable.server.broadcast "calls_room#{data["room"]}", kindOfData: "message", message: data["message"]
  end

  private

    def createOrJoin(room)
      ActionCable.server.broadcast "calls", kindOfData: "log", info: "Received request to create or join room #{room}"
      @@hashUsersByRoom[room] ||= Set.new()
      ActionCable.server.broadcast "calls", kindOfData: "log", info: "Room #{room} now has #{@@hashUsersByRoom[room].length()} + client(s)"
      if @@hashUsersByRoom[room].length == 0
        stream_from "calls_room#{room}" # Join the room
        @@hashUsersByRoom[ room ] << current_user.id
        @@hashRoomsByUser[ current_user.id ] ||= Set.new()
        @@hashRoomsByUser[ current_user.id ] << room
        ActionCable.server.broadcast "calls", kindOfData: "log", info: "Client ID #{current_user.id} created room #{room}"
        ActionCable.server.broadcast "calls_room#{room}_person#{current_user.id}", kindOfData: "created", room: room, user: current_user.id
        Call.find(room).update_column("active", true)
      elsif ( @@hashUsersByRoom[room].length() < Call.where(:id => room).pluck(:maximumNumberOfParticipants)[0] ) || ( @@hashUsersByRoom[ data["room"] ].include?( current_user.id ) )
        ActionCable.server.broadcast "calls", kindOfData: "log", info: "Client ID #{current_user.id} joined room #{room}"
        ActionCable.server.broadcast "calls_room#{room}", kindOfData: "join", room: room
        stream_from "calls_room#{room}" # Join the room
        @@hashUsersByRoom[ room ] << current_user.id
        @@hashRoomsByUser[ current_user.id ] ||= Set.new()
        @@hashRoomsByUser[ current_user.id ] << room
        ActionCable.server.broadcast "calls_room#{room}_person#{current_user.id}", kindOfData: "joined", room: room, user: current_user.id
        ActionCable.server.broadcast "calls_room#{room}", kindOfData: "ready"
      else # full room
        ActionCable.server.broadcast "calls_room#{room}_person#{current_user.id}", kindOfData: "full", room: room
      end
    end

end

在互联网上搜索我看到有类似问题的人,但每个人出于不同的原因而且没有一个对我的情况有用,但我看到了某个地方&#34; STATE_INPROGRESS&#34;意味着&#34;提供/回答交换完成&#34;所以,我无法理解提供/答案交换是否完成...当我尝试与朋友一起使用时,为什么它不起作用?为什么在这种情况下尝试设置更多远程会话描述(当提供/应答交换应该完成时)? 所以基本上我的主要问题是:发生了什么以及如何解决?

如果你到达了问题的这一部分,谢谢你,我很感激! :)

1 个答案:

答案 0 :(得分:0)

如果你想做多方,你需要为每对参与者提供一个对等连接。你目前正在使用一个。

请参阅官方WebRTC样本中的this example