Reputation: 1247
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
User2 enters the room
User1 sends a message to User2 though sockets
User1 presses "call" to call User2, gets local media and starts WebRTC connection
User2 presses "respond" to accept call from User2, accepts offer and responds with his local media
Edit 1: Seems to work if steps are done in the following order:
User1 enters the room
User1 presses "call" to call User2, gets local media and starts WebRTC connection
User2 enters the room
User2 presses "respond" to accept call from User2, accepts offer and responds with his local media
User1 presses "respond"
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:
console output after recieving "hello" message then pressing "respond" from user2:
Upvotes: 0
Views: 2179
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