Petru Tanas
Petru Tanas

Reputation: 1247

How to add video stream with WebRTC connection from another peer?

I am learning WebRTC and trying to create a simple chat with video call capabilities. I am using Django channels to handle the web-sockets and I connected the peers though them. Unfortunately, I am unable to get the other peer's media, and display it on the screen.

The connection seems to be successful, and the messages are traveling successfully though sockets, and no errors appear in console. What am I missing?

the logic is: - User1 enters the room

Edit 1: Seems to work if steps are done in the following order:

I do not quite understand why this works. "pc.ontrack" triggered only in this specific order, and why am I able to start a WebRTC connection before second peer enters the room?

room.html:

<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
{% load static %}
{% extends 'main/header.html' %}

{% block content %}

<body>

<div class="container">
    <a class="waves-effect waves-light btn prefix" id='call'>call</a>
    <a class="waves-effect waves-light btn prefix" id='respond'>respond</a>
  <div class="copy">Send your URL to a friend to start a video call</div>
  <video id="localVideo" autoplay muted></video>
  <video id="remoteVideo" autoplay></video>
    <textarea id="chat-log"  class="materialize-textarea" ></textarea><br/>

<div class="input-field col s12 ">
    <input id="chat-message-input" type="text" />
    <a class="waves-effect waves-light btn prefix" id="chat-message-submit"><i class="material-icons right">send</i></a>

</div>

</div>

</body>
<script>src = "{% static 'main/js/client.js' %}"></script>
{% endblock  %}

client.js:

// Generate random room name if needed

var roomName = "{{ room_name|escapejs }}";
var drone = new WebSocket(
        'ws://' + window.location.host +
        '/ws/chat/' + roomName + '/');


const configuration = {
  iceServers: [{
    urls: 'stun:stun.l.google.com:19302'
  }]
};

pc = new RTCPeerConnection(configuration);


function onSuccess() {};
function onError(error) {
  console.error(error);
};



document.getElementById('call').onclick = function() {startWebRTC(true);};
document.getElementById('respond').onclick = function() {startWebRTC(false);};



// Send signaling data via Scaledrone
function sendMessage(message) {
  var user = "{{user.username}}"
  drone.send(JSON.stringify({
            'message': message,
            'user': user

        }));
  console.log("Message sent")
};


function startWebRTC(isOfferer) {


  // 'onicecandidate' notifies us whenever an ICE agent needs to deliver a
  // message to the other peer through the signaling server
    pc.onicecandidate = event => {
        if (event.candidate) {
            sendMessage({'candidate': event.candidate});
    }
  };

  // If user is offerer let the 'negotiationneeded' event create the offer
  if (isOfferer) {

    pc.onnegotiationneeded = () => {
      pc.createOffer().then(localDescCreated).catch(onError);
      console.log("Offer created")
    }
  }

   // This part does not seem to be working
  // When a remote stream arrives display it in the #remoteVideo element
  pc.ontrack = event => {
    const stream = event.streams[0];
    if (!remoteVideo.srcObject || remoteVideo.srcObject.id !== stream.id) {
      remoteVideo.srcObject = stream;
      console.log("Remote stream added")
    }
  };

  navigator.mediaDevices.getUserMedia({
    audio: true,
    video: true,
  }).then(stream => {
    // Display your local video in #localVideo element
    localVideo.srcObject = stream;
    console.log("Local stream added")
    // Add your stream to be sent to the conneting peer
    stream.getTracks().forEach(track => pc.addTrack(track, stream));
    console.log("Added local stream to track")
  }, onError);



}




function localDescCreated(desc) {
    pc.setLocalDescription(
    desc,
    () => sendMessage({'sdp': pc.localDescription}),
    onError
  );
};

    document.querySelector('#chat-message-input').focus();
    document.querySelector('#chat-message-input').onkeyup = function(e) {
        if (e.keyCode === 13) {  // enter, return
            document.querySelector('#chat-message-submit').click();
        }
    };

    document.querySelector('#chat-message-submit').onclick = function(e) {
        var messageInputDom = document.querySelector('#chat-message-input');
        var message = messageInputDom.value;
        sendMessage(message);

        messageInputDom.value = '';
    };

// Listen to signaling data
drone.onmessage = function(e) {
        var data = JSON.parse(e.data);
        console.info(e)
        var message = data['message'];
        var user = data['user'];
    // Message was sent by us
    if (user === '{{user.username}}') {
        document.querySelector('#chat-log').value += (user +": " + message + '\n');
        elem = document.getElementById("chat-log")
        M.textareaAutoResize(elem);
        console.log("Echo")
      return;
    }
    if (message[0]){
    sdp = message[0]['sdp']
    candidate = message[0]['candidate']
    };

    console.log("Message recieved")
    if (sdp) {


pc.setRemoteDescription(new RTCSessionDescription(sdp),  () => {
        // When receiving an offer lets answer it
    if (pc.remoteDescription.type === 'offer') {
            pc.createAnswer().then(localDescCreated).catch(onError);
            console.log("Offer answerd")
        }
      }, onError);

      // This is called after receiving an offer or answer from another peer

    } else if (candidate) {
      // Add the new ICE candidate to our connections remote description
        pc.addIceCandidate(
        new RTCIceCandidate(candidate), onSuccess, onError);
        console.log("Ice candidate added")
    } else {
        document.querySelector('#chat-log').value += (user +": " + message + '\n');
        elem = document.getElementById("chat-log")
        M.textareaAutoResize(elem);
    }
  };

console output after sending "hello" message then pressing "call" from user1: enter image description here

console output after recieving "hello" message then pressing "respond" from user2: enter image description here

Upvotes: 0

Views: 2179

Answers (1)

Philipp Hancke
Philipp Hancke

Reputation: 17340

On the side that is answering, you are answering immediately, without waiting for getUserMedia and the resulting addTracks. These operations are asynchronous and they must be executed before you call createAnswer. Consequently, the answer does not contain a mediastream and ontrack will not be called on the end that is calling.

Using promise syntax makes this easier, along the lines of

pc.setRemoteDescription(offer)
.then(() => {
  return navigator.mediaDevices.getUserMedia({audio: true, video: true});
})
.then(stream => {
  stream.getTracks().forEach(track => pc.addTrack(track, stream));
  return pc.createAnswer();
})
... then continue with setLocalDescription and signal to the offerer

Upvotes: 1

Related Questions