WebRTC Video Chat with Peer.JS and Node.JS

OVERVIEW

We shall look at setting up a functional WebRTC VideoChat server using the PeerJS Library. The application will be running on Node.js on a Linux Server. In this post we are using a CentOS server. but the steps are easily applicable to Ubuntu as well.

This is an example of a one-to-one video chat where two people can do a video chat as well as send text messages to each other.

We have taken an existing sample script from https://ourcodeworld.com/articles/read/496/how-to-create-a-videochat-with-webrtc-using-peerjs-and-node-js . That article is excellent as a learning tutorial but we have made some changes and modifications based on our needs and also to add recording functionality.

BACKGROUND

One of the main problems developers face, while trying to make a WebRTC application is that the WebRTC specification changes very rapidly and a lot of the existing open source implementations are not able to keep up with the changes and thus, stop working. For that reason, a lot of people go for commercial WebRTC implementations like Vidyo.io or Janus, as these vendors have the resources to keep up with the latest changes and provide an implementation of WebRTC which works consistently.

A secondary problem is that since different browsers provide different levels of WebRTC compatibility, it makes app development complex . A WebRTC library/framework (free or commercial) takes away a lot of that complexity by providing a single interface to the developer, while taking care of different browser implementations internally.

The objective of this post is to make an app using PeerJS, which is free and open source. If you do some searching on google, there are a lot of people who say that PeerJS is outdated and does not conform to the latest WebRTC standards and hence should not be used for applications. They encourage using other libraries like EasyRTC . The fact is that PeerJS has been in active development (as of mid-2020) and there are many forks available from the master repo in github. PeerJS does work – it just requires some tweaks and the usage of the correct version of the library.

1.INSTALL NODE.JS and NPM

sudo yum install nodejs

sudo yum install npm

Verify the node version by typing node –version

Verify the npm version by typing npm –version

If you are on Ubuntu use ‘apt’ instead of ‘yum’ in the above commands.

2.SETUP SSL CERTIFICATES

Since webRTC requires the browser to access your local microphone and webcam, it is necessary to run the webapp on https instead of http. Running https on localhost might work with some tweaks (https://gist.github.com/cecilemuller/9492b848eb8fe46d462abeb26656c4f8) but by and large , it is preferable that the webapp run on a publicly registered domain . Here we will explain how to generate SSL certificates for a registered domain using LetsEncrypt/Certbot. It is assumed that you already have a domain registered and that the domain is pointing to the server where you will host the webapp.

Enable the EPEL repo on Centos by following the instructions on this link , depending on your version of CentOS.

Install Certbot using sudo dnf install certbot

If you are on Ubuntu, type sudo apt install certbot

If there is a webserver already running , stop that service first. Then run the command below for certbot to start a temporary webserver to generate the certificates:

sudo certbot certonly --standalone

Once the key generation process is over, you will find the certificate files in /etc/letsencrypt/live/<domain> where <domain> is the domain of your server.

3.SETUP THE DIRECTORY STRUCTURE

We will now setup the folder structure required for the app. It is assumed that the project folder is being created under /var/www but you are free to make it anywhere. Type the following commands

mkdir webrtc

cd webrtc

mkdir certificates

mkdir public

mkdir server

cd public

mkdir data

mkdir source

cd source

mkdir js

In the folder public/data make sure the current user has write access to this folder as data files will be written into this.

In the certificates folder, we will put the two certificate files created above, namely privkey.pem and cert.pem .

Change current directory to public. Create a file called package.json and set the app name to anything of your choice and save the file:

{
"name": "peerjs-videochat-application-client",
"version":"1.0.0"
}

Next install the express package in the public folder by typing npm install express

Change current directory to server i.e /var/www/webrtc/server

Create a file called package.json and set the app name to anything of your choice and save the file:

{
"name": "peerjs-videochat-application-client",
"version":"1.0.0"
}

We will now install the peer package by typing npm install peer

4.COPY CERTIFICATE FILES

In step 2 we created the SSL certificates using LetsEncrypt. Copy the two files privkey.pem and cert.pem into the /certificates folder in the application root folder. eg. /var/www/webrtc/certificates

If the certificates folder is not created, then create it now and copy the files into this folder. You have to make sure that the current user which is executing the npm scripts has read access to the two certificate files.

5.SETUP FILES AND SCRIPTS

We will now add all the files and scripts required to run the application.

In server folder , create the file peer-server.js . Add the following code:

var fs = require('fs');
var PeerServer = require('peer').PeerServer;

var server = PeerServer({
    port: 9000,
    path: '/peerjs',
    ssl: {
        key: fs.readFileSync('./../certificates/privkey.pem', 'utf8'),
        cert: fs.readFileSync('./../certificates/cert.pem', 'utf8')
    }
});

We have several files to create in the public folder. We will first create the files and then we will look at the work that each file is doing.

chatsession.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Video Chat </title>
    
    <!-- Use Bootswatch CSS from cdn -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css">
</head>

<script>

var qid = "";

var q = window.location.search;
if (q != null && q != "") {
  if (q.indexOf("id=") > -1)
    qid = q.substr(4);
}
</script>

<body>
    <div class="container">
         <div class="row">
            <h3 class="col-sm-12 text-center">
                Videochat  Demo
	    </h3>	
	    <div class="clearfix"></div>
	           <!-- The ID of your current session -->
		    <h4 class="text-center" style="display:none;"> 
			<span id="peer-id-label"></span>
		    </h4>
		    <h4 class="text-center" style="display:none;">
			<span id="peer-id-label2"></span>
		    </h4>

           <div class="row"><div class="col-sm-12 text-center">
		 <button class="btn btn-danger" style="display:none;" id="btnClose" type="button">Close Connection</button>
	    </div>
           </div>
           <div style="display:none;" id="divWait" class="row">
               <div class="col-sm-12 text-center">
                <h4>Please wait while we save the session recording..</h4>
                <img src="resources/ajax-loader.gif" border=0>
	      </div>
           </div>

            <div class="col-md-12 col-lg-12">
                <div class="form-horizontal" id="connection-form">
                    <fieldset>
                        <div class="form-group" style="display:none;">
                            <label for="name" class="col-lg-2 control-label">Username</label>
                            <div class="col-lg-10">
                                <input type="text" class="form-control" name="name" id="name" placeholder="Your random username">
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-lg-10">
                                <input type="text" style="display:none;" class="form-control" name="peer_id" id="peer_id" placeholder="Peer ID" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
                                
                                <!-- Show message if someone connected to the client -->
                                <div id="connected_peer_container" class="hidden">
                                    An user is already connected to your session. Just provide a name to connect !
                                </div>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-lg-10 col-lg-offset-2" xstyle="display:none;">
                                <button id="connect-to-peer-btn" class="btn btn-primary" style="display:none;">Connect to Peer</button>
                            </div>
                        </div>
                    </fieldset>
                </div>
            </div>
            <div class="col-md-12 col-lg-12">
                <div id="chat" class="hidden">
                    <div id="messages-container">
                        <div class="list-group" id="messages"></div>
                    </div>
                    <div id="message-container">
                        <div class="form-group" style="display:none;">
                            <label class="control-label">Live chat</label>
                            <div class="input-group">
                                <span class="input-group-btn" style="display:none;">
                                    <button id="call" class="btn btn-info">Call</button>
                                </span>
                                <input type="text" class="form-control" name="message" id="message" placeholder="Your message here ...">
                                <span class="input-group-btn">
                                    <button id="send-message" class="btn btn-success">Send Message</button>
                                </span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>



       <div class="row">
            <div class="col-md-6 col-lg-6">
                <!-- 
                    Display video of the current user
                    Note: mute your own video, otherwise you'll hear yourself ...
                 -->
                <div class="text-center">
                    <video id="my-camera"  width="650" height="500" autoplay="autoplay" muted="true" class="center-block"></video>
                    <span class="label label-info" id="label1"></span>
                     <br> <br>
                     <button  style="display:none;" id="btnRecord" class="btn btn-danger">Start Recording</button>
                     <br>
                      <div id="divTimer"></div>
                     <br>
		  <div id="divDownload" style="display:none;">
	          <b>My Recording</b><br>

		  <video id="recording" width="300" height="300" controls></video>
	          <br>
	  		<a class="btn btn-default"  id="downloadButton" class="button">
	    		Download
	  		</a>
                  </div> 
	        </div>
            </div>

            <div class="col-md-6 col-lg-6">
                <!-- Display video of the connected peer -->
                <div class="text-center">
                    <video id="peer-camera" width="650" height="500" autoplay="autoplay" class="center-block"></video>
                    <span class="label label-info" id="connected_peer"></span>
                </div>
            </div>
        </div>

     <div style="display:none">

	<div class="left">
	  <div id="startButton" class="button" style="display:none;">
	    Start
	  </div>
	  <video style="display:none;" id="preview" width="160" height="120" autoplay muted></video>
	</div>


	<div class="right">
	  <div id="stopButton" class="button" style="display:none;">
	    Stop
	  </div>
	</div>

	<div id="log" style="width:100%;height:200px;overflow:auto;" style="display:none;">
	</div>

    </div>

    <script  src="https://code.jquery.com/jquery-3.5.1.min.js"
			  integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
			  crossorigin="anonymous"></script>
    <script src="resources/js/peer.min.js"></script>
    <script src="resources/js/moment.js"></script>
    <script src="resources/js/record.js"></script>
    <script src="resources/js/script.js"></script>

</body>

</html>

instructor.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Instructor Page</title>
    
    <!-- Use Bootswatch CSS from cdn -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css">
</head>

<body>
    <div class="container">
        <div class="row">
         	<div class="col-sm-12 text-center">
		    <h4>Welcome To Video Chat</h4>
		    <br>
		    <button class="btn btn-large btn-primary" id="btnLogin" name="btnLogin">Login as Instructor</button>
		</div>
	</div> <!--row-->	

</body>
<script  src="https://code.jquery.com/jquery-3.5.1.min.js"
			  integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
			  crossorigin="anonymous"></script>

	<script>
	     $(document).ready(function() {
		     $('#btnLogin').click(function() {
		     	 $.get('/create-instructor-file', function(data) {
				var json = data; 
             			 if (json.status != "OK")
					 alert(json.msg);
				 else {
				   window.location.href="/chatsession?id=instructor";
				 }
            });
		     });
	     });
	</script>
</html>

list.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>List Recordings</title>
    
    <!-- Use Bootswatch CSS from cdn -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css">
</head>

<body>
    <div class="container">
        <div class="row">
         	<div class="col-sm-12 text-center">
		    <h4>VideoChat Recordings</h4>
		    <br>
                    <div id="listrecordings" class="text-left"></div> 
	        </div>  		
	</div> <!--row-->
    </div>

</body>
<script  src="https://code.jquery.com/jquery-3.5.1.min.js"
			  integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
			  crossorigin="anonymous"></script>

	<script>
	     $(document).ready(function() {
	     	 $.get('/list-recordings', function(data) {
			var json = data; 
             		 if (json.status != "OK") {
		            alert(json.error);
			 } else {
		           var json  = $.parseJSON(json.data);
			   var max = json.length;
			   var html = "Recordings(" + max + ")<br>";
			   for(var i=0; i < max; i++) {
			                                  
			      var row = "<div class='row'><div class='col-sm-6'>json[i].name + "</div><div class='col-sm-3'> " + json[i].size + "</div><div class='col-sm-3'> " + json[i].date + "</div></div>";
			      html += row;
			   }
		           $('#listrecordings').html(html);		
			  
			 }
		     });

	     });
	</script>
</html>

student.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Student Page</title>
    
    <!-- Use Bootswatch CSS from cdn -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/cerulean/bootstrap.min.css">
</head>

<body>
    <div class="container">
        <div class="row">
         	<div class="col-sm-12 text-center">
		    <h4>Welcome To Video Chat</h4>
		    <br>
		    <button class="btn btn-large btn-primary" id="btnLogin" name="btnLogin" style="display:none;">Login as Student </button>
		</div>
	</div> <!--row-->
	<div class="clearfix"></div>
	<div style="display:none;" class="row" id="divNotReady">
		<div class="col-sm-12 text-center alert alert-warning">
			Instructor is not available.Refresh page in some time to check status
		</div>
	</div>

</body>
<script  src="https://code.jquery.com/jquery-3.5.1.min.js"
			  integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
			  crossorigin="anonymous"></script>

	<script>
	     $(document).ready(function() {
	     	 $.get('/check-instructor-file', function(data) {
				var json = data; 
             			 if (json.status != "OK") {
				        $('#btnLogin').hide();
					$('#divNotReady').show();	
				 } else {
				        $('#btnLogin').show();	
					$('#divNotReady').hide();
				 }
		     });

	         $('#btnLogin').click(function() {
			window.location.href="/chatsession?id=student";

 		 });
	     });
	</script>
</html>

website-server.js

/**
 * This script starts a https server accessible at https://localhost:8443
 * to test the chat
 *
 * @author Carlos Delgado
 */
var fs     = require('fs');
var http   = require('http');
var https  = require('https');
var path   = require("path");
var os     = require('os');
var ifaces = os.networkInterfaces();
var moment = require("moment");
var inspect = require('util').inspect;
var bodyParser = require('body-parser');
var util = require("util");
var multiparty = require("multiparty");
const {exec} = require("child_process");

var FILEPATH = "/var/www/webrtc/public/data";

// Public Self-Signed Certificates for HTTPS connection
var privateKey  = fs.readFileSync('./../certificates/privkey.pem', 'utf8');
var certificate = fs.readFileSync('./../certificates/cert.pem', 'utf8');

var credentials = {key: privateKey, cert: certificate};
var express = require('express');
var app = express();

//app.use(bodyParser.urlencoded({limit: '200mb', extended: true}));


app.use(express.json());
app.use(express.urlencoded({limit: '200mb'}));

var httpServer = http.createServer(app);
var httpsServer = https.createServer(credentials, app);

function RecFile(name, size, date) {
 this.name = name;
 this.size = size;
 this.date = date;
}

/**
 *  Show in the console the URL access for other devices in the network
 */
Object.keys(ifaces).forEach(function (ifname) {
    var alias = 0;

    ifaces[ifname].forEach(function (iface) {
        if ('IPv4' !== iface.family || iface.internal !== false) {
            // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
            return;
        }
        
        console.log("");
        console.log("Welcome to the Chat Sandbox");
        console.log("");
        console.log("Test the chat interface from this device at : ", "https://localhost:8443");
        console.log("");
        console.log("And access the chat sandbox from another device through LAN using any of the IPS:");
        console.log("Important: Node.js needs to accept inbound connections through the Host Firewall");
        console.log("");

        if (alias >= 1) {
            console.log("Multiple ipv4 addreses were found ... ");
            // this single interface has multiple ipv4 addresses
            console.log(ifname + ':' + alias, "https://"+ iface.address + ":8443");
        } else {
            // this interface has only one ipv4 adress
            console.log(ifname, "https://"+ iface.address + ":8443");
        }

        ++alias;
    });
});

// Allow access from all the devices of the network (as long as connections are allowed by the firewall)
var LANAccess = "0.0.0.0";
// For http
httpServer.listen(8080, LANAccess);
// For https
httpsServer.listen(8443, LANAccess);

app.get('/', function (req, res) {
    res.sendFile(path.join(__dirname+'/index.html'));
});

app.get('/instructor', function (req, res) {
    res.sendFile(path.join(__dirname+'/instructor.html'));
});


app.get('/student', function (req, res) {
    res.sendFile(path.join(__dirname+'/student.html'));
});



app.get('/chatsession', function (req, res) {
    res.sendFile(path.join(__dirname+'/chatsession.html'));
});

app.get('/create-instructor-file', function (req, res) {
   console.log("create-instructrr-file"); 
    if (fs.existsSync("/tmp/vc-instructor-key.dat")) 
	fs.unlinkSync("/tmp/vc-instructor-key.dat");
    fs.writeFile("/tmp/vc-instructor-key.dat", "dummy",function(err) {
    if(err) {
        return res.json({status: "ERROR", msg: "Could not create instructor file"});
    }
    return res.json({status: "OK", msg: "OK"});
   }); 
});


app.get('/check-instructor-file', function (req, res) {
    if (!fs.existsSync("/tmp/vc-instructor-key.dat")) {
        return res.json({status: "ERROR", msg: "Instructor file does not exist"});
    } else {
	// if its more than 10 min old then it is stale
	var stats = fs.statSync("/tmp/vc-instructor-key.dat");
        var ctime = stats.ctime;
        var localtime = moment();
	var filetime = moment(ctime);
        var diffInMin = localtime.diff(filetime, "minutes");
	if (diffInMin > 10)
        	return res.json({status: "ERROR", msg: "Instructor file has expired. Please ask instructor to login first"});
	return res.json({status: "OK", msg: localtime +"," + filetime});
    }
});

app.get('/delete-instructor-file', function (req, res) {
    if (fs.existsSync("/tmp/vc-instructor-key.dat")) {
        fs.unlinkSync("/tmp/vc-instructor-key.dat");
    } 
    return res.json({status: "OK", msg: ""});
});


app.get('/create-recording-flag', function (req, res) {
   console.log("create-recording-flag"); 
    if (fs.existsSync("/tmp/vc-recording-flag.dat")) 
	fs.unlinkSync("/tmp/vc-recording-flag.dat");
    fs.writeFile("/tmp/vc-recording-flag.dat", "dummy",function(err) {
    if(err) {
        return res.json({status: "ERROR", msg: "Could not create recording flag file"});
    }
    return res.json({status: "OK", msg: "OK"});
   }); 
});




app.get('/check-recording-flag', function (req, res) {
    if (!fs.existsSync("/tmp/vc-recording-flag.dat")) {
        return res.json({status: "ERROR", msg: "Recording flag does not exist"});
    } else {
	return res.json({status: "OK", msg: localtime +"," + filetime});
    }
});

app.get('/delete-recording-flag', function (req, res) {
    if (fs.existsSync("/tmp/vc-recording-flag.dat")) {
        fs.unlinkSync("/tmp/vc-recording-flag.dat");
    } 
    return res.json({status: "OK", msg: ""});
});



app.post('/save-recording', function (req, res) {
   console.log("save-recording"); 
   var fname = req.body.fname;
   console.log("Saving " + req.body.fname);
   var base64Data = req.body.file.replace(/^data:(.*?);base64,/,""); 
   base64Data = base64Data.replace(/ /g, '+'); 
   
   var webmFile =  Buffer.from(base64Data, "base64");
   fs.writeFile("/var/webrtc/public/data/"+ fname, webmFile, function(err) {
       if (err) {
         console.log("File write error:" + err);
         reject(err);
       } else {
          console.log("File write success!!");
               
       } // if (err) else
   
   });
   return res.json({status: "OK", msg: "OK"});
});


app.get('/list', function (req, res) {
	res.sendFile(path.join(__dirname+'/list.html'));
});

app.get('/list-recordings', function (req, res) {
	var fArray = new Array();
	var items = fs.readdirSync(FILEPATH) ;
	console.log("items=" + items.length);
            for (var i=0; i < items.length; i++) {
                if (items[i].endsWith(".webm")) {
		   var f = FILEPATH +"/" + items[i];
		   var stats = fs.statSync(f);
                   var recFile = new RecFile(items[i], stats["size"], stats["ctime"]);
		   fArray.push(recFile); 
	 
		}
            }
	       
	   
   	return res.json({status: "OK", data: JSON.stringify(fArray)});

});



// Expose the css and js resources as "resources"
app.use('/resources', express.static('./source'));

We need to install the recordrtc npm package . In the public directory type npm install recordrtc

Change the current directory to public/source/js. We need to get a third party javascript library moment.js and put it here. This can be downloaded from https://momentjs.com/

We need peer.min.js in this folder. This can be obtained from https://unpkg.com/peerjs@1.2.0/dist/peerjs.min.js

However as a safe measure, the exact version which is being used in this sample can be downloaded from http://truelogic.org/peer.min.js

record.js


let preview = document.getElementById("preview");
let recording = document.getElementById("recording");
let startButton = document.getElementById("startButton");
let stopButton = document.getElementById("stopButton");
let downloadButton = document.getElementById("downloadButton");
let logElement = document.getElementById("log");
let mycamera = document.getElementById("my-camera");
var recorder = null;

let recordingTimeMS = 5000;
let lengthInMMS = 300000;
var recordedChunks = [];
var rFileName = '';

var recStart = null;
var timerRec = null;

mycamera.onplay = function() {
  console.log("mycamera.onplay()");
  mycamera.captureStream = mycamera.captureStream || mycamera.mozCaptureStream;
}

function log(msg) {
  //logElement.innerHTML += msg + "\n";
	console.log("recorder-" +msg);
}

function wait(delayInMS) {
  return new Promise(resolve => setTimeout(resolve, delayInMS));
}

function startRecording(stream, lengthms) {
 console.log("startRecording()"); 
 
  recorder = new MediaRecorder(stream);
  let data = [];

  recorder.ondataavailable = handleDataAvailable;
  recorder.start();
  log(recorder.state + " for " + (lengthms/1000) + " seconds...");

  let stopped = new Promise((resolve, reject) => {
    recorder.onstop = resolve;
    recorder.onerror = event => reject(event.name);
  });

  let recorded = wait(lengthms).then(
    () => recorder.state == "recording" && recorder.stop()
  )

  recStart = moment();
  timerRec = setInterval(recTimer, 1000);
}

function handleDataAvailable(event) {
 console.log("handleDataAvailable start");
 if (event.data.size > 0) {
   console.log("dta size=" + event.data.size);
   recordedChunks.push(event.data);

 mStop();
 }
}

function doRecorderStop() {
  if (timerRec != null)
    clearInterval(timerRec);
  $('#divTimer').html("");

  if (recorder != null)
   recorder.stop();

  $.get("/delete-instructor-file", function(data) {
        
  });
}


function mStop() {
   console.log("mStop()");
   var saveFile = moment().format("MMDDYYYY_hhmm_") + rFileName;
   console.log("saving " + saveFile);     
   $('#divWait').show();
   let recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
    recording.src = URL.createObjectURL(recordedBlob);
    downloadButton.href = recording.src;
    downloadButton.download = saveFile;

    //document.getElementById("divDownload").style.display = "";
    var reader = new window.FileReader();
    reader.readAsDataURL(recordedBlob);
    console.log("reader started");
    reader.onloadend = function() {
       console.log("reader.onloadend()");
       base64data = reader.result;
	
    var post = "fname=" + saveFile + "&file=" + base64data;
	    $.ajax({
	       url: "/save-recording",
	       data: post,
	       processData: false,
	       type: "POST",
	       success: function(data) {
                 $('#divWait').hide();  
		 alert("Recording has been saved successfully");
	        goBack(); 
	       },
	       error: function(err) {
                 $('#divWait').hide();  
		  alert("error" + err);
	       }
	    });	
        
     };
   	
}
function stop(stream) {
  stream.getTracks().forEach(track => track.stop());
}


//stopButton.addEventListener("click", function() {
 function stopRecording() {	
  stop(preview.srcObject);
 }
//}, false);

function recTimer() {
   var sysTime = moment();
   var hrs = moment.utc(sysTime.diff(recStart)).format("HH");
   var mins = moment.utc(sysTime.diff(recStart)).format("mm");
   var secs = moment.utc(sysTime.diff(recStart)).format("ss");
   console.log(hrs + ":" + mins + ":" + secs);
   $('#divTimer').html(hrs + ":" + mins + ":" + secs);
}

function goBack() {
}

script.js

// When the DOM is ready
document.addEventListener("DOMContentLoaded", function(event) {
    var peer_id;
    var username;
    var conn;
    var isRecording = false;
    var MSG_START_RECORDING = "START_RECORDING";
    var MSG_STOP_RECORDING = "STOP_RECORDING";

   console.log("qid=" + qid);
         if (qid == "instructor") {
          document.getElementById("peer-id-label2").innerHTML = "student";
 	  document.getElementById("peer_id").value =  "student";
 
	  document.getElementById("label1").textContent =  "Instructor";
	  document.getElementById("connected_peer").textContent =  "Student";
       } else {
          document.getElementById("peer-id-label2").innerHTML = "instructor";
 	  document.getElementById("peer_id").value =  "instructor";
	  document.getElementById("label1").textContent =  "Student";
	  document.getElementById("connected_peer").textContent =  "Instructor";
      }
   /**
     * Important: the host needs to be changed according to your requirements.
     * e.g if you want to access the Peer server from another device, the
     * host would be the IP of your host namely 192.xxx.xxx.xx instead
     * of localhost.
     *
     * The iceServers on this example are public and can be used for your project.
     */
    var peer = new Peer(qid, {
        host: "yourdomain.com",
        port: 9000,
        path: '/peerjs',
        debug: 3,
        config: {
            'iceServers': [
                { url: 'stun:108.177.98.127:19302' },
                {
                    url: 'turn:numb.viagenie.ca',
                    credential: 'muazkh',
                    username: 'webrtc@live.com'
                }
	       
            ]
        }
    });

    // Once the initialization succeeds:
    // Show the ID that allows other user to connect to your session.
    peer.on('open', function () {
        document.getElementById("peer-id-label").innerHTML = peer.id;
 
	// if this is student, start video session
	if (qid == "student") {
	    document.getElementById("connect-to-peer-btn").click();
	}		
    });

    // When someone connects to your session:
    //
    // 1. Hide the peer_id field of the connection form and set automatically its value
    // as the peer of the user that requested the connection.
    // 2.
    peer.on('connection', function (connection) {
        conn = connection;
        peer_id = connection.peer;
       
        // Use the handleMessage to callback when a message comes in
        conn.on('data', handleMessage);

        // Hide peer_id field and set the incoming peer id as value
        document.getElementById("peer_id").className += " hidden";
        document.getElementById("peer_id").value = peer_id;
        //document.getElementById("connected_peer").innerHTML = connection.metadata.username;
	document.getElementById("btnClose").style.display = "";
        
	document.getElementById("btnClose").onclick = function() { 
	        var closeNow = false;
		if (isRecording && qid == "instructor") {
		  $('#btnRecord').trigger("click");
		}
		else if (isRecording && qid == "student") {
		  	      // msg to student
			var data = {
			    from: qid,
			    text: MSG_STOP_RECORDING
			};
		} else {
		  closeNow = true;
		}
	
		peer.disconnect(); peer.destroy();
                if (closeNow) {
			if (qid == "instructor")
				window.location.href="/instructor";
			else if (qid == "student")
				window.location.href="/student";
		}
        }
     	if (qid == "instructor") {
	 document.getElementById("connect-to-peer-btn").click();
	} else if (qid == "student") {
	 //document.getElementById("call").click();
	}

	if (qid == "instructor")
		rFileName = "instructor.webm";
	else if (qid == "student")
		rFileName = "student.webm";

	if (qid == "instructor")	
		document.getElementById("btnRecord").style.display = "";
    });

   //3
  peer.on("close", function() {
        alert("Peer Session has been closed or disconnected"); 
        peer.destroy();
        doRecorderStop();
	});
  
  peer.on("disconnect", function() {
 
        alert("Peer Session has been closed or disconnected"); 
        peer.destroy();
	doRecorderStop();
  });

    peer.on('error', function(err){
        alert("An error occurred with peer: " + err);
        console.error(err);
        window.location.reload(); 
    });



  document.getElementById("btnRecord").addEventListener("click", function(){
       if (!isRecording) {
           isRecording = true;
           $("#btnRecord").text("Stop Recording");
           startRecording(window.localStream, lengthInMMS);

  	      // msg to student
		var data = {
		    from: qid,
		    text: MSG_START_RECORDING
		};

		// Send the message with Peer
		conn.send(data);


       } else {
	      // send msg to student 
		var data = {
		    from: qid,
		    text: MSG_STOP_RECORDING
		};

		// Send the message with Peer
		conn.send(data);


           isRecording = false; 
           $("#btnRecord").text("Start Recording");
	   doRecorderStop();
	}
  });
    /**
     * Handle the on receive call event
     */
    peer.on('call', function (call) {
        var acceptsCall = confirm("Videocall incoming, do you want to accept it ?");

        if(acceptsCall){
            // Answer the call with your own video/audio stream
            call.answer(window.localStream);

            // Receive data
            call.on('stream', function (stream) {
                // Store a global reference of the other user stream
                window.peer_stream = stream;
                // Display the stream of the other user in the peer-camera video element !
                onReceiveStream(stream, 'peer-camera');
            });

            // Handle when the call finishes
            call.on('close', function(){
                alert("The videocall has finished");
	        doRecorderStop();
	    });

            // use call.close() to finish a call
        }else{
            console.log("Call denied !");
        }
    });

    /**
     * Starts the request of the camera and microphone
     *
     * @param {Object} callbacks
     */
    function requestLocalVideo(callbacks) {
        // Monkeypatch for crossbrowser geusermedia
        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

        // Request audio an video
        navigator.getUserMedia({ audio: true, video: true }, 
		callbacks.success , callbacks.error
	);
	 
}

    /**
     * Handle the providen stream (video and audio) to the desired video element
     *
     * @param {*} stream
     * @param {*} element_id
     */
    function onReceiveStream(stream, element_id) {
        // Retrieve the video element according to the desired
        var video = document.getElementById(element_id);
        // Set the given stream as the video source
        //video.src = window.URL.createObjectURL(stream);

        video.srcObject =stream;
	video.onloadedmetadata = function(e) {video.play();}
	
        // Store a global reference of the stream
        window.peer_stream = stream;
    }

    /**
     * Appends the received and sent message to the listview
     *
     * @param {Object} data
     */
    function handleMessage(data) {
         if (data.text == MSG_START_RECORDING) {
	     startRecording(window.localStream, lengthInMMS);
	     return;
	 }
         else if (data.text == MSG_STOP_RECORDING) {
	     doRecorderStop();
	     return;
	 }
	

	 var orientation = "text-left";

        // If the message is yours, set text to right !
        if(data.from == username){
            orientation = "text-right"
        }
	var textClass = "success";
	if (data.from == "instructor")
		textClass = "danger";

        var messageHTML =  '';
        messageHTML += '<span class="' + textClass + '"><b>'+ data.from +'</b>: ' + data.text + '</span><br>';

        document.getElementById("messages").innerHTML += messageHTML;
    }

    /**
     * Handle the send message button
     */
    document.getElementById("send-message").addEventListener("click", function(){
        // Get the text to send
        var text = document.getElementById("message").value;

        // Prepare the data to send
        var data = {
            from: qid,
            text: text
        };

        // Send the message with Peer
        conn.send(data);

        // Handle the message on the UI
        handleMessage(data);

        document.getElementById("message").value = "";
    }, false);

    /**
     *  Request a videocall the other user
     */
    document.getElementById("call").addEventListener("click", function(){
        console.log('Calling  ' + peer_id);
        console.log(peer);

	var options = {
           'constraints': {
		'mandatory': {
		    'OfferToReceiveAudio':true,
		    'OfferToReceiveVideo':true
		}	
	    }
        }
        var call = peer.call(peer_id, window.localStream, options);

        call.on('stream', function (stream) {
	   console.log("Call.on('stream')");
            window.peer_stream = stream;

            onReceiveStream(stream, 'peer-camera');
        });
    }, false);

    /**
     * On click the connect button, initialize connection with peer
     */
    document.getElementById("connect-to-peer-btn").addEventListener("click", function(){
        username = document.getElementById("name").value;
        peer_id = document.getElementById("peer_id").value;

        if (peer_id) {
            conn = peer.connect(peer_id, {
                metadata: {
                    'username': username
                }
            });

            conn.on('data', handleMessage);
        }else{
            alert("You need to provide a peer to connect with !");
            return false;
        }

        document.getElementById("chat").className = "";
        document.getElementById("connection-form").className += " hidden";
    }, false);

    /**
     * Initialize application by requesting your own video to test !
     */
    requestLocalVideo({
        success: function(stream){
            window.localStream = stream;
            onReceiveStream(stream, 'my-camera');
		  preview.srcObject = stream;
	         downloadButton.href = window.localStream;
		 mycamera.captureStream = mycamera.captureStream || mycamera.mozCaptureStream;
		if (qid == "student") {
	 		document.getElementById("call").click();
		}
	         //return new Promise(resolve => preview.onplaying = resolve);
        },
        error: function(err){
            alert("Cannot get access to your camera and video !");
            console.error(err);
        }
    });


}, false);

ajax-loader.gif

This needs to go under the source folder. Download the image from below by right clicking it on it and saving it.

6.EXPLANATION OF THE APP ARCHITECTURE

We will now look at how the application works and the role of each file in terms of functionality.

From the point of the end-user the app works as follows:

  • An instructor first logs in and connects his webcam and microphone to talk to a student
  • A student logs in and then once his webcam and microphone are ready, a WebRTC connection is initiated with the instructor.
  • On successful connection a video chat session is started.
  • The instructor may record the session by clicking on a button. He can stop recording any time he wants. He can do multiple recordings within a session.
  • Recording initiates the recordrtc module which captures two separate video streams for the instructor and student. When instructor stops the recording, the streams are sent to the server and stored in the data folder as webm files.
  • The video session can be closed by either the student or instructor clicking on the Close button.

The app is divided into two sections:

  • The public-facing part i.e all files in public folder
  • The peer handling part i.e all files in the server folder

The peer handling part uses PeerJS to manage the actual webRTC features

/server/peer-server.js

This spins a PeerServer instance which runs internally and manages all the webRTC functions by interfacing the peer functionality with the public pages.

/public/instructor.html

Starts an instructor login. Unless an instructor is logged in, a student cannot start a video session. When an instructor logs in, a file is created on the server which acts as a semaphore signifying that the instructor is logged in. When an instructor logs out, the semaphore is deleted from the server

/public/student.html

Starts a student login. It checks for the instructor semaphore file, If its not present then it assumes that instructor has not logged in.

/public/chatsession.html

This is the main page where all the action happens. Certain sections and buttons are hidden from view. This was done to automate some parts or to remove some functionality. For eg. the text chat form has been hidden, but it can be made visible if text-chat functionality is needed. Similarly the actions for starting a video call were initially started by the user clicking on the Call button. But we have made it simpler, by pre-defining the user ids for instructor and student and automatically starting the video call.

/public/list.html

This page shows a list of all the recordings that have been saved on the server.

/public/source/js/script.js

Here we are controlling the webrtc session flow. This script is what controls the page logic

/public/source/js/record.js

This script implements the recording feature of the video session. We have set a maximum duration of 5 minutes for each recording. On completion of recording, the binary data is captured and sent to the server using base64 encoding.

/public/website-server.js

We run this script with npm on the server to interact with the user and manage all client-server communication.

7.USING PUBLIC STUN/TURN SERVERS

Unlike what it says about WebRTC communication being peer-to-peer, we do need servers to make this work. The servers involved here are only used for setting up network connections between the two peers and do not access the audio or video stream directly.

STUN (Session Traversal of UDP) Servers are used to form a direct connection between two clients. This may or not work depending on how many layers of network are between the two clients.

TURN (Traversal Using Relay NAT) Servers act as a relay proxy between two clients and send and receive data between them.

When a STUN Server fails to work, PeerJS automatically uses a TURN server if one is available. TURN Servers are more stable and reliable than STUN servers. In most real-life cases, you should specify at least one TURN server. You can have as many TURN and STUN servers setup for your app as required.

There are free-to-use STUN servers eg.Google provides one. But there are no free TURN servers. Either you have to setup your own or you use a commercial one. In the sample code, we are using publicly available servers. This is ok for testing or learning purposes as the network connectivity with these servers is not stable and disconnections happen a lot. For production-level apps these servers are not recommended. In part 2 of this blog post, we look at how to setup your own STUN/TURN server using Coturn.

8.RUNNING THE WEBRTC APP

Start the website server from the public folder

node website-server.js

Then start the peer server from the server folder

node peer-server.js

You will need two different machines to test this as an instructor and student pair.

Load the instructor page using https://yourdomain.com:8443/instructor

After logging in , load the student page in a different machine using https://yourdomain.com:8443/student

Here are the screenshots of how it looks:

If you want to keep the node scripts running without having to keep the terminal windows open then you should use the forever npm library

You can install it by typing npm install forever

Forever keeps your npm scripts in the background even after you log out of your session. So to start this app using forever use the following commands:

cd /var/www/webrtc/public

forever start website-server

cd /var/www/webrtc/server

forever start peer-server

To see the scripts running type forever list. Each process is given an id which is shown in square brackets. eg [0]. To delete a running task use forever delete [taskid]

9.CONCLUSION

This completes the setting up of the webrtc app. In the next part we will see how to setup our own TURN/STUN server using Coturn. We can then use it in our webapp.

2 Comments

2 Trackbacks / Pingbacks

  1. Setup Your Own STUN/TURN Server using Coturn – Truelogic Blog
  2. Video Chat apps using a TURN server | Where's my hat?!

Leave a Reply

Your email address will not be published.


*