Andrew
Andrew

Reputation: 1

WEBRTC, establish connection if user A to user B. With different Video and Audio constraints

This code, is handling then new User connected.

User Class

class User{
  
  constructor(friendID){
    this.friendID = friendID;
    this.userName = null;
    this.myPeer = new RTCPeerConnection(servers);
    this.videoManager = new VideoManager();
    this.setup_events();
  }

  get name(){
    return this.userName;
  }

  get id(){
    return this.friendID;
  }

  setup_events(){ 

    let this_instance = this;

    this.myPeer.onicecandidate = function(event){ 
      if (event.candidate !== null){
        LOG("ICE CANDIDATE SEND TO " + this_instance.friendID);
        LOG_obj(event.candidate);
        socket.emit("ice-candidate", event.candidate, this_instance.friendID);
      }else{
        LOG("EMPTY CANDIDATE");
      }
    }
    this.myPeer.addEventListener('track', async(event) => {
      const [remoteStream] = event.streams;
      this_instance.videoManager.createVideo();
      this_instance.videoManager.setStream(remoteStream);
      LOG("ADDED stream to VideoObject");
    });
  }

  add_candidate(candidate){
    this.myPeer.addIceCandidate( new RTCIceCandidate(candidate) );
    LOG("CANDIDATE ADDED TO PEER");
  } 

  accept_offer(userOffer){
    this.myPeer.setRemoteDescription(new RTCSessionDescription(userOffer));
    LOG("ACCEPTED OFFER");
  }

  async create_offer(){

    MediaRules.get_MediaRules()
      .then( (mediaRules) => {

        navigator.mediaDevices.getUserMedia(mediaRules).then( (mediaStream) =>{
          let tracks = mediaStream.getTracks();

          tracks.forEach( track => { this.myPeer.addTrack(track, mediaStream); });

          LOG("ADDED ALL TRACKS");
        }).then( () => {
            this.myPeer.createOffer(mediaRules).then( (offerObj) => {
              
              this.myPeer.setLocalDescription(offerObj);
              socket.emit("user-offer", offerObj, this.friendID);
        });
      });
  });
  }

  accept_answer(userAnswer){
    this.myPeer.setRemoteDescription(new RTCSessionDescription(userAnswer));
    LOG("ACCEPTED ANSWER");
  }

  async create_answer(){

    MediaRules.get_MediaRules().then( (mediaRules) => {
      
      navigator.mediaDevices.getUserMedia(mediaRules).then( (mediaStream) => {
        let tracks = mediaStream.getTracks();
        tracks.forEach( track => { this.myPeer.addTrack(track, mediaStream); });
        LOG("ADDED ALL TRACKS");
      }).then( () => {
          this.myPeer.createAnswer(mediaRules).then( (answerObj) => {
            this.myPeer.setLocalDescription(answerObj); 
            socket.emit("user-answer", answerObj, this.friendID);
          });
        });
    });
  }
}

User Pool

class UsersPool{
  
  constructor(){
    this.UsersMap = {};
  }

  addUser(userObj){
    this.UsersMap[userObj.id] = userObj;
  }

  accept_IceCandidate(candidateObject, user_id){
    this.UsersMap[user_id].add_candidate(candidateObject);
  }

  accept_Offer(offerObject, user_id){
    LOG("ACCEPT OFFER FROM " + user_id);
    this.UsersMap[user_id].accept_offer(offerObject);
  }

  accept_Answer(answerObject, user_id){
    this.UsersMap[user_id].accept_answer(answerObject);
  }

  async CreateSendOffer(user_id){
    await this.UsersMap[user_id].create_offer();
  }

  async CreateSendAnswer(user_id){
    await this.UsersMap[user_id].create_answer();
  }
}

Media Constraints

class MediaConstraints{

  async get_MediaRules(){
    let mediaRules = { video: false, audio: false };

    let devicesEnum = await navigator.mediaDevices.enumerateDevices();

    devicesEnum.forEach( device => {
      if ( device.kind == "audioinput" ){
        mediaRules["audio"] = true;
      }
      else if ( device.kind == "videoinput"){
        mediaRules["video"] = true;
      }
    });
    return mediaRules;
  }
}

Video Manager (creates video element by user)

class VideoManager {

  constructor(){
    this.videoObject = null;
  }

  createVideo(){
    let videoObject = document.createElement("video");
    let divVideo = document.createElement("div");

    videoObject.setAttribute('width', "600");
    videoObject.setAttribute('height', "800");

    divVideo.appendChild(videoObject); 

    document.body.appendChild(divVideo);

    this.videoObject = videoObject;
  }

  setStream(stream){
    this.videoObject.srcObject = stream; 
    this.videoObject.play();
  }
}

Well, the problem is here. icecandidate is working nice, signaling server is working too. TURN/STUN server works fine.

My main question is how to create constraints and setup correctly Offer and Answer if User A don't have webcamera but User B has. At the moment i get error that STUN server is broken, but this is because peers can't finish establishing connection between each other.

How to make it, if i have only microphone on my Laptop, but on other Laptop i have video and microphone.

EDIT 0: Well, looks like WebRTC doesn't like if constraints are different, if User A create offer with {video: false, audio: true}, and send it to User B, and User B creates answer with {video: true, audio: true} then it fails to connect because constraints are different.

Still don't understand why this is a problem.

EDIT 1: Looks like the only way is to use addTransceiver and to control manually media.

Upvotes: 0

Views: 128

Answers (1)

Andrew
Andrew

Reputation: 1

Actually, the problem was not in Linux Firefox version. The Problem was in Offer-Answer exchange and using AddTrack function.

Problem was that, if User A doesn't have WebCamera but User B have, it can't finish normally connection, and as a result ice-candidate is not triggered.

So, i solved it using AddTransceiver().

An example how to make connection if someone doesn't have webacamera or microphone.

Create RTCPeerConnection.

let myPeer = new RTCPeerConnection();

Make a function that is looking for active devices. so in result you need to get

real_devices = { video: false, audio: true } for example if there is not webcamera

async get_MediaRules(){

let devicesEnum = await navigator.mediaDevices.enumerateDevices();

devicesEnum.forEach( device => {
  if ( device.kind == "audioinput" ){
    this.physicalRule["audio"] = true;
  }
  else if ( device.kind == "videoinput"){
    this.physicalRule["video"] = true;
  }
}); 

return this.physicalRule;

}

Get mediaStream

let myStream = navigator.mediaDevices.getUserMedia(real_devices);

Create Transceiver and add it to connection ( do this on Caller Side ).

 async transceiverSetup(){

    let videoTracks = myStream.getVideoTracks();
    let audioTracks = myStream.getAudioTracks();

    if (videoTracks.length > 0){
      this.clientPeer.addTransceiver(videoTracks[0]);
    }
    else{
      let video = this.myStream.addTransceiver("video");
      video.direction = "recvonly";
    }
    
    if (audioTracks.length > 0){
      this.myStream.addTransceiver(audioTracks[0]);
    }
    else{
      let audio = this.myStream.addTransceiver("audio");
      audio.direction = "recvonly";
    }
  }

After that you call

myPeer.createOffer()...

And on other side after you receiver Offer, you call remoteTrackSetup(). function and you setup on your side transceivers.

  async configure_transceiver(mediaTracks, transceivers){
    transceivers.forEach( async(any) => {
      if (any.receiver.track){
        if (mediaTracks.length > 0){
          any.direction = "sendrecv";
          await any.sender.replaceTrack(mediaTracks[0]);
        }
        else{
          any.direction = "recvonly";
          await any.sender.replaceTrack(null);
        }
      }else{
        if (mediaTracks.length > 0){
          any.direction = "sendonly";
          await any.sender.replaceTrack(mediaTracks[0]);
        }else{
          any.direction = "inactive";
          await mediaTracks.sender.replaceTrack(null);
        }
      }
    });
  }

  async remoteTrackSetup(){
    let mediaStream = GlobalState.Media;

    let audioTracks = mediaStream.getAudioTracks();
    let videoTracks = mediaStream.getVideoTracks();

    let transceivers = this.clientPeer.getTransceivers();

    let audioTransceivers = transceivers.filter(function(tr){
      return tr.receiver.track.kind == "audio";
    });

    let videoTransceivers = transceivers.filter(function(tr){
      return tr.receiver.track.kind == "video";
    });
    await this.configure_transceiver(audioTracks, audioTransceivers);
    await this.configure_transceiver(videoTracks, videoTransceivers); 
  }

After those functions you call

myPeer.createAnswer()...

And connection is fully established.

Here is code for ontrack event.

 setTransceiver(transceiver){

    if(!this.videoObject.srcObject || this.videoObject.srcObject.getTracks().length == 2){

      this.videoObject.srcObject = new MediaStream([transceiver.receiver.track]);
    }else{

      this.videoObject.srcObject.addTrack(transceiver.receiver.track);
    }
  }

Upvotes: 0

Related Questions