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

September 02, 2021
Written by
Twilion
Reviewed by
Twilion
Twilion

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

This article is for reference only. We're not onboarding new customers to Programmable Video. Existing customers can continue to use the product until December 5, 2024.


We recommend migrating your application to the API provided by our preferred video partner, Zoom. We've prepared this migration guide to assist you in minimizing any service disruption.

Have you ever wished you could build a video chat application to talk to a friend, family member, or colleague one-on-one? With Twilio WebRTC Go, you can build your own video application where two participants can video chat for free.

In this tutorial, you will learn how to build a 1:1 video chat application with Ruby and 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 a Ruby/Sinatra server that will grant Access Tokens for the participants in the video chat.

If you would like to skip ahead and take a look at the code, check out the project repository on GitHub here.

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!)
  • Ruby version 3.0.0+
  • Bundler for installing dependencies
  • ngrok

Create the project directory and install dependencies

To get started, create a new directory called explore-webrtc-go-ruby and change into that directory by running the following commands in your terminal:

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

Next, use Bundler to initialize a new application in this directory by running the following command:

bundle init

This will create a new Gemfile in your project directory.

For this project, you will need the following gems:

  • Sinatra, the Ruby web framework you will use to build this project
  • sinatra-contrib, for some helpful extensions
  • Puma, a Ruby web server
  • twilio-ruby, the helper library for interacting with the Twilio APIs
  • dotenv, to load the environment variables from your .env file into your application
  • rack-contrib, which includes a useful JSON body parser    

Add these gems to your project by running the following command in your terminal window:

bundle add sinatra sinatra-contrib puma twilio-ruby dotenv rack-contrib

If you take a look at the Gemfile now, you will see that the 6 gems have been added and installed. (To see details about the versions of the gems used in this project, you can view the Gemfile here.)

Save your Twilio credentials safely 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. Add the following variables to your new file:

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. If you’re using GitHub, 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:

.env

Now it's time to write the code to bring your video chat to life.

Create a Sinatra server and serve static files

Create two new files in your project directory: one called server.rb and the other called config.ru. Open server.rb in your code editor and add the following code to load requirements:

require 'sinatra'
require 'sinatra/reloader'
require 'sinatra/json'
require 'rack/contrib'
require 'twilio-ruby'

Next, create an application called VideoApp by adding the following code to server.rb:

class VideoApp < Sinatra::Base
  use Rack::JSONBodyParser

  # Reload the server when you change code in this file
  configure :development do
    register Sinatra::Reloader
  end

  get '/' do
    send_file File.join(settings.public_folder, 'index.html')
  end
end

With the code above, you are able to serve a static HTML file from a public folder. Static files are served by default from a directory named public. When a user navigates to the root endpoint of your application, the layout from public/index.html is what they will see.

Create the public directory now by running the following command in your terminal from the root directory of the project:

mkdir public

Inside this folder, create and open a new file called index.html, then paste in the following code:

<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.16.0/twilio-video.min.js'></script>
    <script defer src='app.js' type='text/javascript'></script>
    <title>Explore 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 a layout for the video chat application, including the Twilio Video library from CDN in a <script> tag. Since you are using WebRTC Go for this project, you are creating 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.

In addition to the HTML file, create and open a new file, public/styles.css. Then, copy the CSS from this repository and paste it into your file.

Now that you have some HTML and CSS, open config.ru in your code editor and add the following code to the file:

require 'dotenv'
Dotenv.load

require './server'
configure { set :server, :puma }
run VideoApp

This configuration will load the environment variables from your .env file and run the video application using the Puma server.

To start the application on port 5000, run the following command in your terminal:

bundle exec rackup -p 5000

You will see a log statement in your terminal similar to the below once the server is running:

Puma starting in single mode...
* Puma version: 5.4.0 (ruby 3.0.0-p0) ("Super Flight")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 1869
* Listening on http://127.0.0.1:5000
* Listening on http://[::1]:5000
Use Ctrl-C to stop

Set up an ngrok tunnel

In this project, you'll be using ngrok to connect the Sinatra 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 similar to the below in your terminal window:


ngrok by @inconshreveable                                                                                                             (Ctrl+C to quit)

Version                       2.3.40
Region                        <YOUR_REGION>
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://XXXXXXXXXXX.ngrok.io -> http://localhost:5000
Forwarding                    https://XXXXXXXXXXX.ngrok.io -> 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 local server.

Open the https:// ngrok forwarding URL in a browser window, and you will see a form like the one below:

Input field and button for joining a video call.

Now that you have set up a structure to hold your video chat, it's time to use JavaScript to display your video feed preview in the browser.

Display a preview of your video

Create and open a new file called public/app.js. Inside this file, add the following lines of code to set up the variables you will need for this part of the project:

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, paste in 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>. If you refresh the page in your browser window, the browser will ask for your permission to turn on the camera. Once you grant permission, you will be able to see your video feed preview.

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

Generate an access token and a WebRTC Go video room

Now that you have a video preview set up, it's time to write the code that will let you join the video call. To join a video call, you will need a short-lived credential called an Access Token. In addition to generating the Access Token, you'll also be creating the video room on the server side using the Rooms REST API so you can specify that you would like to create a go room.

In server.rb, add the following /token endpoint to the VideoApp class, just below the entry endpoint but before the final end statement:

  post '/token' do
    # Get the user's identity and the room name from the request
    @identity = params['identity']
    @room_name = params['room']

    # Handle error if no identity was passed into the request
    json status: 400, error: 'No identity in request' if @identity.nil?

    twilio_account_sid = ENV['TWILIO_ACCOUNT_SID']
    twilio_api_key_sid = ENV['TWILIO_API_KEY_SID']
    twilio_api_key_secret = ENV['TWILIO_API_KEY_SECRET']

    # Set up the Twilio Client
    @client = Twilio::REST::Client.new twilio_api_key_sid, twilio_api_key_secret

    # Check whether the room you want to create exists already
    room_list = @client.video.rooms.list(unique_name: @room_name)
    room = room_list.find { |room| room.unique_name == @room_name }

    # If the room doesn't exist already, create it
    if room.nil?
      room = @client.video.rooms.create(
        type: 'go',
        unique_name: @room_name
      )
    end

    # Create an access token
    token = Twilio::JWT::AccessToken.new(twilio_account_sid, twilio_api_key_sid, twilio_api_key_secret, [], identity: @identity);

    # Create Video grant for your token
    grant = Twilio::JWT::AccessToken::VideoGrant.new
    grant.room = @room_name
    token.add_grant(grant)

    # Generate and return the token as a JSON response
    json status: 200, token: token.to_jwt
  end

When a participant enters their name in the application's input field and clicks the Join Video Call button, their name and the video room name will be passed to the /token endpoint via a POST request. The endpoint will then use your Twilio credentials and the Twilio Ruby helper library to create an access token for this user and a Go video room that this token will be associated with. Then the token will be returned to the client side of your application. The JavaScript code you will write in the next step will use this 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.

Let's move on to these JavaScript steps next.

Connect to a video room as a local participant

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

Just below your addLocalVideo function definition, 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 identity from the username 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 that don’t exist yet.

The connect function mentioned above will take the user's identity 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 && 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.

Connect a remote participant to the video room

You may have noticed, in the connect function you added before, 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  creates a new <div> for each remote participant when they connect to the room, displays their username (identity), and attaches their video and audio tracks to the <div>. The function also creates event listeners for when these remote participants publish or unpublish their tracks — if someone starts or stops sharing their media, your application can attach or detach these tracks from the UI 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);
};

Now that you have code for participantConnected(), add the following participantDisconnected() function just below that. This function removes the remote participant's video feed and username from the UI:

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

Next, you will need 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 the ngrok forwarding URL from earlier in the tutorial. Hard refresh the page to request the updated HTML and JS files, and then you will 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 from a different device. You may even want to share the link with a friend. Connect to the video room with a different username than the one you used in the previous steps. Once you have both 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. Take a look at this WebRTC Go overview post if you would like to learn even more about what you can do with Go rooms.

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.