Creating a chatroulette
The chatroulette that we will build is only for people residing in India, that is, a peer cannot connect to the PeerServer if the IP address of the peer doesn't resolve to India. We added this filter to make the website a little more complex to code so that you can learn how to check whether a user is allowed to connect to PeerServer or not.
We will use a single server that will serve webpages and also act as a PeerServer, that is, we will integrate PeerServer with the Express server.
We won't get into designing the frontend of our chatroulette. We will only be concentrating on building the architecture and functionalities.
The exercise files for this chapter contain two directories: Chatroulette
and Custom-PeerServer
. In the Chatroulette
directory, there are two directories: Initial
and Final
. In the Final
directory, you will find the complete chatroulette source code. In the Initial
directory, you will only find the HTML code for our chatroulette. The Initial
directory is to help you quickly get started with building the chatroulette.
You will place the code related to the frontend functionality of the site in the Initial/public/js/main.js
file and you will place the code related to the server side functionality in the Initial/app.js
file.
Building the backend
Our site will basically contain three URL end points: a root path for serving the home page, the /find
path to find the ID of a free user for chatting, and finally the /signaling
path that serves as the end point for PeerServer.
Every user will have a unique ID that is generated by PeerServer. For a user to retrieve the ID of another free user using the /find
URL, they must be first be connected to PeerServer.
The server will maintain two different arrays, that is, the first array contains IDs of the users connected to PeerServer and the second array contains IDs of the users that need a partner to chat.
Let's get started with building our backend. Place the following code in the app.js
file to create our web server and serve the home page of our site:
var express = require("express"); var app = express(); app.use(express.static(__dirname + "/public")); app.get("/", function(httpRequest, httpResponse, next){ httpResponse.sendFile(__dirname + "/public/html/index.html"); }) var server = app.listen(8080);
Here we are serving the index.html
file as our home page. Run the node app.js
command to start the server. I am assuming that you are running node.js
on the localhost, so open the http://localhost:8080/
URL on the browser to see the home page. The home page should look similar to the following image:
The following are the different elements of the home page:
- At the top of the home page, we will display the status of the
PeerServer
connection,DataConnection
, andMediaConnection
. - Then we will display a video element and message box.
MediaStream
of the remote peer will be rendered on the video element. - Then we have drop-down boxes for the user to select a microphone and webcam that they want to use if they have multiple microphones or webcams connected to their computer.
- Then we have checkboxes that allow the users to pause or resume their audio and video.
- Finally, we have a button that allows the user to disconnect from the current user and chat with another user.
Every interactive element in the HTML page has an ID associated with it. While coding the frontend of the website, we will be using their IDs to get their reference.
Now let's create our signaling server. Here is the code for this. Place it in the app.js
file:
var requestIp = require("request-ip"); var geoip = require("geoip-lite"); app.use("/signaling", function(httpRequest, httpResponse, next){ var clientIp = requestIp.getClientIp(httpRequest); var geo = geoip.lookup(clientIp); if(geo != null) { if(geo.country == "IN") { next(); } else { httpResponse.end(); } } else { next(); } }); var ExpressPeerServer = require("peer").ExpressPeerServer(server); app.use("/signaling", ExpressPeerServer); var connected_users = []; ExpressPeerServer.on("connection", function(id){ var idx = connected_users.indexOf(id); if(idx === -1) //only add id if it's not in the array yet { connected_users.push(id); } }); ExpressPeerServer.on("disconnect", function(id){ var idx = connected_users.indexOf(id); if(idx !== -1) { connected_users.splice(idx, 1); } idx = waiting_peers.indexOf(id); if(idx !== -1) { waiting_peers.splice(idx, 1); } });
The following is how the code works:
- Before the user can connect to PeerServer, we will find the country to which the IP address of the user belongs. We will find the IP address using the
request-ip
module and resolve the IP address to the country using thegeoip-lite
module. If the country isIN
or the country name couldn't be resolved, then we will allow the user to connect to PeerServer by triggering the next middleware, otherwise we will stop them by sending an empty response. - When a user connects to PeerServer, we will add the ID of the user in the
connected_users
array that maintains a list IDs if the users that are connected to PeerServer. Similarly, when the user disconnects from the PeerServer, we will remove the ID of the user from theconnected_users
array.
Now let's define route for the /find
path using which a user can find another user who is free to chat. The following is the code for this. Place this code in the app.js
file:
var waiting_peers = []; app.get("/find", function(httpRequest, httpResponse, next){ var id = httpRequest.query.id; if(connected_users.indexOf(id) !== -1) { var idx = waiting_peers.indexOf(id); if(idx === -1) { waiting_peers.push(id); } if(waiting_peers.length > 1) { waiting_peers.splice(idx, 1); var user_found = waiting_peers[0]; waiting_peers.splice(0, 1); httpResponse.send(user_found); } else { httpResponse.status(404).send("Not found"); } } else { httpResponse.status(404).send("Not found"); } })
Here is how the code works:
- The
waiting_users
array holds the IDs of the users who are free and looking for a partner to chat to. - When a user makes a request to the
/find
path, the route handler first checks whether the user is connected to PeerServer or not by checking whether the user ID is present in theconnected_users
array. - If the user is not connected to PeerServer, then it sends an HTTP 404 error. If the user is connected to PeerServer, then it checks whether the user's ID is present in the
waiting_list
array. If not, it adds in the array and proceeds. - Now it checks whether any other user ID is also present in the
waiting_list
array, and if yes, then it sends the first user ID in the list and then removes all user IDs from thewaiting_list
array. If it doesn't find any other user ID in thewaiting_list
array, then it simply sends404 error
.
Now we are done building the backend of our website. Before we get into building the frontend of our site, make sure that you restart the server with the latest code.
Building the frontend
First of all, as soon as the home page loads, we need to find the microphones and webcams connected to the user computer and list them so that the user can choose the desired device. The following is the code to do this. Place this code in the main.js
file:
window.addEventListener("load", function(){ MediaStreamTrack.getSources(function(devices){ var audioCount = 1; var videoCount = 1; for(var count = 0; count < devices.length; count++) { if(devices[count].kind == "audio") { var name = ""; if(devices[count].label == "") { name = "Microphone " + audioCount; audioCount++; } else { name = devices[count].label; } document.getElementById("audioInput").innerHTML = document.getElementById("audioInput").innerHTML + "<option value='" + devices[count].id + "'>" + name + "</option>"; } else if(devices[count].kind == "video") { var name = ""; if(devices[count].label == "") { name = "Webcam " + videoCount; videoCount++; } else { name = devices[count].label; } document.getElementById("videoInput").innerHTML = document.getElementById("videoInput").innerHTML + "<option value='" + devices[count].id + "'>" + name + "</option>"; } } }); });
Here we are retrieving the audio and video input devices using MediaStream.getSources
and populating the <select>
tags so that the user can choose an option.
As soon as the home page loads, we also need to create a Peer
instance. Here is the code to do this. Place this code in the main.js
file:
var peer = null; var dc = null; var mc = null; var ms = null; var rms = null; window.addEventListener("load", function(){ peer = new Peer({host: "localhost", port: 8080, path: "/signaling", debug: true}); peer.on("disconnected", function(){ var interval = setInterval(function(){ if(peer.open == true || peer.destroyed == true) { clearInterval(interval); } else { peer.reconnect(); } }, 4000) }) peer.on("connection", function(dataConnection){ if(dc == null || dc.open == false) { dc = dataConnection; dc.on("data", function(data){ document.getElementById("messages").innerHTML = document.getElementById("messages").innerHTML + "<li><span class='right'>" + data + "</span><div class='clear'></div></li> "; document.getElementById("messages-container").scrollTop = document.getElementById("messages-container").scrollHeight; }) dc.on("close", function(){ document.getElementById("messages").innerHTML = ""; }) } else { dataConnection.close(); } }) peer.on("call", function(mediaConnection){ if(mc == null || mc.open == false) { mc = mediaConnection; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; navigator.getUserMedia({video: true, audio: true}, function(mediaStream) { ms = mediaStream; mc.answer(mediaStream); mc.on("stream", function(remoteStream){ rms = remoteStream; document.getElementById("peerVideo").setAttribute("src", URL.createObjectURL(remoteStream)); document.getElementById("peerVideo").play(); }) }, function(e){ alert("An error occured while retrieving webcam and microphone stream"); }) } else { mediaConnection.close(); } }) });
Here is how the code works:
- First we declared five global variables.
peer
will hold reference for thePeer
instance,dc
will hold reference forDataConnection
,mc
will hold reference forMediaConnection
,ms
will hold reference for the localMediaStream
, andrms
will hold reference for the remoteMediaStream
. - Then, as soon as the page finished loading, we connected to PeerServer, creating a
Peer
instance and attaching event handlers for thedisconnected
,connection
, andcall
event handlers. - Then we made sure that in case a peer gets disconnected from PeerServer due to some reason, then it automatically tries to connect to PeerServer.
- If another peer tries to establish
DataConnection
with us, then we will only accept it if there is no otherDataConnection
currently established, otherwise we will reject it. After acceptingDataConnection
, we attached the event handlers for thedata
andclose
events to print the incoming messages in the chat box, and clear all messages in the chat box ifDataConnection
is closed. - Similarly, if another peer tries to establish
MediaConnection
with us, we will only accept it if there is no otherMediaConnection
currently established, otherwise we will reject it. After accepting theMediaConnection
, we will attach the event handler for thestream
event so that when remoteMediaStream
arrives, we can display it.
In the preceding code, we are waiting for another peer to establish DataConnection
and MediaConnection
with us.
Now let's write a code to find a free peer and establish DataConnection
and MediaConnection
with it. The following is the code for this. Place this code in the main.js
file:
function ajaxRequestObject() { var request; if(window.XMLHttpRequest) { request = new XMLHttpRequest(); } else if(window.ActiveXObject) { try { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { request = new ActiveXObject('Microsoft.XMLHTTP'); } } return request; } function connectToNextPeer() { var request = ajaxRequestObject(); var url = "/find?id=" + peer.id; request.open("GET", url); request.addEventListener("load", function(){ if(request.readyState === 4) { if(request.status === 200) { dc = peer.connect(request.responseText, {reliable: true, ordered: true}); dc.on("data", function(data){ document.getElementById("messages").innerHTML = document.getElementById("messages").innerHTML + "<li><span class='right'>" + data + "</span><div class='clear'></div></li>"; document.getElementById("messages-container").scrollTop = document.getElementById("messages-container").scrollHeight; }) dc.on("close", function(){ document.getElementById("messages").innerHTML = ""; }) navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; var audioInputID = document.getElementById("audioInput").options[document.getElementById("audioInput").selectedIndex].value; var videoInputID = document.getElementById("videoInput").options[document.getElementById("videoInput").selectedIndex].value; navigator.getUserMedia({video: {mandatory: {sourceId: videoInputID}}, audio: {mandatory: {sourceId: audioInputID}}}, function(mediaStream) { ms = mediaStream; if(document.getElementById("audioToggle").checked) { var tracks = ms.getAudioTracks(); if(document.getElementById("audioToggle").checked) { tracks[0].enabled = true; } else { tracks[0].enabled = false; } } if(document.getElementById("videoToggle").checked) { var tracks = ms.getVideoTracks(); if(document.getElementById("videoToggle").checked) { tracks[0].enabled = true; } else { tracks[0].enabled = false; } } mc = peer.call(request.responseText, ms); mc.on("stream", function(remoteStream){ rms = remoteStream; document.getElementById("peerVideo").setAttribute("src", URL.createObjectURL(remoteStream)); document.getElementById("peerVideo").play(); }) }, function(e){ alert("An error occured while retrieving webcam and microphone stream"); }); } } }, false); request.send(null); } function communication() { if(peer != null && peer.disconnected == false && peer.destroyed == false) { if(dc == null || mc == null || dc.open == false || mc.open == false) { connectToNextPeer(); } } } setInterval(communication, 4000);
This code is long but easy to understand. Here is how the code works:
- First we defined a
ajaxRequestObject()
function that just returns an AJAX object and hides browser differences by creating an AJAX object. - Then we defined the
connectToNextPeer()
method that makes requests for a free ID from the/next
path, and if found, it establishesDataConnection
andMediaConnection
with this peer. It also attaches the necessary event handlers that are same as the previous code. - While retrieving
MediaStream
, it uses the device selected by the user in the dropdown. - Before calling the other peer, it sets the
enabled
property totrue
orfalse
, depending on whether the checkbox is checked or not respectively. - Finally, we set a timer that calls the
connectToNext()
peer once in every four second if the peer is connected to PeerServer, andMediaConnection
orDataConnection
is currently not established with another peer.
Now we need to write code to send the message to a connected peer when the user presses the Enter key on the text input fields of the message box. Here is the code to do this. Place this code in the main.js
file:
document.getElementById("message-input-box").addEventListener("keypress", function(){ if(dc != null && dc.open == true) { var key = window.event.keyCode; if (key == 13) { var message = document.getElementById("message-input-box").value; document.getElementById("message-input-box").value = ""; dc.send(message); document.getElementById("messages").innerHTML = document.getElementById("messages").innerHTML + "<li><span class='left'>" + message + "</span><div class='clear'></div></li> "; document.getElementById("messages-container").scrollTop = document.getElementById("messages-container").scrollHeight; } else { return; } } })
Here, at first, we are checking whether DataConnection
is established or not. If DataConnection
is currently established, then we will send a message to the connected peer and also display the message in the message box.
Now we need to write the code to pause or resume audio and video when the user toggles the checkboxes. The following is the code to do this. Place this code in the main.js
file:
document.getElementById("videoToggle").addEventListener("click", function(){ if(ms !== null) { var tracks = ms.getVideoTracks(); if(document.getElementById("videoToggle").checked) { tracks[0].enabled = true; } else { tracks[0].enabled = false; } } }); document.getElementById("audioToggle").addEventListener("click", function(){ if(ms !== null) { var tracks = ms.getAudioTracks(); if(document.getElementById("audioToggle").checked) { tracks[0].enabled = true; } else { tracks[0].enabled = false; } } });
Here we are achieving this functionality by assigning true
or false
to the enabled property of the tracks.
We need to close MediaConnection
and DataConnection
and find another user for chatting when the user clicks on the Next User button. The following is the code to do this. Place this code in the main.js
file:
document.getElementById("next").addEventListener("click", function(){ if(mc != null) { mc.close(); } if(dc != null) { dc.close(); } connectToNextPeer(); })
If there is any MediaConnection
or DataConnection
currently established, then we are closing it. Then we will call the connectToNextPeer()
method to establish MediaConnection
and DataConnection
.
Now we finally need to display the status of the peer-to-peer connection and PeerServer connection. Here is the code to do this. Place this code in the main.js
file:
setInterval(function(){ if(dc == null || mc == null || dc.open == false || mc.open == false) { document.getElementById("peerStatus").innerHTML = "Waiting for a free peer"; } else { document.getElementById("peerStatus").innerHTML = "Connected to a peer"; } if(peer != null && peer.disconnected == false && peer.destroyed == false) { document.getElementById("peerServerStatus").innerHTML = "Connected to PeerServer"; } else { document.getElementById("peerServerStatus").innerHTML = "Not connected to PeerServer"; } }, 4000);
Here we are checking and updating the status every 4
seconds.
Testing the website
To test the chatroulette website we just created, first make sure that the server is running and then open the http://localhost:8080/
URL in two different tabs, browsers, or devices.
Now you will see that both of them automatically get connected and are able to chat with each other.