Build a Free 1:1 Video Chat Application with Twilio WebRTC Go and JavaScript

July 14, 2021
Written by
Mia Adjei
Twilion
Reviewed by

Build a Free 1:1 Video Chat Application with Twilio WebRTC Go and JavaScript

Have you ever wished you could build your own video chat application to talk to a friend, family member, or colleague one-on-one?

There are so many projects you could build or enhance with video — a video chat for friends, a telemedicine application, a virtual real estate tour, a tutoring app, a dating application, and many more. When you're getting started building a new project, it's great to have a way to build for free and explore your ideas.

With Twilio WebRTC Go, you can build your own 1:1 video application where participants can video chat for free. You can build your own WebRTC-enabled video app without needing to be concerned with some of the challenges of building directly on WebRTC.

In this tutorial, you will learn how to build a 1:1 video chat application with JavaScript and set it up to run on ngrok so you can chat with a friend. The application will consist of two parts: a vanilla JavaScript client-side application that will allow you to connect to a video room, and an Express server that will grant access tokens for the participants in the video chat.

Let's get started!

Prerequisites

  • A free Twilio account. (If you register here, you'll receive $10 in Twilio credit when you upgrade to a paid account!)
  • Node.js (version 14.16.1 or higher) and npm installed on your machine.
  • ngrok

Create the project directory and install dependencies

To get started, navigate to where you would like to set up your project. Create a new directory called explore-webrtc-go and change into that directory by running the following commands in your terminal:

mkdir explore-webrtc-go
cd explore-webrtc-go

Next, set up a new Node.js project with a default package.json file by running the following command:

npm init --yes

Once you have your package.json file, you're ready to install the needed dependencies.


For this project, you will need the following packages:

Run the following command in your terminal to install the needed packages:

npm install express twilio dotenv node-dev

If you check your package.json file now, you'll notice that the packages above have been installed as dependencies.

Save your Twilio credentials as environment variables

Next, create a new file named .env at the root of your project and open it in your code editor. The .env file is where you will keep your Twilio account credentials. Open the .env file in your text editor and add the following variables:

TWILIO_ACCOUNT_SID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY_SID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

You’ll need to replace the placeholder text above with your actual Twilio credentials, which can be found in the Twilio Console. Log in to the Twilio Console and find your Account SID:

Twilio console, showing location of Account SID

Copy and paste the value for Account SID to replace the placeholder text for TWILIO_ACCOUNT_SID.

Then, navigate to the API Keys section of the console and generate a new API Key. Copy the API Key's values for SID and Secret to replace the placeholder text for TWILIO_API_KEY_SID and TWILIO_API_KEY_SECRET.

It’s important to keep these private credentials secure and out of version control, so create a .gitignore file at the root of your project. Here you can list the files and directories that you want git to ignore from being tracked or committed. Open .gitignore in your code editor and add the .env file. While you're in here, add the node_modules directory as well:

.env
node_modules

Now it's time to get into writing the code to bring your video chat to life.

Create an Express server

Create a new file called server.js at the root of your project. This is the place where you will write your server-side code.

Open up server.js in your code editor. Add the following lines of code to server.js, which will load the environment variables from your .env file, start a new Express application, and set the application to run on port 5000:

require('dotenv').config();
const express = require('express');
const app = express();
const port = 5000;

Below the code you added above, add the following lines of code to start the Express server:

// Start the Express server
app.listen(port, () => {
  console.log(`Express server running on port ${port}`);
});

Now, open package.json in your code editor. Inside the scripts section, add a start script as shown below. You can replace the test script that was automatically generated by npm init --yes earlier:

  "scripts": {
    "start": "node-dev server.js"
  },

To run the start script, return to your terminal window and run the following command:

npm start

Once you have done this, you should see the following log statement in your terminal window, letting you know that the Express server is running:

Express server running on port 5000

Set up an ngrok tunnel

In this project, you'll be using ngrok to connect the Express application running locally on your machine to a temporary public URL. To start a new ngrok tunnel, open up a new terminal window and run the following command:

ngrok http 5000

Once ngrok is running, you will see text like the below in your terminal window:

ngrok by @inconshreveable                                       (Ctrl+C to quit)

Session Status                online
Account                      <YOUR_ACCOUNT_NAME>
Version                       2.3.40
Region                        <YOUR_REGION>
Web Interface                 http://127.0.0.1:4040
Forwarding                    <URL> -> http://localhost:5000
Forwarding                    <URL> -> http://localhost:5000

Connections                   ttl     opn     rt1     rt5     p50     p90
                             0       0       0.00    0.00    0.00    0.00

Take a look at the URLs next to Forwarding. Now, any requests that are made to these ngrok URLs will be forwarded to your Express server.

Create the application layout and serve static files

Now that your server is running, it's time to create the rest of your application's layout. Create a new directory called public at the root of your project. This directory will hold your static files:

mkdir public

Inside the public directory, create 3 new files: app.js, index.html, and styles.css.

Open up public/index.html in your code editor and add the following code to the file:

<html>
  <head>
    <meta name='viewport' content='width=device-width, initial-scale=1.0' />
    <link rel='stylesheet' type='text/css' href='styles.css'>
    <script defer src='https://sdk.twilio.com/js/video/releases/2.15.0/twilio-video.min.js'></script>
    <script defer src='app.js' type='text/javascript'></script>
    <title>Try WebRTC Go</title>
  </head>
  <body>
    <div id='participants'>
      <div id='localParticipant' class='participant'>
        <div class='identity' id='localIdentity'></div>
      </div>
      <div id='remoteParticipant' class='participant'>
        <div class='identity' id='remoteIdentity'></div>
      </div>
    </div>
    <form id='login'>
      <input id='username' type='text' placeholder='Enter your name...' required>
      <button id='joinOrLeave'>Join Video Call</button>
    </form>
  </body>
</html>

With this HTML code, you create the layout for the video chat application, as well as include the Twilio Video library from CDN in a <script> tag. Since you are using WebRTC Go, it is a one-on-one video chat with only two participants. This means that you will only need to show two video feeds — one for the localParticipant and one for the remoteParticipant. This layout also includes a login form that will let the participants enter their names and click a button to join the video call.

Return to server.js and add the following lines of code just below the variables at the top of the file:

app.use(express.json());

// Serve static files from the public directory
app.use(express.static('public'));

app.get('/', (req, res) => {
  res.sendFile('public/index.html');
});

With this code, you have set your application to use the Express JSON middleware, serve your static files from the public directory you just created, and show your layout from index.html when a user navigates to the root endpoint of your application.

If you visit one of the ngrok forwarding URLs in your browser, you will see something that looks like the below:

Basic HTML form with input field and "Join Video Call" button. No CSS styles have been applied.

Let's add some CSS to make it a bit more interesting. Open public/styles.css in your code editor, then add the following styles to the file:

body {
  width: 80%;
  margin: 0 auto;
  display: grid;
  align-items: center;
  font-family: sans-serif;
}

#participants {
  display: flex;
  justify-content: center;
  margin-top: 2rem;
}

.participant {
  position: relative;
  border-radius: 0.625rem;
  margin: 0.625rem;
  width: 25vw;
  overflow: hidden;
  background-color: rgb(233, 230, 230);
}

.identity {
  position: absolute;
  left: 0;
  bottom: 0.9375rem;
  color: white;
  padding-left: 0.9375rem;
  z-index: 10;
}

form {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  padding: 1rem 5rem;
}

input {
  border: none;
  background: rgb(233, 230, 230);
  padding: 1rem 11.5rem;
  height: 4rem;
  font-size: 1.125rem;
  text-align: center;
}

button {
  width: 10rem;
  height: 4rem;
  background: rgb(247, 120, 184);
  color: rgb(255, 255, 255);
  border: none;
  font-size: 1rem;
}

button:hover, button:active {
  background: rgb(184, 77, 132);
}

video {
  width: 25rem;
  border-radius: 0.438rem;
  margin: 0 auto;
}

@media only screen and (max-width: 500px) {

  body {
    width: 100%;
    margin: 0 auto;
    display: grid;
    align-items: center;
    font-family: sans-serif;
  }

  #participants {
    flex-direction: column;
    margin: 1rem 0;
  }

  .participant {
    border-radius: 0;
    margin: 0 auto;
    width: 100vw;
    height: 65vw;
  }

  form {
    padding: 1rem;
  }

  input {
    padding: 0;
    width: 100%;
    height: 4rem;
  }

  button {
    width: 100%;
    font-size: 1.125rem;
  }

  video {
    width: 30rem;
    border-radius: 0;
  }
}

If you refresh your browser window, the application so far should look more like this:

HTML form, now with CSS styles applied. The input field is shaded light gray, and the button is colored pink.

Now that you have set up a structure to hold your video chat, the next step is to use JavaScript to display a preview of your video as the local participant.

Display a preview of your video

Open public/app.js in your code editor and add the following variables at the top of the file:

const participants = document.getElementById('participants');
const localParticipant = document.getElementById('localParticipant');
const localIdentity = document.getElementById('localIdentity');
const remoteParticipant = document.getElementById('remoteParticipant');
const remoteIdentity = document.getElementById('remoteIdentity');
const login = document.getElementById('login');
const usernameInput = document.getElementById('username');
const joinLeaveButton = document.getElementById('joinOrLeave');

let connected = false;
let room;

The first variables refer to specific HTML elements in your UI. Setting them to variables allows you to work with them more easily. Following these, you also have variables for connected and room. The connected variable describes whether the local participant is connected to a video room, and the room variable describes that video room.

Just below your list of variables, add the following addLocalVideo function, and then call the function, as shown in the code below:

const addLocalVideo = async () => {
  const videoTrack = await Twilio.Video.createLocalVideoTrack();
  const trackElement = videoTrack.attach();
  localParticipant.appendChild(trackElement);
};

addLocalVideo();

This function uses the Twilio Programmable Video library to create a local video track, which will be attached to the localParticipant <div>. When you navigate to the application's URL in your browser window, you should see your video preview there. (If this is your first time visiting the page, an alert message may pop up, asking if localhost:5000 can access your camera. Once you grant permission, you'll be able to see your video feed.)

Local participant div contains a video feed showing a yellow rubber duck.

Now that you have seen your video preview, you're ready to write the code that will let you join the video call.

Generate an access token and a WebRTC Go video room

In order to join a video call, you will need to generate a short-lived credential called an access token. This will happen on the server side of your application.

In addition to generating an access token, you'll also be creating the video room on the server side of the application. Using the Rooms REST API, you are able to specify what type of video room you want to create, and in this project, you'll create a Go room.

Return to server.js in your code editor and add the following variables to the file, just below the variables already in the file:

require('dotenv').config();
const express = require('express');
const app = express();
const port = 5000;
const AccessToken = require('twilio').jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;

Then, just below that code, add the following lines of code to initialize the Twilio client:

const twilioClient = require('twilio')(process.env.TWILIO_API_KEY_SID,
  process.env.TWILIO_API_KEY_SECRET, { accountSid: process.env.TWILIO_ACCOUNT_SID });

When a participant navigates to your application, enters their username in the input field, then clicks Join Video Call, their name will be passed to a /token endpoint here on the server side via a POST request. This endpoint will get the user's username and the video room's name from the request, then use your Twilio credentials and the Twilio Node Helper Library to create an access token for this user. The endpoint will also check whether the Go room exists already, and if it does not, will create a new one. Then, the endpoint will return the access token to the client side of your application. Your JavaScript code will then use this access token to connect to the video room.

It is also possible to configure your Twilio account to only use Go Rooms for your video applications. If you are interested in doing this, navigate to the Default Room Settings page in the Twilio Console. There, you can select Go Rooms as the Room Type and then click the Save button. Once you have done this, all video applications that you create will use Go Rooms by default.

In server.js, just below your app.get block, add the following /token endpoint:

app.post('/token', async (req, res) => {
  if (!req.body.identity || !req.body.room) {
    return res.status(400);
  }

  // Get the user's identity and the room name from the request
  const identity  = req.body.identity;
  const roomName  = req.body.room;

  try {
    // See if the room exists already
    const roomList = await twilioClient.video.rooms.list({uniqueName: roomName, status: 'in-progress'});

    let room;

    if (!roomList.length) {
      // Call the Twilio video API to create the new Go room
      room = await twilioClient.video.rooms.create({
        uniqueName: roomName,
        type: 'go'
      });
    } else {
      room = roomList[0];
    }

    // Create a video grant for this specific room
    const videoGrant = new VideoGrant({
      room: room.uniqueName,
    })

    // Create an access token
    const token = new AccessToken(
      process.env.TWILIO_ACCOUNT_SID,
      process.env.TWILIO_API_KEY_SID,
      process.env.TWILIO_API_KEY_SECRET,
    );

    // Add the video grant and the user's identity to the token
    token.addGrant(videoGrant);
    token.identity = identity;

    // Serialize the token to a JWT and return it to the client side
    res.send({
      token: token.toJwt()
    });

  } catch (error) {
    res.status(400).send({error});
  }
});

Connect to a video room as a local participant

Now that you are able to generate access tokens, it's time to write the code that will let you join the video chat as the local participant. Return to public/app.js in your code editor.

Just below your addLocalVideo function, create a new function called connectOrDisconnect that will handle the event when a user clicks the Join Video Call button:

const connectOrDisconnect = async (event) => {
  event.preventDefault();
  if (!connected) {
    const identity = usernameInput.value;
    joinLeaveButton.disabled = true;
    joinLeaveButton.innerHTML = 'Connecting...';

    try {
      await connect(identity);
    } catch (error) {
      console.log(error);
      alert('Failed to connect to video room.');
      joinLeaveButton.innerHTML = 'Join Video Call';
      joinLeaveButton.disabled = false;
    }
  }
  else {
    disconnect();
  }
};

This function will check the connected variable to see whether the user is already connected to a video room. If the user is not already connected, this function will get the user's username from the input field in the UI and then try to connect to the video room. If the user did not enter a name in the input field, an alert message will pop up to let them know that they must complete this step before connecting.

If a user is already connected to a video room, the connected variable will already be set to true, which will trigger calling the disconnect function, disconnecting the participant from the video room and resetting the UI.

You may have noticed that the function described above references individual functions called connect and disconnect.

The connect function mentioned above will take the user's username from the input field and send it in a request to the /token endpoint you created earlier. Once the server side sends back an access token, this token is passed into another connect function within the client-side twilio-video library linked in the first <script> tag in index.html. If the connection is successful, the local participant will be connected to the video room and this video room will be assigned to the global variable for room. Then, the function sets up event listeners to listen for when the other participant joins the video call. Once the local participant has joined the video call, the text of the Join Video Call button changes to Leave Video Call for that participant, and the field for entering a username is hidden.

Add the following connect function to public/app.js just below the connectOrDisconnect function:

const connect = async (identity) => {
  const response = await fetch('/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      'identity': identity,
      'room': 'My Video Room'
    })
  });

  const data = await response.json();
  room = await Twilio.Video.connect(data.token);
  localIdentity.innerHTML = identity;

  room.participants.forEach(participantConnected);
  room.on('participantConnected', participantConnected);
  room.on('participantDisconnected', participantDisconnected);
  connected = true;

  joinLeaveButton.innerHTML = 'Leave Video Call';
  joinLeaveButton.disabled = false;
  usernameInput.style.display = 'none';
};

Next, it's time to add the disconnect function. This function will handle disconnecting a participant from the video room when they click the Leave Video Call button. It will also remove the remote participant's video feed from the UI, set the button's text back to Join Video Call, and let the username input appear again.

Add the following disconnect function just below your code for the connect function:

const disconnect = () => {
  room.disconnect();
  connected = false;
  remoteParticipant.lastElementChild.remove();
  remoteIdentity.innerHTML = '';
  localIdentity.innerHTML = '';
  joinLeaveButton.innerHTML = 'Join Video Call';
  usernameInput.style.display = 'inline-block';
};

Now, just above where you call the addLocalVideo function at the bottom of public/app.js, add add an event listener to the login form that will listen for the submit event and call connectOrDisconnect when this event occurs:

login.addEventListener('submit', connectOrDisconnect);
addLocalVideo();

Now that you have taken care of the local participant's connection to and disconnection from the video room, it's time to add some code to handle when the other participant joins and leaves the room.

Handle the remote participant's connection and disconnection

You might have noticed in the connect function you added above, that when a participantConnected event occurs in the video room, the function participantConnected gets called. A similar thing occurs for the participantDisconnected event. You will need to add these two functions in order to handle the connection and disconnection of the remote participant.

Start by adding the participantConnected function. This function will create a new <div> for the remote participant when they connect to the room, display their username (identity), and attach their video and audio tracks to the <div>. The function also creates event handlers for responding to when the remote participant's tracks are subscribed to or unsubscribed from; if the participant turns off their audio or video feed, you want the application to be able to attach or detach these tracks as needed.

Add the following participantConnected function just below your disconnect function in public/app.js:

const participantConnected = (participant) => {
  const tracksDiv = document.createElement('div');
  tracksDiv.setAttribute('id', participant.sid);
  remoteParticipant.appendChild(tracksDiv);
  remoteIdentity.innerHTML = participant.identity;

  participant.tracks.forEach(publication => {
    if (publication.isSubscribed) {
      trackSubscribed(tracksDiv, publication.track);
    }
  });

  participant.on('trackSubscribed', track => trackSubscribed(tracksDiv, track));
  participant.on('trackUnsubscribed', trackUnsubscribed);
};

Next, it's time to add the participantDisconnected function for when the remote participant leaves the video call. This function will remove the remote participant's video feed and username from the UI.

Add the following participantDisconnected function just below your participantConnected function:

const participantDisconnected = (participant) => {
  document.getElementById(participant.sid).remove();
  remoteIdentity.innerHTML = '';
};

Next, it's time to add code for when the local participant subscribes to or unsubscribes from a remote participant's audio or video tracks. Add the following trackSubscribed and trackUnsubscribed functions to public/app.js just below your code for participantDisconnected:

const trackSubscribed = (div, track) => {
  const trackElement = track.attach();
  div.appendChild(trackElement);
};

const trackUnsubscribed = (track) => {
  track.detach().forEach(element => {
    element.remove()
  });
};

Now you have all the code you need for this project! It's time to test everything out.

Testing your video chat application

Return to your browser window pointing to one of the ngrok forwarding URLs from earlier in the tutorial. If you refresh the page, you should see your own video feed preview there. Enter your name in the input field and click the Join Video Call button. You will see the input field disappear and the text of the button change to say Leave Video Call. You will also see your name just below your video feed.

To test out chatting with a remote participant, navigate to the ngrok forwarding URL link in a second browser window or even from a different device. Connect to the video room with a different username than the one you used in the previous steps. Once you have connected to the video room, you will be able to video chat!

Video chat between Ducky and Another Ducky

What's next for your free 1:1 video chat with WebRTC Go?

You've just built a cool video chat application that you can use for free 1:1 video conversations! There are so many ways you could use an application like this. Maybe you're interested in using the 1:1 chat for a tutoring session, a quick business call, or just to catch up with a friend. If you're interested in learning even more about WebRTC Go, consider taking a look at this blog post next.

If you would like to see the code for this project in its entirety, check out the repository on GitHub here.

What will you build next? Perhaps you're interested in allowing video chat participants to mute and unmute their audio? Or maybe you want to learn how to add virtual backgrounds to your video chat? There are so many exciting things you can create. I can't wait to see what you build!

Mia Adjei is a Software Developer on the Developer Voices team. They love to help developers build out new project ideas and discover aha moments. Mia can be reached at madjei [at] twilio.com.