0

I am trying to implement a real time audio/video group calls, but for now I want to get it for only two participants.

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).

I am using Chrome 53 on Linux (Ubuntu 16.04).

The error messages are the following, for the peer who sends the offer there are 6 errors in the browser console. 1st:

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

2nd, 3rd, 4th, and 5th:

addIceCandidate error: OperationError: Error processing ICE candidate

6th:

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

And for the peer who receives the offer and sends and answer there is 1 error in the browser console:

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

In case you want to see all the messages in the console, the ones in the peer who sends the offer are here: WebRTC error from offer peer. And the ones in the other peer browser console are here: WebRTC error from answer peer.

The code that matters in the the HTML file is the following (there are other files with Javascript code that I will show later):

<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>

The other Javascript files contain the next code (all the files together here):

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()
    }
  }
}

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:

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}

And the Ruby one (the server side):

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

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?

If you reached this part of the question, thank you, I appreciate it! :)

Marta
  • 25
  • 2
  • 9

1 Answers1

0

If you want to do multiparty you need one peerconnection for each pair of participants. You're currently using a single one.

See this example from the official WebRTC samples.

Philipp Hancke
  • 15,855
  • 2
  • 23
  • 31
  • Thanks for answering :) Yes, I've seen that in a book already, but as I said before for now I just want it for 2 participants and the error is testing it with only 2. In the future I will make it for more people and that's why you can see in the signaling server that is prepared for more people but not in the client side yet because if I didn't get it to make it work for 2 yet, it is not the moment to add more complication. – Marta Sep 25 '16 at 18:52
  • In case you want to test it, this is the [website](https://lanformon.herokuapp.com/) where you can join any of those calls and try it for 2 people (with different browsers or with the same one but one window in incognito so you can use 2 accounts). I've just created two users for SO, with email "stackoverflowuser01@email.com" and "stackoverflowuser02@email.com", both with password: "password" – Marta Sep 25 '16 at 18:59
  • I think that it doesn't matter if you want 2 users or 5. You need one peerConnection for each one. – Samuel Méndez Sep 26 '16 at 06:49
  • Each user needs 1 peerConnection to connect with another user, so if a user wants to connect with another 4 users (then in total in the call they are 5 people), each user needs 4 peerConnections, each peerConnection to connect with another user, but if he wants to connect with only 1 user he needs 1 peerConnection (and there are in total 2 users, the other person and himself), and the other person also needs only one to connect with the first one. You can see what I am saying [here](https://github.com/googlecodelabs/webrtc-web/tree/master/step-05) and [here](http://webrtcbook.com/code.html) – Marta Sep 26 '16 at 07:49