Add a Raise Hand Feature to Your Video Application

May 23, 2022
Written by
Mia Adjei
Twilion
Reviewed by

Add a Raise Hand Feature to Your Video Application

If you have participated in a video chat before, it's likely that you have seen or heard of the raise hand feature. This feature allows participants to raise their hand during the call to let the host know that they need something or would like to come off mute and speak aloud. Wouldn't it be cool if your video application had this feature as well?

In this tutorial, you will learn how to add a raise hand feature to a Twilio Video application using the DataTrack API, which allows participants to send low-latency messages to the other people on the call. With the click of a button, you will be able to raise or lower your hand, and this raised hand will show in the UI.

Let's get started!

Prerequisites

Setup

This tutorial will add a new feature on top of an already-built video application. The starter project is built using Twilio Video, Twilio Functions, and vanilla JavaScript.

To get started, clone the starter video application by running the following command on your command line:

git clone https://github.com/adjeim/video-chat-serverless.git twilio-video-raise-hand
cd twilio-video-raise-hand

Copy the placeholder values in .env.template into a new file called .env. Then, replace the placeholder text with the values for your Twilio Account SID, Auth Token, API Key SID, and API Key Secret. You can find or generate these values in the Twilio Console.

Once you have completed this step, run the following command from the root of the project to install the necessary dependencies:

npm install

Start the application by running the following command:

npm start

Once the application is running, you will be able to see it by navigating to http://localhost:3000/ in your browser.

Video application running in browser window

Now you are ready to build the raise hand feature.

Add a button to the HTML and update the CSS

Let's start by updating the HTML to include a button that will allow a participant to raise their hand.

You may have noticed that there are two files named index.html — one inside the assets directory, and the other nested inside assets/assets. This is because Twilio Runtime has an interesting way of hosting root assets. Keep this in mind when developing your project and make sure to update the code in both places.

Open assets/index.html and assets/assets/index.html in your code editor. The code is the same in both files. Here, you will see the basic structure of the application's interface, which currently consists of a gallery where the participants' video feeds will appear, a controls section where the participants will be able to join or leave the video chat, and a status section where any status messages will appear.

Inside the controls section, just below the other buttons, add a new button that participants will click to raise their hand:

        <button id='button-join'>Join Room</button>
        <button id='button-leave' disabled>Leave Room</button>
        <button id='button-raise-hand' disabled>Raise Hand ✋</button>

Just like the Leave Room button, the Raise Hand button will be disabled before the participant joins a video room. (After all, no one else will see it if you raise your hand before even joining the video call!)

Next, open assets/styles.css and add the following CSS code at the bottom of the file:

button.raised {
  background-color: rgb(145, 237, 145);
}

div.raised {
  position: relative;
}

div.raised::before {
  position: absolute;
  top: 25px;
  left: 25px;
  z-index: 2;
  content: "✋";
  transform: scale(2);
}

These styles will allow the UI to change when someone raises their hand. The Raise Hand button will change color, and a raised hand emoji (✋) will appear in the top left corner of the video feed for the person who raised their hand.

Add raise hand logic to JavaScript code

Next, open assets/app.js. Here, we will update the JavaScript code with logic to make the raise hand feature actually work.

Just below your constants for the other buttons in the video application's HTML, add a new constant for the Raise Hand button, as well as for the local data track and the div where the local participant's video feed will appear:

// buttons
const joinRoomButton = document.getElementById('button-join');
const leaveRoomButton = document.getElementById('button-leave');
const raiseHandButton = document.getElementById('button-raise-hand');

const ROOM_NAME = 'my-video-chat';
let videoRoom;
let localDataTrack;
let localVideoDiv;

Since we are now referring to the localVideoDiv in the global scope, we can refactor the addLocalVideo() function to assign the new div to this variable. Update addLocalVideo() to remove the const declaration:

const addLocalVideo = async () =>  {
  const videoTrack = await Twilio.Video.createLocalVideoTrack();
  localVideoDiv = document.createElement('div');
  localVideoDiv.classList.add('participant', 'localParticipant');

  const trackElement = videoTrack.attach();
  localVideoDiv.appendChild(trackElement);
  gallery.appendChild(localVideoDiv);
  leaveRoomButton.disabled = true;
};

Next, scroll down to the joinRoom() function. This is the function that connects a participant to the video call. Right now, this function only sets up the video and audio tracks. To send data about whether or not someone is raising their hand, you also will need to connect the data track.

Inside the joinRoom() function, update the portion of the code that creates the tracks, adding the data track as well:

    // Creates the audio, video, and data tracks
    localDataTrack = new Twilio.Video.LocalDataTrack();
    const localTracks = [... await Twilio.Video.createLocalTracks(), localDataTrack];

    videoRoom = await Twilio.Video.connect(data.token, {
      name: ROOM_NAME,
      tracks: localTracks
    });

In the same function, add another line to make the raiseHandButton active once a participant has successfully joined the video call:

    leaveRoomButton.disabled = false;
    raiseHandButton.disabled = false;

When a participant leaves the video call, you'll also want to disable the Raise Hand button, so add a line to the leaveRoom() function to accomplish this:

  joinRoomButton.disabled = false;
  leaveRoomButton.disabled = true;
  identityInput.disabled = false;
  raiseHandButton.disabled = true;

Next, it's time to add the code that actually controls raising and lowering your hand in the video call. We'll add functions to check whether a hand is raised already, and then functions to raise and lower the hand.

Just below the participantDisconnected() function, add the following functions for handIsRaised(), raiseHand() and lowerHand():

const handIsRaised = () => {
  if (raiseHandButton.classList.contains('raised') && localVideoDiv.classList.contains('raised')) {
    return true;
  }
  return false;
}

const raiseHand = async () => {
  // Show a raised hand in the UI for the local participant, then 
  // send the raised hand notification via data track to the others on the call
  raiseHandButton.classList.add('raised');
  localVideoDiv.classList.add('raised');

  localDataTrack.send(
    JSON.stringify({
      sid: videoRoom.localParticipant.sid,
      raised: true
    }
  ));
}

const lowerHand = async () => {
  raiseHandButton.classList.remove('raised');
  localVideoDiv.classList.remove('raised');

  localDataTrack.send(
    JSON.stringify({
      sid: videoRoom.localParticipant.sid,
      raised: false
    }
  ));
}

The handIsRaised() function is helpful because it will check whether the raised class has already been added to the raiseHandButton and localVideoDiv.

The raiseHand() function will add the raised class to the div displaying the video feed for the local participant who has clicked the Raise Hand button. This will allow them to see the raised hand emoji appear locally. Then, the function sends a message via the localDataTrack, letting other participants on the call know who has raised their hand, so the raised hand emoji can appear for them as well.

The lowerHand() function is very similar to the raiseHand() function, except that it removes the raised class and sends a data track message to let the other participants know someone has lowered their hand.

If a participant leaves a video call while their hand is raised, we want to make sure to lower their hand. In the leaveRoom() function, just above the code that changes the buttons' status, call lowerHand() when someone leaves:

…
  lowerHand();

  joinRoomButton.disabled = false;
  leaveRoomButton.disabled = true;
  identityInput.disabled = false;
  raiseHandButton.disabled = true;

Now that you have the code to raise and lower your hand, we can connect this code to the button element by adding an event listener, as shown below. Add this event listener at the end of the file, just below the others:

// Event listeners
joinRoomButton.addEventListener('click', joinRoom);
leaveRoomButton.addEventListener('click', leaveRoom);

raiseHandButton.addEventListener('click', () => {
  handIsRaised() ? lowerHand() : raiseHand();
});

The logic inside this event listener allows a participant to toggle between a raised and lowered hand when they click the button.

Now that you are set up to send raised hand messages over the data track, you will also need to make sure that the other participants on the call can receive those messages.

Find the participantConnected() function. This is where we will add code to handle messages that are received over the data track. Scroll down to where the handlers for the trackSubscribed and trackUnsubscribed events are. Replace them with the following code:

…
  participant.on('trackSubscribed', track => {
    if (track.kind === 'video' || track.kind === 'audio') {
      tracksDiv.appendChild(track.attach());
    }

    // Set up a listener for the data track
    if (track.kind === 'data') {
      track.on('message', message => {
        const messageData = JSON.parse(message);
        const raisedHandSid = messageData.sid;
        const selectedVideoDiv = document.querySelector(`div[data-sid=${raisedHandSid}]`);

        // Add or remove the raised class to show or dismiss the raised hand
        if (messageData.raised) {
          selectedVideoDiv.classList.add('raised');
        } else {
          selectedVideoDiv.classList.remove('raised');
        }
      });
    }
  });

  participant.on('trackUnsubscribed', track => {
    // Remove audio and video elements from the page
    if (track.kind === 'audio' || track.kind === 'video') {
      track.detach().forEach(element => element.remove());
    }
  });

Inside the handler for trackSubscribed, we've changed the code to attach only the audio and video tracks to the page, and to listen for messages coming across the data track. When a message is received, this code will parse the message to get the sid of the participant who has raised their hand, find their video feed in the UI, and add or remove the raised class to show or hide their raised hand. Inside the handler for trackUnsubscribed, we've changed the code slightly so that only the audio and video tracks that were attached to the div earlier can be removed. Since the data track was not attached to a div, it does not need to be detached here.

Test your application

It's time to test the raise hand feature!

Refresh the browser window where your application is running. You will see the Raise Hand button in the UI now, but it is disabled. Join the video call, and you will see that the button becomes enabled.

Open the application in another tab, and join the video call again with a different name. Once you have done this, try clicking the Raise Hand button in one of the windows. You will see the raised hand emoji appear in the top left corner of your video feed, and the other participants on the call will see it as well. The raised hand will persist until the participant clicks the button again to lower it.

Video participant on the left has their hand raised

What's next for adding features to your video application?

Now that you have learned how to build a raise hand feature, perhaps you're interested in trying this application with a friend. If you would like to deploy this application and get a shareable link, you can do this by running the following command:

npm run deploy

Once deployed, you will see output in your terminal, part of which will look like the text below:

✔ Serverless project successfully deployed

Deployment Details
Domain: video-chat-serverless-XXXX-dev.twil.io
Service:
   video-chat-serverless (ZSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX)

The application will be available at a URL with the format https://video-chat-serverless-XXXX-dev.twil.io, and you can share this URL with your friends!

If you're looking for more ideas for what to add next, perhaps you could continue building on the raise hand feature, perhaps by rearranging the video tiles to push participants with raised hands closer to the top of the UI. This would be a useful feature for when there are many people on the call, and it may make it easier for the host to notice which person has their hand raised.

You could also add other features such as the ability to remove a participant, or even a way to play background music during the video call.

I can't wait to see what you build!

Mia Adjei is a Software Engineer for Technical Content at Twilio. They love to help developers build out new project ideas and discover aha moments. Mia can be reached at madjei [at] twilio.com.