DEV Community

Cover image for Create simple voice chat app with nodejs
Hossein Mobarakian
Hossein Mobarakian

Posted on

Create simple voice chat app with nodejs

Hi, I'm Hossein in this article we will build a simple voice chat web application with nodejs and socketIo.
In the first step, we will create a simple interface for our app. To did that we use handlebars.
Before starting coding we must install dependencies, run commands below:
npm init -y
npm i express socket.io express-handlebars

After installing dependencies, create and open index.js file and put codes below into it:

const express = require("express");
const app = express();
const handlebars = require("express-handlebars");
const http = require("http").Server(app);
const io = require("socket.io")(http);

//To holding users information 
const socketsStatus = {};

//config and set handlebars to express
const customHandlebars = handlebars.create({ layoutsDir: "./views" });

app.engine("handlebars", customHandlebars.engine);
app.set("view engine", "handlebars");

//enable user access to public folder 
app.use("/files", express.static("public"));

app.get("/home" , (req , res)=>{
    res.render("index");
});

http.listen(3000, () => {
  console.log("the app is run in port 3000!");
});

Enter fullscreen mode Exit fullscreen mode

Now we jump into handlebars files first create main.handlebars in views directory:

<!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>Hossein Mobarakian - voice chat application</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.1.2/socket.io.js" integrity="sha512-iZIBSs+gDyTH0ZhUem9eQ1t4DcEn2B9lHxfRMeGQhyNdSUz+rb+5A3ummX6DQTOIs1XK0gOteOg/LPtSo9VJ+w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
    {{{body}}}

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Note: main.handlebars is the base template in handlebars and you can change it in config your handlebars setting.

In this step we can go to create index.handlebars in the views directory.

<header>
    <div class="user-controller">
        <p id="username-label"></p>
        <div id="username-div">
            <input type="text" id="username">
            <button class="username-btn" onclick="changeUsername()">Change username</button>

        </div>
    </div>

    <div class="controller">
        <button class="control-btn disable-btn" onclick="toggleMicrophone(this)">Open microphone</button>
        <button class="control-btn disable-btn" onclick="toggleMute(this)">Mute</button>
        <button class="control-btn disable-btn" onclick="toggleConnection(this)">Go online</button>
    </div>

</header>
<h2>users list</h2>
<ul class="users" id="users">


</ul>

<script src="/files/js/index.js"></script>
<link rel="stylesheet" href="/files/css/index.css">
Enter fullscreen mode Exit fullscreen mode

Add some style to our interface with create index.css in /public/css/ folder.

html , body {
    width: 100%;
    height: 100%;
    overflow: hidden;
}
.controller{
    margin: 0;
    padding: 0;
    overflow: hidden;
    display: flex;
    justify-content: center;

}
body{
    display: flex;
    text-align: center;
    flex-flow: column;
    margin: 0;
    padding: 0;
    background-color: rgb(12 11 25);
    color: #fff;
}
header{
    margin:0;
    padding: 20px 0;
    width: 100%;
    height: fit-content;
    background-color: rgb(15, 15, 44);
    color: #fff;

}
.control-btn{
    width: 120px;
    padding: 10px 0;
    border: none;
    border-radius: 8px;
    cursor: pointer;
}
.enable-btn{
    background-color: rgb(26, 184, 26);
    color: #fff;
    border-bottom: 5px solid rgb(18, 131, 18);
    margin: 10px ;
}

.enable-btn:hover{
    border-bottom: none;
    margin-top: 15px;
}
.disable-btn{
    margin: 10px ;
    background-color: rgb(172, 25, 25);
    color: #fff;
    border-bottom: 5px solid rgb(184, 57, 57);
}
.disable-btn:hover{
    border-bottom: none;
    margin-top: 15px;
}
.username-btn{
    width: 200px;
    margin: 10px auto;
    padding: 10px 0;
}
input{
    width: 200px;
    padding: 10px;
    margin: 10px auto;
}

#username-div{
    display: none;
}
#username-label{
    width: 200px;
    height: fit-content;
    margin: 0 auto;
    padding: 10px 20px;
    background-color: rgb(12 11 25);
    border-radius: 8px;
    border: 2px solid rgb(26, 26, 77);
    cursor: pointer;
}
ul.users{
    width: 100%;
    margin: 0;
    padding: 0;
}
ul.users li{
    width: 90%;
    margin: 10px auto;
    padding: 10px 0;
    text-align: center;
    background-color: rgb(15 15 44);
    list-style: none;
    color: #fff;
    border-radius: 8px;
}

Enter fullscreen mode Exit fullscreen mode

Alt Text

In the last part of this project, we use socket to make our app in realtime.Now put socket codes into index.js above the http.listen(...):


io.on("connection", function (socket) {
  const socketId = socket.id;
  socketsStatus[socket.id] = {};


  console.log("connect");

  socket.on("voice", function (data) {

    var newData = data.split(";");
    newData[0] = "data:audio/ogg;";
    newData = newData[0] + newData[1];

    for (const id in socketsStatus) {

      if (id != socketId && !socketsStatus[id].mute && socketsStatus[id].online)
        socket.broadcast.to(id).emit("send", newData);
    }

  });

  socket.on("userInformation", function (data) {
    socketsStatus[socketId] = data;

    io.sockets.emit("usersUpdate",socketsStatus);
  });


  socket.on("disconnect", function () {
    delete socketsStatus[socketId];
  });

});
Enter fullscreen mode Exit fullscreen mode

After that create a front-end javascript file in /public/js/index.js and put codes below into it:

const userStatus = {
  microphone: false,
  mute: false,
  username: "user#" + Math.floor(Math.random() * 999999),
  online: false,
};

const usernameInput = document.getElementById("username");
const usernameLabel = document.getElementById("username-label");
const usernameDiv = document.getElementById("username-div");
const usersDiv = document.getElementById("users");

usernameInput.value = userStatus.username;
usernameLabel.innerText = userStatus.username;


window.onload = (e) => {
  mainFunction(1000);
};

var socket = io("ws://localhost:3000");
socket.emit("userInformation", userStatus);


function mainFunction(time) {


  navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
    var madiaRecorder = new MediaRecorder(stream);
    madiaRecorder.start();

    var audioChunks = [];

    madiaRecorder.addEventListener("dataavailable", function (event) {
      audioChunks.push(event.data);
    });

    madiaRecorder.addEventListener("stop", function () {
      var audioBlob = new Blob(audioChunks);

      audioChunks = [];

      var fileReader = new FileReader();
      fileReader.readAsDataURL(audioBlob);
      fileReader.onloadend = function () {
        if (!userStatus.microphone || !userStatus.online) return;

        var base64String = fileReader.result;
        socket.emit("voice", base64String);

      };

      madiaRecorder.start();


      setTimeout(function () {
        madiaRecorder.stop();
      }, time);
    });

    setTimeout(function () {
      madiaRecorder.stop();
    }, time);
  });


  socket.on("send", function (data) {
    var audio = new Audio(data);
    audio.play();
  });

  socket.on("usersUpdate", function (data) {
    usersDiv.innerHTML = '';
    for (const key in data) {
      if (!Object.hasOwnProperty.call(data, key)) continue;

      const element = data[key];
      const li = document.createElement("li");
      li.innerText = element.username;
      usersDiv.append(li);

    }
  });

}

usernameLabel.onclick = function () {
  usernameDiv.style.display = "block";
  usernameLabel.style.display = "none";
}

function changeUsername() {
  userStatus.username = usernameInput.value;
  usernameLabel.innerText = userStatus.username;
  usernameDiv.style.display = "none";
  usernameLabel.style.display = "block";
  emitUserInformation();
}

function toggleConnection(e) {
  userStatus.online = !userStatus.online;

  editButtonClass(e, userStatus.online);
  emitUserInformation();
}

function toggleMute(e) {
  userStatus.mute = !userStatus.mute;

  editButtonClass(e, userStatus.mute);
  emitUserInformation();
}

function toggleMicrophone(e) {
  userStatus.microphone = !userStatus.microphone;
  editButtonClass(e, userStatus.microphone);
  emitUserInformation();
}


function editButtonClass(target, bool) {
  const classList = target.classList;
  classList.remove("enable-btn");
  classList.remove("disable-btn");

  if (bool)
    return classList.add("enable-btn");

  classList.add("disable-btn");
}

function emitUserInformation() {
  socket.emit("userInformation", userStatus);
}


Enter fullscreen mode Exit fullscreen mode

run command:
node index.js
Congratulation! Now you have a realtime voice chat app created with nodejs and socketIo.I hope useful this article to you and thank you to read it.

Top comments (14)

Collapse
 
airplanegobrr profile image
AirplaneGoBrr • Edited

OK I would like to add a few suggestions

  1. use newData.join("") for the server or you could just do "data:audio/ogg;"+data and that would work as well
  2. Check if navigator AND navigator.mediaDevices exists ( Just a simple if check )
  3. Post the code on Github
  4. Tell the client what other clients status are EX if someone is muted or their mic is off

But thank you very much for all the code! It's very helpful! However the 1 second delay is kindaa... ehh but I tried to change it to <1000ms and it didn't seam to work without cutting out sadly

Collapse
 
judgegodwins profile image
Judgegodwins

Is there a GitHub repo for this?

Collapse
 
hosseinmobarakian profile image
Hossein Mobarakian

No

Collapse
 
akash4919 profile image
Akash4919

Friend I am developing a backend server of a game where players can communicate with each other with real time audio and chat message can you help me a little with it......Thankyou and your this code helped me alot

Collapse
 
itsfuad profile image
Fuad Hasan

Would be nice if there was a running demo app.

Collapse
 
lylest profile image
wackyizzy

looking forward to try this

Collapse
 
itsfuad profile image
Fuad Hasan

Someone upload the structured code to github. I've become too lazy to structure this code🙃

Collapse
 
buglessir profile image
Mohammad

If you upload your codes on somewhere like GitHub, it will help others to use it more easily

Collapse
 
perrut profile image
Matheus Perrut

Thank you for that!!!

Collapse
 
robbelroot profile image
Robert S.

Hmm i'm getting a data.split error, that i cant read split of null, so data seems to be null.

Collapse
 
itsfuad profile image
Fuad Hasan

use default values like

data ||= [];
Enter fullscreen mode Exit fullscreen mode
Collapse
 
harry_moon_96c23379521e3a profile image
Harry Moon

Is anyone here successfully installed this code. Can you share?