Create a Peer to Peer Chat App with webRTC

Create a Peer to Peer Chat App with webRTC

In this article we will use webRTC and socket.io to create a peer to peer chat app between two clients.

The server

We need a server for signaling in order to establish a connection between the two peers. The two clients must know where is the other client.

Initialize a new node.js project:

npm init

Install express:

npm install --save express

Install socket.io for signaling:

npm install --save socket.io

Install ejs for server side rendering:

npm install --save ejs

Install nodemon for auto rerun after code change

npm install --save nodemon

Server code: server.js

This is the simplest server for signaling in order to connect max two peers and one view:


//Loading dependencies & initializing express
var os = require("os");
var express = require("express");
var app = express();
var http = require("http");
//For signalling in WebRTC
var socketIO = require("socket.io");

app.use(express.static("public"));

app.get("/", function (req, res) {
  res.render("index.ejs");
});

var server = http.createServer(app);

server.listen(process.env.PORT || 3000);

var io = socketIO(server);

io.sockets.on("connection", function (socket) {
  // Convenience function to log server messages on the client.
  // Arguments is an array like object which contains all the arguments of log().
  // To push all the arguments of log() in array, we have to use apply().
  function log() {
    var array = ["Message from server:"];
    array.push.apply(array, arguments);
    socket.emit("log", array);
  }

  //Defining Socket Connections
  socket.on("message", function (message, room) {
    log("Client said: ", message);
    // for a real app, would be room-only (not broadcast)
    socket.in(room).emit("message", message, room);
  });

  socket.on("create or join", function (room, clientName) {
    log("Received request to create or join room " + room);

    var clientsInRoom = io.sockets.adapter.rooms.get(room);

    var numClients = clientsInRoom ? clientsInRoom.size : 0;
    log("Room " + room + " now has " + numClients + " client(s)");

    if (numClients === 0) {
      socket.join(room);
      log("Client ID " + socket.id + " created room " + room);
      socket.emit("created", room, socket.id);
    } else if (numClients === 1) {
      log("Client ID " + socket.id + " joined room " + room);
      //this message ("join") will be received only by the first client since the client has not joined the room yet
      io.sockets.in(room).emit("join", room, clientName); //this client name is the name of the second client who wants to join
      socket.join(room);
      //this mesage will be received by the second client
      socket.emit("joined", room, socket.id);
      //this message will be received by two cleints after the join of the second client
      io.sockets.in(room).emit("ready");
    } else {
      // max two clients
      socket.emit("full", room);
    }
  });

  socket.on("creatorname", (room,client) => {
    // to all clients in room1 except the sender
    socket.to(room).emit("mynameis",client);
  });

  socket.on("ipaddr", function () {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function (details) {
        if (details.family === "IPv4" && details.address !== "127.0.0.1") {
          socket.emit("ipaddr", details.address);
        }
      });
    }
  });

  socket.on("bye", function () {
    console.log("received bye");
  });
});

Code explanation

  • At line 12 we use express and ejs to render a simple html page that will help us to make the UI for the file sharing.

  • At line 16 a http server is created and is set to listen to port 3000 or the port defined by a PORT environment variable at line 18

  • At line 20 a new instance of socket.io is initialized and we pass http server as a parameter.

  • At line 22 we listen at the connection event where we have defined all our other events that occur after connection.

  • function log() is a function that emits to the client who sent the message a log event and some message that is defined in the arguments as an array

Lets talk about sockets events that are defined inside our server code.

  • create or join when the server receives this event from the client initially at line 44 it gets the number of clients in room. and stores it in var clientsInRoom, if this variable is null then we define the numClients as zero otherwise we set it as the clientsInRoom. At line 49 if the clients who sends the message is the first then we emit the created event to the client otherwise at line 54 if is the second who sends this message we send join to the first client, we join the second client and a joined event is sent to the second client that just joined the room. At the end the two clients will receive ready event. If anyone else except these two clients send other create or join event he will receive a full event.

  • ipaddr event is for signaling webRTC

Now for the clients:

For the client your will need a config.js file which contains a json object for STUN/TURN server. I will not explain in details what is this for in this article but it is necessary to help two clients to exchange their public IP Addresses. The gist above is not correct for security reasons but you can create your own config object at xirsys

const turnConfig = {
    iceServers: [
      { urls: ["stun:fr-turn1.xirsys.com"] },
      {
        username:"Wj3dtFyTovJl_655q7_9Y-Uy_DTma3qU6uTZmdAqUvb0TiOcYH295GlvO4exr4KnAAAAAGGYE3dlbmVhc2xhcmk=",
        credential: "8a1494e0-497d-11ec-9fcf-0242ac120004",
        urls: [
          "turn:fr-turn1.xirsys.com:80?transport=udp",
          "turn:fr-turn1.xirsys.com:3478?transport=udp",
          "turn:fr-turn1.xirsys.com:80?transport=tcp",
          "turn:fr-turn1.xirsys.com:3478?transport=tcp",
          "turns:fr-turn1.xirsys.com:443?transport=tcp",
          "turns:fr-turn1.xirsys.com:5349?transport=tcp",
        ],
      },
    ],
  };
  

The client JavaScript:

//Defining some global utility variables
var isChannelReady = false;
var isInitiator = false;
var isStarted = false;
var pc;
var turnReady;
var datachannel;
var clientName = "user" + Math.floor(Math.random() * 1000 + 1);
var remoteclient;

document.getElementById("yourname").innerHTML="You: "+clientName

//Initialize turn/stun server here
//turnconfig will be defined in public/js/config.js
var pcConfig = turnConfig;


//setting test room
var room = "test";

//Initializing socket.io
var socket = io.connect();

//Defining socket events

//Event - Client has created the room i.e. is the first member of the room
socket.on("created", function (room) {
  console.log("Created room " + room);
  isInitiator = true;
});

//Event - Room is full
socket.on("full", function (room) {
  console.log("Room " + room + " is full");
});

//Event - Another client tries to join room
//this message is received only by the client that connected first
//when the second peer is connected
socket.on("join", function (room, client) {
  console.log("Another peer made a request to join room ", room ," whith name :",client);
  console.log("This peer is the initiator of room " + room + "!");
  sendmessagebutton.disabled = false;
  isChannelReady = true;
  remoteclient = client;
  document.getElementById("remotename").innerHTML=client
  socket.emit("creatorname", room, clientName);
});

socket.on("mynameis", (client) => {
  console.log("The creator's name is " + client);
  remoteclient = client;
  document.getElementById("remotename").innerHTML=client
});

//Event - Client has joined the room
//this message is received only by the client that connected second
socket.on("joined", function (room) {
  console.log("joined: " + room);
  isChannelReady = true;
  sendmessagebutton.disabled = false;
});

//Event - server asks to log a message
socket.on("log", function (array) {
  console.log.apply(console, array);
});

//Event - for sending meta for establishing a direct connection using WebRTC
//The Driver code
socket.on("message", function (message, room) {
  console.log("Client received message:", message, room);
  if (message === "gotuser") {
    maybeStart();
  } else if (message.type === "offer") {
    if (!isInitiator && !isStarted) {
      maybeStart();
    }
    pc.setRemoteDescription(new RTCSessionDescription(message));
    doAnswer();
  } else if (message.type === "answer" && isStarted) {
    pc.setRemoteDescription(new RTCSessionDescription(message));
  } else if (message.type === "candidate" && isStarted) {
    var candidate = new RTCIceCandidate({
      sdpMLineIndex: message.label,
      candidate: message.candidate,
    });
    pc.addIceCandidate(candidate);
  } else if (message === "bye" && isStarted) {
    handleRemoteHangup();
  }
});

//Function to send message in a room
function sendMessage(message, room) {
  console.log("Client sending message: ", message, room);
  socket.emit("message", message, room);
}

//If initiator, create the peer connection
function maybeStart() {
  console.log(">>>>>>> maybeStart() ", isStarted, isChannelReady);
  if (!isStarted && isChannelReady) {
    console.log(">>>>>> creating peer connection");
    createPeerConnection();
    isStarted = true;
    console.log("isInitiator", isInitiator);
    if (isInitiator) {
      doCall();
    }
  }
}

//Sending bye if user closes the window
window.onbeforeunload = function () {
  sendMessage("bye", room);
};
var datachannel;
//Creating peer connection
function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(pcConfig);
    pc.onicecandidate = handleIceCandidate;
    console.log("Created RTCPeerConnnection");
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
    alert("Cannot create RTCPeerConnection object.");
    return;
  }
}

//Function to handle Ice candidates generated by the browser
function handleIceCandidate(event) {
  console.log("icecandidate event: ", event);
  if (event.candidate) {
    sendMessage(
      {
        type: "candidate",
        label: event.candidate.sdpMLineIndex,
        id: event.candidate.sdpMid,
        candidate: event.candidate.candidate,
      },
      room
    );
  } else {
    console.log("End of candidates.");
  }
}

function handleCreateOfferError(event) {
  console.log("createOffer() error: ", event);
}

//Function to create offer
function doCall() {
  console.log("Sending offer to peer");
  pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}

//Function to create answer for the received offer
function doAnswer() {
  console.log("Sending answer to peer.");
  pc.createAnswer().then(
    setLocalAndSendMessage,
    onCreateSessionDescriptionError
  );
}

//Function to set description of local media
function setLocalAndSendMessage(sessionDescription) {
  pc.setLocalDescription(sessionDescription);
  console.log("setLocalAndSendMessage sending message", sessionDescription);
  sendMessage(sessionDescription, room);
}

function onCreateSessionDescriptionError(error) {
  trace("Failed to create session description: " + error.toString());
}

function hangup() {
  console.log("Hanging up.");
  stop();
  sendMessage("bye", room);
}

function handleRemoteHangup() {
  console.log("Session terminated.");
  stop();
  isInitiator = false;
}

function stop() {
  isStarted = false;
  pc.close();
  pc = null;
}

var connectbutton = document.getElementById("connectbutton");
if (connectbutton) {
  connectbutton.addEventListener("click", () => {
    if (connectbutton.innerHTML !== "Connected") {
      socket.emit("create or join", room, clientName);
      sendMessage("gotuser", room);
      if (isInitiator) {
        maybeStart();
      }
    }
    connectbutton.innerHTML = "Connected";
    //connection logic
  });
}

The view:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Webrtc messaging</title>
  </head>

  <body>
    <h1>Send messages peer to peer</h1>

    <!-- These paragraphs will be initialized when the connection has been established -->
    <p id="yourname"></p>
    <p id="remotename"></p>

    <button id="connectbutton">Connect with peer</button>

    <!-- Import SocketIO for signalling -->
    <script src="/socket.io/socket.io.js"></script>

    <!-- Import WebRTC adapter for compatibility with all the browsers  -->
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

    <script src="js/config.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>

Edit the package.json to look like this:

{
  "name": "file_sharing_webrtc",
  "version": "1.0.0",
  "description": "share files peer to peer with webrtc",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
    "dev":"nodemon server.js"

  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/tzagiollari/share_file_securely_peer_to_peer_with_WEBRTC.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/tzagiollari/share_file_securely_peer_to_peer_with_WEBRTC/issues"
  },
  "homepage": "https://github.com/tzagiollari/share_file_securely_peer_to_peer_with_WEBRTC#readme",
  "dependencies": {
    "ejs": "^3.1.6",
    "express": "^4.17.1",
    "socket.io": "^4.4.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

Run the project

npm run dev

Now open two tabs on browser and click connect in every tab , also open debug window to see logging(f12)

Image description

Now that we have made a peer to peer connection we can create a data channel so clients can send messages to each other.

We will only modify the client code:

//Defining some global utility variables
var isChannelReady = false;
var isInitiator = false;
var isStarted = false;
var pc;
var turnReady;
var datachannel;
var clientName = "user" + Math.floor(Math.random() * 1000 + 1);
var remoteclient;

document.getElementById("yourname").innerHTML="You: "+clientName

//Initialize turn/stun server here
//turnconfig will be defined in public/js/config.js
var pcConfig = turnConfig;

// Prompting for room name:
// var room = prompt('Enter room name:');
//setting test room
var room = "test";

//Initializing socket.io
var socket = io.connect();

//Ask server to add in the room if room name is provided by the user
if (room !== "") {
  // socket.emit('create or join', room);
  // console.log('Attempted to create or  join room', room);
}

//Defining socket events

//Event - Client has created the room i.e. is the first member of the room
socket.on("created", function (room) {
  console.log("Created room " + room);
  isInitiator = true;
});

//Event - Room is full
socket.on("full", function (room) {
  console.log("Room " + room + " is full");
});

//Event - Another client tries to join room
//this message is received only by the client that connected first
//when the second peer is connected
socket.on("join", function (room, client) {
  console.log(
    "Another peer made a request to join room " +
      room +
      " whith name :" +
      client
  );
  console.log("This peer is the initiator of room " + room + "!");
  sendmessagebutton.disabled = false;
  isChannelReady = true;
  remoteclient = client;
  document.getElementById("remotename").innerHTML=client
  socket.emit("creatorname", room, clientName);
});

socket.on("mynameis", (client) => {
  console.log("The creator's name is " + client);
  remoteclient = client;
  document.getElementById("remotename").innerHTML=client
});

//Event - Client has joined the room
//this message is received only by the client that connected second
socket.on("joined", function (room) {
  console.log("joined: " + room);
  isChannelReady = true;
  sendmessagebutton.disabled = false;
});

//Event - server asks to log a message
socket.on("log", function (array) {
  console.log.apply(console, array);
});

//Event - for sending meta for establishing a direct connection using WebRTC
//The Driver code
socket.on("message", function (message, room) {
  console.log("Client received message:", message, room);
  if (message === "gotuser") {
    maybeStart();
  } else if (message.type === "offer") {
    if (!isInitiator && !isStarted) {
      maybeStart();
    }
    pc.setRemoteDescription(new RTCSessionDescription(message));
    doAnswer();
  } else if (message.type === "answer" && isStarted) {
    pc.setRemoteDescription(new RTCSessionDescription(message));
  } else if (message.type === "candidate" && isStarted) {
    var candidate = new RTCIceCandidate({
      sdpMLineIndex: message.label,
      candidate: message.candidate,
    });
    pc.addIceCandidate(candidate);
  } else if (message === "bye" && isStarted) {
    handleRemoteHangup();
  }
});

//Function to send message in a room
function sendMessage(message, room) {
  console.log("Client sending message: ", message, room);
  socket.emit("message", message, room);
}

//If initiator, create the peer connection
function maybeStart() {
  console.log(">>>>>>> maybeStart() ", isStarted, isChannelReady);
  if (!isStarted && isChannelReady) {
    console.log(">>>>>> creating peer connection");
    createPeerConnection();
    isStarted = true;
    console.log("isInitiator", isInitiator);
    if (isInitiator) {
      doCall();
    }
  }
}

//Sending bye if user closes the window
window.onbeforeunload = function () {
  sendMessage("bye", room);
};
var datachannel;
//Creating peer connection
function createPeerConnection() {
  try {
    pc = new RTCPeerConnection(pcConfig);
    pc.onicecandidate = handleIceCandidate;
    console.log("Created RTCPeerConnnection");

    // Offerer side
    datachannel = pc.createDataChannel("filetransfer");
    datachannel.onopen = (event) => {
      //datachannel.send("oferer sent:THIS")
    };

    datachannel.onmessage = (event) => {
      console.log("The oferrer received a message" + event.data);
    };
    datachannel.onerror = (error) => {
      //console.log("Data Channel Error:", error);
    };

    datachannel.onclose = () => {
      //console.log("Data Channel closed");
    };

    // Answerer side
    pc.ondatachannel = function (event) {
      var channel = event.channel;
      channel.onopen = function (event) {
        channel.send("ANSWEREROPEN");
      };
      channel.onmessage = async (event) => {
        try {
          var themessage = event.data;
          console.log(themessage, event);
          viewmsgtoelement(document.getElementById("messagesent"), themessage);
        } catch (err) {
          console.log(err);
        }
      };
    };
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
    alert("Cannot create RTCPeerConnection object.");
    return;
  }
}

//Function to handle Ice candidates generated by the browser
function handleIceCandidate(event) {
  console.log("icecandidate event: ", event);
  if (event.candidate) {
    sendMessage(
      {
        type: "candidate",
        label: event.candidate.sdpMLineIndex,
        id: event.candidate.sdpMid,
        candidate: event.candidate.candidate,
      },
      room
    );
  } else {
    console.log("End of candidates.");
  }
}

function handleCreateOfferError(event) {
  console.log("createOffer() error: ", event);
}

//Function to create offer
function doCall() {
  console.log("Sending offer to peer");
  pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}

//Function to create answer for the received offer
function doAnswer() {
  console.log("Sending answer to peer.");
  pc.createAnswer().then(
    setLocalAndSendMessage,
    onCreateSessionDescriptionError
  );
}

//Function to set description of local media
function setLocalAndSendMessage(sessionDescription) {
  pc.setLocalDescription(sessionDescription);
  console.log("setLocalAndSendMessage sending message", sessionDescription);
  sendMessage(sessionDescription, room);
}

function onCreateSessionDescriptionError(error) {
  trace("Failed to create session description: " + error.toString());
}

function hangup() {
  console.log("Hanging up.");
  stop();
  sendMessage("bye", room);
}

function handleRemoteHangup() {
  console.log("Session terminated.");
  stop();
  isInitiator = false;
}

function stop() {
  isStarted = false;
  pc.close();
  pc = null;
}

var connectbutton = document.getElementById("connectbutton");
if (connectbutton) {
  connectbutton.addEventListener("click", () => {
    if (connectbutton.innerHTML !== "Connected") {
      socket.emit("create or join", room, clientName);
      sendMessage("gotuser", room);
      if (isInitiator) {
        maybeStart();
      }
    }
    connectbutton.innerHTML = "Connected";
    //connection logic
  });
}

let messagetexted = "";
//DOM elements

var messageinput = document.getElementById("messagearea");
if (messageinput) {
  //Tip: This event is similar to the onchange event.
  //The difference is that the oninput event occurs immediately
  // after the value of an element has changed, while onchange occurs
  //when the element loses focus, after the content has been changed.
  //The other difference is that the onchange event also works on <select> elements.
  messageinput.addEventListener("input", (event) => {
    console.log(event.target.value);
    messagetexted = event.target.value;
  });
}

var sendmessagebutton = document.getElementById("sendmessage");
if (sendmessagebutton) {
  sendmessagebutton.disabled = true;
  sendmessagebutton.addEventListener("click", () => {
    var themessage = "<p>" + clientName + ":" + messagetexted + "</p>";
    viewmsgtoelement(document.getElementById("messagesent"), themessage);
    datachannel.send(themessage);
    messageinput.value = "";
    messagetexted = "";
  });
}

function viewmsgtoelement(element, message) {
  element.innerHTML += "\n" + message;
}

Lets add a textarea , a send button and printout the messages in the browser

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <h1>Send messages peer to peer</h1>

    <!-- These paragraphs will be initialized when the connection has been established -->
    <p id="yourname"></p>
    <p id="remotename"></p>

    <button id="connectbutton">Connect with peer</button>
    <textarea id="messagearea" name="message" rows="5" cols="50"></textarea>
    <button id="sendmessage">Send message</button>

    <div id="messagesent"></div>

    <!-- Import SocketIO for signalling -->
    <script src="/socket.io/socket.io.js"></script>

    <!-- Import WebRTC adapter for compatibility with all the browsers  -->
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

    <script src="js/config.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>

Now run again the project with:

npm run dev

After you click connect in two browsers you see that send message buttons are being enabled

Write a message in the textarea, then click send and see the message in the other tab

Congratulation we made a super message app with some incredible UI!

Image description

Source Code

Original Post(Dev.to)

Thanks for your time.

More to read