title: WebRTC Error: Failed to create remote session description. Called in wrong state
body: <p>I am trying to implement a real time audio/video group calls, but for now I want to get it for only two participants.</p>
  
  <p>It is not working and I don't understand why :( (actually when I test it with myself with two different accounts at the same time, I see some error messages but it works anyway, but when I test it with a friend in real different networks, the same error messages appear but in this case we can not hear or see each other).</p>
  
  <p>I am using Chrome 53 on Linux (Ubuntu 16.04).</p>
  
  <p>The error messages are the following, for the peer who sends the offer there are 6 errors in the browser console.
  1st:</p>
  
  <pre><code>Failed to create remote session description: OperationError: Failed to set remote offer sdp: Called in wrong state: STATE_SENTOFFER
  </code></pre>
  
  <p>2nd, 3rd, 4th, and 5th:</p>
  
  <pre><code>addIceCandidate error: OperationError: Error processing ICE candidate
  </code></pre>
  
  <p>6th: </p>
  
  <pre><code>Failed to set local session description: OperationError: CreateAnswer failed because remote_description is not an offer
  </code></pre>
  
  <p>And for the peer who receives the offer and sends and answer there is 1 error in the browser console:</p>
  
  <pre><code>Failed to create remote session description: OperationError: Failed to set remote answer sdp: Called in wrong state: STATE_INPROGRESS
  </code></pre>
  
  <p>In case you want to see all the messages in the console, the ones in the peer who sends the offer are here: <a href="http://pastebin.com/b3PafwbU" rel="nofollow">WebRTC error from offer peer</a>. And the ones in the other peer browser console are here: <a href="http://pastebin.com/7ZRyTqrs" rel="nofollow">WebRTC error from answer peer</a>. </p>
  
  <p>The code that matters in the the HTML file is the following (there are other files with Javascript code that I will show later):</p>
  
  <pre><code>&lt;div class='row'&gt;
    &lt;div class='col-xs'&gt;
      &lt;div class='box center-xs middle xs'&gt;
        &lt;h1&gt;Call CallNameExample&lt;/h1&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class='row'&gt;
    &lt;div class='col-xs'&gt;
      &lt;div class='box center-content'&gt;
        &lt;button class='btn btn-info btn-37 no-padding circle' id='btnChangeCamStatus'&gt;
          &lt;i class='material-icons' id='iconCamOff'&gt;
            videocam_off
          &lt;/i&gt;
          &lt;i class='material-icons hidden' id='iconCamOn'&gt;
            videocam
          &lt;/i&gt;
        &lt;/button&gt;
        &lt;button class='btn btn-info btn-37 no-padding circle' id='btnChangeMicStatus'&gt;
          &lt;i aria-hidden='true' class='fa fa-microphone-slash' id='iconMicOff'&gt;&lt;/i&gt;
          &lt;i aria-hidden='true' class='fa fa-microphone hidden' id='iconMicOn'&gt;&lt;/i&gt;
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class='row'&gt;
    &lt;div class='col-xs'&gt;
      &lt;div class='box center-xs middle xs'&gt;
        &lt;video autoplay height='200px' id='bigRemoteVideo' width='200px'&gt;&lt;/video&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;script&gt;
    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)
  
    }())
  &lt;/script&gt;
  </code></pre>
  
  <p>The other Javascript files contain the next code (all the files together here):</p>
  
  <pre><code>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 &amp;&amp; localStream) {
      pc.addStream(localStream)
      console.log("Added localStream to pc")
      if (justJoinedRoom) {
        console.log("call to DOCALL() from attachLocalMedia()")
        doCall()
      }
    }
  }
  </code></pre>
  
  <p>And finally the code related to the signaling. But first I want to clarify I am doing this website with Rails 5 and the signaling with WebSockets via ActionCable, so the CoffeeScript file (the client side) for the channel is this one:</p>
  
  <pre><code>window.createOrJoin = (roomID) -&gt;
    App.call = App.cable.subscriptions.create { channel: "CallChannel", room: roomID },
      connected: -&gt;
        # Called when the subscription is ready for use on the server
        createPeerConnection()
  
      disconnected: -&gt;
        # Called when the subscription has been terminated by the server
  
      received: (data) -&gt;
        # 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) -&gt;
        console.log("Client sending message: " + JSON.stringify(data));
        @perform "message", {message: data, room: roomID}
  </code></pre>
  
  <p>And the Ruby one (the server side):</p>
  
  <pre><code>class CallChannel &lt; 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"] ] &amp;&amp; @@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 ] &amp;&amp; @@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 ] &lt;&lt; current_user.id
          @@hashRoomsByUser[ current_user.id ] ||= Set.new()
          @@hashRoomsByUser[ current_user.id ] &lt;&lt; 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() &lt; Call.where(:id =&gt; 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 ] &lt;&lt; current_user.id
          @@hashRoomsByUser[ current_user.id ] ||= Set.new()
          @@hashRoomsByUser[ current_user.id ] &lt;&lt; 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
  </code></pre>
  
  <p>Searching on the Internet I saw people with similar problems but each one was for a different reason and none of them was useful for my situation, but I saw somewhere that "STATE_INPROGRESS" means "Offer/answer exchange completed" so from that I can't understand if the offer/answer exchange was completed... why it is not working when I try to use it with a friend? and why is it trying to set more remote session descriptions in that case (when the offer/answer exchange is supposed to be completed)?
  So basically my main question is: what is going on and how can I solve it?</p>
  
  <p>If you reached this part of the question, thank you, I appreciate it! :)</p>