WebRTC - Video Call with SocketIO Nodejs

WebRTC is an open standard for real-time, plugin-free video, audio, and data communication. WebRTC enables peer-to-peer communication, it allows communication among browsers and devices. All the modern browser supports WebRTC.

In this post, we will talk about WebRTC and create a basic Video Calling web app on Nodejs with the help of socket IO.

WebRTC

Web Real-Time Communication is a technology that allows Web applications and sites to capture audio and/or video media. WebRTC allows Web applications and sites to exchange data between browsers without requiring any intermediary.

WebRTC establishes connections between two peers and is represented by the RTCPeerConnection interface. Once the connection is established, media streams and/or data channels can be added to the connection. Connections between peers can be made without requiring any special drivers or plug-ins, and can often be made without any intermediary servers.

Signaling

WebRTC can work without the servers, but it requires some kind of server to establish the connection. The server act as a channel to exchange information required to establish a peer-to-peer connection.

The information that required to transfer is "Offer", "Answer" and  "about the Network Connection".

Peer A (Ram) who will be the initiator of the connection, will create an Offer. They will then send this offer to Peer B (Sita) using the chosen signal channel. Peer B (Sita) will receive the Offer from the signal channel and create an Answer. They will then send this back to Peer A (Ram) along the signal channel.

Once Offer and Answer is Done, Now the connection between the to peer should be started. For that peer exchange RTCIceCandidate, An ICE candidate describes the protocols and routing needed for WebRTC to be able to communicate with a remote device. Each peer proposes their best candidates, starting with best to worst. Then they agree on using once and the connection is established.

All these Candidate exchanges need to be handled through the signalling channel.

STUN and TURN

If the two devices are separated by Network Address Translation (NAT), to collect require ICE candidates, we need some other service.

Network address translation (NAT) is a method of mapping an IP address space into another by modifying network address information in the IP header of packets while they are in transit across a traffic routing device.

STUN is on the public internet, the apps can use STUN server to discover its IP:port from a public perspective. STUN server simply check the IP:Port of the incoming request and respond it back, there is not much work there is so not much powerful server is required.

If WebRTC cannot establish a connection with the above methods, TURN servers can be used as a fallback, relaying data between endpoints.

Setup STUN and TURN server on Ubuntu

Video Call with SocketIO on Nodejs

We understand some basics of WebRTC, let's use WebRTC to create a Video Calling application using SocketIO as a signalling channel.

For the purposes of this tutorial, We will create a basic application where we can log in (just to get the name) then make a call, and accept a call. Nothing more. In this post We will look through the core component on the code, You can check the source code in our Github.

Server

As we have already mentioned, we will use Nodejs SocketIO for signalling the information between the clients, we will have static files to serve as frontend within Nodejs.

We have a basic express application with SocketIO v4. Here is socket.js, the initIO is called from index.js passing the httpServer.


    const { Server } = require('socket.io');
let IO;

module.exports.initIO = (httpServer) => {
IO = new Server(httpServer);

IO.use((socket, next) => {
if (socket.handshake.query) {
let userName = socket.handshake.query.name
socket.user = userName;
next();
}
})

IO.on('connection', (socket) => {
console.log(socket.user, "Connected");
socket.join(socket.user);

socket.on('call', (data) => {
let callee = data.name;
let rtcMessage = data.rtcMessage;

socket.to(callee).emit("newCall", {
caller: socket.user,
rtcMessage: rtcMessage
})

})

socket.on('answerCall', (data) => {
let caller = data.caller;
rtcMessage = data.rtcMessage

socket.to(caller).emit("callAnswered", {
callee: socket.user,
rtcMessage: rtcMessage
})

})

socket.on('ICEcandidate', (data) => {
let otherUser = data.user;
let rtcMessage = data.rtcMessage;

socket.to(otherUser).emit("ICEcandidate", {
sender: socket.user,
rtcMessage: rtcMessage
})
})
})
}

module.exports.getIO = () => {
if (!IO) {
throw Error("IO not initilized.")
} else {
return IO;
}
}

As we have already discussed, we need a server to pass 3 core information Offer, Answer and ICECandidate. The 'call', event send the offer from the caller to the callee, the 'answerCall' event send Answer from the callee to the caller. the 'ICEcandidate' event, exchange the data.

This is the minimalist version of the signalling server we need.

Client

As WebRTC is peer to peer connection, there are relatively more tasks on the front end side. The front-end need to handle much of the tasks here. Let's get started.

As a basic HTML, we have login input, to get the user's input. call input to get the input for the user to call, the answer view, which displays the caller's information and option to accept. The calling view, which shows the ringing state, and the call in progress view, which shows the call in progress information. We have two video views as well, one for the local and another for the remote.

socketIO Connections and events

function connectSocket() {
socket = io.connect(baseURL, {
query: {
name: myName
}
});

socket.on('newCall', data => {
otherUser = data.caller;
remoteRTCMessage = data.rtcMessage

//DISPLAY ANSWER SCREEN
})

socket.on('callAnswered', data => {
remoteRTCMessage = data.rtcMessage
peerConnection.setRemoteDescription(new RTCSessionDescription(remoteRTCMessage));

callProgress()
})

socket.on('ICEcandidate', data => {
let message = data.rtcMessage

let candidate = new RTCIceCandidate({
sdpMLineIndex: message.label,
candidate: message.candidate
});

if (peerConnection) {
peerConnection.addIceCandidate(candidate);
}
})
}

peerConnection is an instance of RTCPeerConnection.

The connectSocket methods called when a user enter his/her name and hit log in. Here we simply listen for

  • new call event [newCall]
  • call answered event [callAnswered] and
  • ICEcandidate event [ICEcandidate].

We trigger similar events when:

  • user enter the name can click call [call event],
  • when we click answer [answerCall event] and
  • when we get ICE candidate [ICEcandidate event].

Before processing a call / accepting a call we need to get the media stream, create peer connections and set up to establish a connection.


//event from html
function call() {
let userToCall = document.getElementById("callName").value;
otherUser = userToCall;

beReady()
.then(bool => {
processCall(userToCall)
})
}

//event from html
function answer() {
beReady()
.then(bool => {
processAccept();
})

document.getElementById("answer").style.display = "none";
}

The beReady method creates the media stream, then calls 


function beReady() {
return navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
.then(stream => {
localStream = stream;
localVideo.srcObject = stream;

return createConnectionAndAddStream()
})
.catch(function (e) {
alert('getUserMedia() error: ' + e.name);
});
}

function createConnectionAndAddStream() {
createPeerConnection();
peerConnection.addStream(localStream);
return true;
}

function createPeerConnection() {
try {
// peerConnection = new RTCPeerConnection(pcConfig);
peerConnection = new RTCPeerConnection();
peerConnection.onicecandidate = handleIceCandidate;
peerConnection.onaddstream = handleRemoteStreamAdded;
peerConnection.onremovestream = handleRemoteStreamRemoved;
console.log('Created RTCPeerConnnection');
return;
} catch (e) {
console.log('Failed to create PeerConnection, exception: ' + e.message);
alert('Cannot create RTCPeerConnection object.');
return;
}
}

function handleIceCandidate(event) {
if (event.candidate) {

//EMIT TO SOCKET
sendICEcandidate({
user: otherUser,
rtcMessage: {
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
}
})

} else {
console.log('End of candidates.');
}
}

function handleRemoteStreamAdded(event) {
remoteStream = event.stream;
remoteVideo.srcObject = remoteStream;
}

function handleRemoteStreamRemoved(event) {
remoteVideo.srcObject = null;
localVideo.srcObject = null;
}

function processCall(userName) {
peerConnection.createOffer((sessionDescription) => {
peerConnection.setLocalDescription(sessionDescription);
//EMIT TO SOCKET
sendCall({
name: userName,
rtcMessage: sessionDescription
})
}, (error) => {
console.log("Error");
});
}

function processAccept() {

peerConnection.setRemoteDescription(new RTCSessionDescription(remoteRTCMessage));
peerConnection.createAnswer((sessionDescription) => {
peerConnection.setLocalDescription(sessionDescription);

//EMIT TO SOCKET
answerCall({
caller: otherUser,
rtcMessage: sessionDescription
})

}, (error) => {
console.log("Error");
})
}

That's it. 

Now the peers can communicate with each other through the video call without any third-party application required.

You can find the source code on this Github repo https://github.com/BloggerNepal/WebRTC---Video-Call-with-SocketIO-Nodejs.

WebRTC not Working on Lan

Here we use the getUserMedia API to get the audio-video of the user, But some browsers restrict the use of these APIs to Secure Origins only. As Described on Deprecating Powerful Features on Insecure Origins. These APIs include:

  • Geolocation
  • Device motion / orientation
  • EME
  • getUserMedia
  • AppCache 
  • Notifications

But Lan network is not considered a secure origin. As described on Prefer Secure Origins For Powerful New Features, "Secure origins" are origins that match at least one of the following (scheme, host, port) patterns: 

  • (https, *, *)
  • (wss, *, *)
  • (*, localhost, *)
  • (*, 127/8, *)
  • (*, ::1/128, *)
  • (file, *, —)
So you cannot test on LAN network, you should test on local, or deploy using an SSL. To deploy one follow Nodejs Deployment with SSL certificate.

Conclusion

WebRTC is a technology to communicate within browsers without having the server in the middle of the peers. WebRTC can also be used to get the media access of the device for Web Application and the data can be used in any way required. WebRTC requires a signalling server to establish the communication, once the connection is established, it out of the way. STUN and TURN servers come into play when WebRTC cannot establish a direct path among peers. We created a video calling application using SocketIO on Nodejs as a signalling channel.

Posted by Sagar Devkota

1 Comments