Simple WebRTC text chat

Emad Elsaid
7 min readJul 15, 2020

--

Recently I have been interested in peer 2 peer networks, and I learned that browsers are now capable of connecting to each other without middle servers with javascript, WebRTC is the standard that every browser implements to allow that.

The following paragraphs will walk you through the steps to implement a simple peer2peer chat, the advantage here is that your messages won’t go through a server but directly to the other user.

To start lets create a new directory for our little project

mkdir chat
cd chat

so we’ll start with an empty HTML page (index.html)

<html>
<body>
<script type="application/javascript" src="script.js" ></script>
</body>
</html>

and a javascript file (script.js)

alert("hello");

Now we need to run http server in chat directory, so lets use a simple ruby script to do that, create a file server.rb that starts a server as follows

require 'webrick'
WEBrick::HTTPServer.new(:Port => ENV.fetch("PORT", 3000), :DocumentRoot => Dir.pwd).start

This starts a server on port 3000 so run ruby server.rb and lets visit http://localhost:3000 , you should see the following

lets keep the developers console open to spot any errors

Lets connect to the Signaling server

For signaling we’re going to use the public instance of Inbox an open source signaling server that I created and running it for the public for free on https://connect.inboxgo.org/inbox

Lets create two functions, one for getting new messages and another to send a message to a specific peer

script.js should look like that

const server = "https://connect.inboxgo.org/inbox";async function SignalReceive(username, password) {
const headers = new Headers({
Authorization: 'Basic ' + window.btoa(username + ":" + password)
});
const response = await fetch(server, {
method: 'GET',
cache: 'no-cache',
headers: headers
});
try {
var data = await response.json();
console.log('Received', data);
return data;
} catch(e) {
return null;
}
}
async function SignalSend(username, password, to, message) {
console.log('Sending', message);
const headers = new Headers({
Authorization: 'Basic ' + window.btoa(username + ":" + password)
});
const response = await fetch(`${server}?to=${to}`, {
method: 'POST',
cache: 'no-cache',
headers: headers,
body: JSON.stringify(message)
});
}

These two functions will allow us to choose a username and a password then send a message to another user an receive messages from any user.

Lets try these functions in developer console

in the developer console we used SignalReceive to create 2 users and wait for a message, then send a message from one user to the other.

Now we build the user interface to allow the user to choose his username and password and listen for messages.

lets change the index.html to have 2 inputs and a button

<html>
<body>
<form id="userform">
<label>Username</label>
<input id="username" type="text">
<label>Password</label>
<input id="password" type="password">
<button onclick="CreateUser()" type="button">Create</button>
</form>
<script type="application/javascript" src="script.js" ></script>
</body>
</html>

Now we should see username and password inputs and a button to create a user

Lets create the CreateUser function to get the username and password and use them to poll for new messages

So add this CreateUser function to script.js

async function CreateUser() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
document.getElementById("userform").style.display = "none";
while ( true ) {
let message = await SignalReceive(username, password);
}
}

it gets the values from the form then hides it and start polling for new message indefinitely for now (in the future we should stop when we connect to the other peer)

so when you refresh the page and fill in a username and a password then click the button you should see this in the network tab

The request will wait for a message or until it timeout, if it timeout the loop will fail because we didn’t handle the error, so lets wrap the line that gets the signal message in a try block to retry.

async function CreateUser() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
document.getElementById("userform").style.display = "none";
while ( true ) {
try {
let message = await SignalReceive(username, password);
} catch (e) {
console.log("didn't receive a message, retrying...")
}
}
}

Side note: the user inbox will be saved on the server for one minute from your last request for a message.

Lets Start our WebRTC adventure

First we need to start a webrtc peer connection, We’ll use simple peer to simplify our code here so lets include it in our index.html from a CDN and add change our createUser function to use it and send/receive from the inbox server.

Also first you need to specify who you’re trying to initiate connection to, to do that add a new input field to our form.

<label>Other peer Username (only if your the initiator)</label>
<input id="to" type="text">

And this is the createUser function after adding simple peer logic.

async function CreateUser() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
let to = document.getElementById("to").value;
let poll = true;
document.getElementById("userform").style.display = "none";
var peer = new SimplePeer({ initiator: (to.length > 0) }) peer.on('signal', data => {
if ( to.length > 0 ) SignalSend(username, password, to, data);
})
peer.on('connect', () => {
poll = false;
peer.send('Hello ' + to);
});
peer.on('data', data => {
console.log('got a message from the other peer: ' + data);
});
peer.on('error', err => console.log('error', err)); while ( poll ) {
try {
let message = await SignalReceive(username, password);
to = message.from;
peer.signal(message.data);
} catch (e) {
console.log("didn't receive a message or an error, retrying...", e);
}
}
}

This will decide if you’re the initiator based on the to field, if you set it then you know who to connect to and you’re the initiator, then when we get a message on the other peer side we’ll set the to variable to complete connection and use to to send messages to the other side.

Also when we’re connected we stop polling and we’ll send a message to the other side.

When we receive a message we’ll log it to the console, to try this logic you need 2 tabs:

  • The first tab enter username and password and leave the other peer username empty
  • The second tab enter another username and a password and enter the first username
  • You should start seeing messages send and received in the console
  • When the connection is established you should see a message from the other peer saying Hello username

Now we need to have a form to send an receive and display chat, lets add this to our index.html

<div id="log"></div>
<p>
<input type="text" id="message" />
<button type="button" onclick="sendMessage()" >Send</button>
</p>

And define a sendMessage function in script.js , make sure you move peer outside of CreateUser to be able to use it in sendMessage

function sendMessage() {
const message = document.getElementById("message").value;
peer.send(message);
}

This is how it looks like when you use it in the console

we need to add the received message to the log div instead of logging it to the console, so we should improve the block that received the data from the other peer and append to the log div instead.

peer.on('data', data => {
var p = document.createElement("p");
p.innerText = data;
document.getElementById("log").appendChild(p);
});

When you test it now it should look like that

I will leave you now to improve on it

  • Clear the input field after pressing the button
  • Add your message to the log when you send it
  • Add the user name in front of the message
  • Now the receiver peer will accept chat from anyone, you can limit it to an expected peer name

Couple things to notice:

  • Your application doesn’t need a server, you can host it on your machine and it still works even between two machines in different networks
  • You didn’t need to write the signaling server as we used Inbox for that this is one of my open source projects that I also serve it for the public, so you can use it for your projects
  • You didn’t have to use the browser RTCPeerConnection as SimplePeer abstract it in a nice way.

I will leave this code in a gist as it’s two files it doesn’t really need to be a project.

https://gist.github.com/emad-elsaid/91fd7e303215bc6ccbd132f840315a8f

--

--

Emad Elsaid

A ruby developer, Fantasy/Sci-fi addict, works for @twitter