Next article

Leaflet is a free, open source and easily available, straightforward, lightweight, adaptable and likely the most mainstream open-source intelligent mapping library right now. Leaflet was...

Integrate video and chat functionality with WebRTC, Socket.IO & Node.js

Overview

We’re going to learn how you can integrate simple video call and voice chat functionality in your website with WebRTC, Socket.IO & Node.js.

WebRTC (Web Real-Time Communications) is an open source project which enables real-time communication of audio, video and data in Web and native apps. And we’ll use Socket.IO and Node.js development services for setting up signaling server.

Requirements

• Latest browser which is compatible to WebRTC application development
• Node.js development
• Basic understanding of Node.js and client-side JavaScript
Paid or Free PHP editor

Alright, so with this basic understanding and requirements in place, let’s get started with the integration process.

Building a real-time signaling server

For setting up the signaling server, we need to install Socket.IO and ExpressJS. In your project’s directory, execute following command:

Terminal/Command Prompt

npm i --save socket.io express

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

Now, here we can create the file signaler.js which will hold the code for the signaling server.

signaler.js

var port = 3000;
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http); // socket.io instance initialization
 
// listen on the connection event for incoming sockets
io.on('connection', function(socket){
 console.log('A new client connected');
});
 
http.listen(port, function(){
 console.log('Signaler listening on *: ', port);
});

Here, we have:

  • added the required libraries
  • instantiated our ExpressJS app and
  • set the app to listen on a defined port, i.e. 3000

We have also set up an event listener to listen on the connection event for incoming sockets. This on() method takes two arguments:

  • the name of the event and
  • callback which will be executed after every specified event

For instance, the “connection” event, which returns a socket object that is passed to the callback function. By using this socket object, you can send and receive any events you want, with any data you want. Such emitted events can be captured by the Socket.IO client(s) respectively.

You may check out this emit-cheatsheet, for emitting events as required in your application.

In following code snippet, we’ve included some basic setup for the simple chat functionality, to exchange events/messages as briefed in the comments.

signaler.js

...
// listen on the connection event for incoming sockets
io.on('connection', function(socket){
 console.log('A new client connected');
 
 // To subscribe the socket to a given channel
 socket.on('join', function (data) {
   socket.join(data.username);
 });
 
 // To keep track of online users
 socket.on('userPresence', function (data) {
   onlineUsers[socket.id] = {
     username: data.username
   };
   socket.broadcast.emit('onlineUsers', onlineUsers);
 });
 
 // For message passing
 socket.on('message', function (data) {
   io.sockets.to(data.toUsername).emit('message', data.data);
 });
 
 // To listen for a client's disconnection from server and intimate other clients about the same
 socket.on('disconnect', function (data) {
   socket.broadcast.emit('disconnected', onlineUsers[socket.id].username);
 
   delete onlineUsers[socket.id];
   socket.broadcast.emit('onlineUsers', onlineUsers);
 });
});
...

As you’ll learn in the following sections, eventually, from client side, we are going to:

  • establish a connection with the signaler
  • subscribe the socket to a given channel (channel name will be logged-in user’s username)
  • keep track of online users
  • send/receive event/message within the specific room, for various purposes including
  • making/receiving/answering/ending a call

As in the above code, we’ve subscribed the socket to a specified channel using join() method.

onlineUsers array will hold the data for currently online users, which is updated whenever a client is connected/disconnected, and subsequently broadcasting the event – ‘onlineUsers’ – listening to which, the respective client(s) can be updated about the online presence of the user who has connected/disconnected.

And for sending messages within the specific room, we’ve used:

io.sockets.to(data.toUsername).emit('message', data.data);

Which will be utilized for communicating with a specific user having his/her own unique room.

We can test our signaling server, by starting the application with:

Terminal/Command Prompt

node signaler.js
Signaler listening on *: 3000

Integrating Socket.IO and WebRTC components on client side

Now that we have our signaler up and running, let’s integrate Socket.IO on the client side. For this chat functionality, we’ve used code from WebRTC Experiments & Demos – written by Muaz Khan, and made some customizations as per requirements.

We need to include following libraries in the project’s HTML page, let’s say, index.html

  • socket.io.js: Socket.IO client library that loads on the browser side.
  • adapter-latest.js: A library which ensures interoperability across different browser implementations of WebRTC.

index.html

...
<script src="https://cdn.webrtc-experiment.com/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
...
<script src="/WebRTC/PeerConnection.js"></script>
<script src="/WebRTC/client.js"></script>
...

NodeJS developers can write the code for handling client-side user interactions and to establish the signaler-connection which will eventually receive all the updates from the server without refreshing the page.

/WebRTC/client.js

...
 var SIGNALING_SERVER = 'http://localhost:3000/';
 // To create a new connection to the signaling server
 socket = io.connect(SIGNALING_SERVER);
 socket.on('connect', function () {
   // To subscribe the socket to a given channel
   socket.emit('join', {
     username: loggedInUser.username
   });
 });
 // For sending message
 socket.send = function (message) {
   socket.emit('message', {
     fromUsername: peer.fromUsername,
     toUsername: peer.toUsername,
     data: message
   });
 };
 socket.on('disconnect', function () {
   // To intimate other clients about disconnection from server
   socket.emit('disconnect', {
     username: loggedInUser.username
   });
 });
...
...
 // To keep track of online users
 socket_client.on('onlineUsers', function (onlineUsers) {
   $.each(onlineUsers, function (n, user) {
     if (user &amp;&amp; user.username != loggedInUser.username &amp;&amp; !(user.username == 'undefined' || user.username == '')) {
       chatObject.data.connections[user.username] = {
       onlineStatus: 'online'
       };
     }
  });
 });
 // To listen for other clients' disconnection from server
 socket_client.on('disconnected', function (username) {
   chatObject.data.connections[username] = {
     onlineStatus: 'offline',
   };
 });
 
 // To intimate other clients about online presence
 socket_client.emit('userPresence', {
   username: username
 });
...

In the above code, firstly, we’ve established a signaler-connection, and instantiated socket.

Then we’ve several listeners which are triggered whenever the socket is connected/disconnected. On socket connection, we’ve emitted join event so as to subscribe the socket to the logged-in user’s channel. Through this code, we can keep track of online presence of all the users, on both client and server side, and display other users’ status on the client side.

We have also defined an essential function expression for sending a message, which comprises of:

  • fromUsername: Room of the sender of the message
  • toUsername: Room of an intended recipient of the message
  • data: Message data itself

That’s it! If you now reload the server and the website, you should see the console log as follows:

Terminal/Command Prompt

node signaler.js
Signaler listening on *: 3000
A new client connected

If you open several tabs, it will print several messages for each new client connected.

With some supplementary code and utilizing the events for online presence and messaging, you can display the online/offline status of other clients, send/receive messages to/from online clients and so on.

Making & Receiving a call

Get video & voice from your webcam

In order to get video & voice from your webcam, we need to call navigator.getUserMedia() method. When this method is called, the browser will request the user for permission to access the Camera/Microphone (if this is the first time Camera/Microphone access has been requested for the current origin). Once the user allows access, a MediaStream is returned, which can be used by a media element via the src/mozSrcObject attribute, to render the media stream on the client side.

/WebRTC/client.js

...
 // To get video &amp; voice from webcam
 function getUserMedia(mediaType, callback) {
   var mediaStreamConstraints = {};
   if (mediaType == 'audio') {
     mediaStreamConstraints = {
       audio: {
         echoCancellation: true
       }
     };
     window.mediaType = 'audio';
   } else
     window.mediaType = 'video';
   if (mediaType == 'video') {
     mediaStreamConstraints = {
       audio: {
       echoCancellation: true
       },
       video: {
       optional: [],
       mandatory: {}
       }
     };
   }
   navigator.getUserMedia(mediaStreamConstraints, function (stream) {
     if (peer)
       peer.mediaType = mediaType == 'audio' ? 'audio' : 'video';
     callback(stream);
     var mediaElement = document.createElement(mediaType == 'audio' ? 'audio' : 'video');
     mediaElement.id = 'selfMedia';
     mediaElement.preload = 'none';
     mediaElement[isGecko ? 'mozSrcObject' : 'src'] = isGecko ? stream : (window.URL || window.webkitURL).createObjectURL(stream);
     mediaElement.controls = false;
     mediaElement.muted = true;
     mediaElement.volume = 0;
     peer.onStreamAdded({
       mediaElement: mediaElement,
       username: username,
       stream: stream
     });
   }, function () {
     alert('Could not connect camera!');
   });
 }
...

As you can see in the above code, depending on whether it’s a Video call/Voice call,

  • We have set respective media stream constraints. There are lots more options for these constraints. You may take a look at the demo at:
    https://webrtc.github.io/samples/src/content/peerconnection/constraints/
  • Once the stream has been added, we’ve created respective HTML element which will be rendered on the client side.
  • Or if there’s any issue in capturing the stream, we can inform the user about the same.

Stream video & voice with RTCPeerConnection

RTCPeerConnection is an API for making WebRTC calls to stream video and audio, and exchange data. As in the included script by Muaz Khan – PeerConnection.js – it sets up a connection between two RTCPeerConnection objects which are known as peers.

/WebRTC/client.js

...
 // Instantiate PeerConnection
 peer = new PeerConnection(socket);
 // Setup peer methods
 setPeerMethod(peer);
 function setPeerMethod(peer) {
   // On incoming call
   peer.onUserFound = function (messageData) {
     // ...
     // Handle UI for the incoming call
     // ...
     // On call accept
     getUserMedia(chatObject.data.callType, function (stream) {
       peer.toUsername = callerUserId;
       peer.fromUsername = loggedInUser.username;
       peer.addStream(stream);
       peer.sendParticipationRequest(callerUserId);
     });
   };
   // Render media-stream elements for both caller and callee respectively
   peer.onStreamAdded = function (e) {
     var media = e.mediaElement;
     if (chatObject.data.callType == 'video') {
       addVideo(media);
     } else {
       addAudio(media);
     }
   };
   // Remove media-stream elements
   peer.onStreamEnded = function (e) {
     // ...
   };
 };
 function addVideo(video) {
   var video_id = video.getAttribute('id');
   if (video_id == 'selfMedia') {
     $('#selfVideoContainer').append(video);
   } else {
     if (chatObject.data.callTimer == 0) {
       chatObject.data.callTimer = startTimer('callTimer');
       peer.stopBroadcast();
     }
     $('#otherVideoContainer').append(video);
   }
   // Show loading animation.
   var playPromise = video.play();
   if (playPromise !== undefined) {
     playPromise.then(function (_) {
       // Automatic playback started!
       // Show playing UI.
     }).catch(function (error) {
       // Auto-play was prevented
       // Show paused UI.
     });
   }
   scaleVideos();
 };
 function addAudio() {
   // Similar to addVideo()
   // ...
 };
...

In the above code, first off, we have instantiated a PeerConnection object –peer, and then we’ve set up its methods with setPeerMethod(peer).

peer.onUserFound is triggered whenever a client is receiving a call, so from here, we can prompt the user about the incoming call and let the user Answer/Decline the call.

If the callee accepts the call, we have:

  • Called the getUserMedia() to capture callee’s media stream
  • Subsequently set the peer connection data and
  • Sent the participation request to the caller using peer.sendParticipationRequest()

Using addVideo()/addAudio(), we can render the media stream on the client side for caller and callee, consequently. Moreover, we can also display call timer and stop broadcasting outgoing call from caller’s end.

Combine peer connection and signaling

In the following code snippet, we’ve included some of the updated code as in PeerConnection.js, in order to make it work for both video call & voice call and to stop the media stream.

PeerConnection.js

...
 // ...
 // Some customization in PeerConnection.js
 // ...
 root.startBroadcasting = function (broadcastData) {
   // ...
   socket.send({
     userid: root.userid,
     broadcasting: true,
     broadcastData: broadcastData
   });
   // ...
 };
 // ...
 // ...
 onStreamAdded: function (stream) {
   // ...
   var eType = 'video';
   if (root.MediaStream &amp;&amp; root.MediaStream.getVideoTracks &amp;&amp; !root.MediaStream.getVideoTracks().length) {
     eType = 'audio';
   }
   var mediaElement = document.createElement(eType);
   mediaElement.id = 'callerMedia_' + root.participant;
   mediaElement.preload = 'none';
   mediaElement[isFirefox ? 'mozSrcObject' : 'src'] = isFirefox ? stream : window.URL.createObjectURL(stream);
   mediaElement.autoplay = true;
   mediaElement.controls = false;
   // ...
 }
 // ...
 function closePeerConnections() {
   // ...
   // To stop the media stream
   if (root.MediaStream) {
     root.MediaStream.getTracks().forEach(function (track) {
       return track.stop();
     });
   }
   // ...
 }
 // ...
 // if someone is broadcasting himself!
 if (message.broadcasting &amp;&amp; root.onUserFound) {
   var messageData = {
     userid: message.userid,
     callerName: message.broadcastData.callerName,
     callType: message.broadcastData.callType
   };
   root.onUserFound(messageData);
 }
 // ...
...

/WebRTC/client.js

...
 // ...
 // Handle message broadcast and media-streaming for outgoing call
 function createCustomRoom(calleeId) {
   peer.userid = userName;
   peer.toUsername = calleeId;
   getUserMedia(chatObject.data.callType, function (stream) {
     peer.addStream(stream);
     peer.startBroadcasting({
       callerName: loggedInUser.FullName,
       calleeId: calleeId,
       callType: chatObject.data.callType
     });
   });
 }
 // ...
 // Handle Outgoing call
 $(document).on('click', '.call_button', function () {
   chatObject.data.callType = $(this).attr('data-call_type');
   var calleeId = $(this).attr('data-username');
   if (typeof chatObject.data.connections[calleeId] != 'undefined' &amp;&amp; chatObject.data.connections[calleeId].onlineStatus != 'offline') {
     createCustomRoom(calleeId);
     chatObject.data.callee = {
       UserId: calleeId
     };
     chatObject.data.callStatus = (chatObject.data.callType == 'video') ? 'outgoing' : 'outgoingAudio';
     chatObject.onOutgoingCall();
   }
 });
 // ...
 // Handle End call
 $(document).on('click', '.end_button', function () {
   peer.toUsername = $(this).attr('data-username');
   peer.close();
 });
 // ...
...

As you might have understood in the above code, here we have managed message broadcast, media streaming and UI for an Outgoing call and when the call is ended.

When a user attempts to make a call, we have:

  • ascertained that the callee is available for the call
  • then called the createCustomRoom() to:
  • Set the peer connection data
  • Capture caller’s media stream and
  • Subsequently started broadcasting the message for the outgoing call to the respective callee.

As you would have guessed, this broadcasted message (i.e. call) will be received by the respective callee, through peer.onUserFound, and rest can be followed up as described therein.

When either of the communicating parties ends/declines a call, we need to:

  • close the peer connection using peer.close()
  • stop the media stream and
  • handle UI which is rendering the media streams

And that’s what there is to it.

Taking it to the next level, you can handle UI/UX for the even better experience by some fine tuning to ensure smooth flow across all the call-states, for multiple users, texting during ongoing video/voice call, engaged-tone and so on as per requirements.

Conclusion

Congratulations! You have successfully integrated video/voice chat functionality in your project, which enables users to do real-time video/voice streaming and data exchange.

Here, you learned how you can integrate simple video call and voice chat functionality in your website and how to use the core WebRTC APIs and set up a messaging server using Socket.IO and Node.js.

  • Get video & voice from your webcam
  • Stream video & voice with RTCPeerConnection
  • Setting up a signaling service for exchanging messages
  • Combine peer connection and signaling

Henceforth, you can explore more call applications including browser notifications, text messaging, screen sharing, group calls etc.

Comments

  • Leave a message...