Build a Video Chat Application with OpenTok and Nexmo In App Messaging
Published on May 11, 2021

In this blog post, we’re going to build a web application that allows users to video chat and send messages to each other using OpenTok and Nexmo In-App Messaging.

To see the full code, please check out the following repo. You can also check out our recent webinar that covers the application.

Prerequisites

DT API Account

To complete this tutorial, you will need a DT API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the DT API Dashboard.

Structure of the App

Create a directory and name it whatever you’d like:

mkdir video-messaging-app cd video-messaging-app

We’ll go ahead and create a few files and subfolders inside the directory using the following commands:

mkdir public public/js views touch public/js/index.js views/index.ejs server.js config.js

Our project structure should now look like this:

video-messaging-app
├── package.json
├── package-lock.json
├── views
│   ├── index.ejs
├── public
│   ├── js
│       ├── index.js
├── config.js
├── server.js

Dependencies

We’ll create an NPM project and install all of the dependencies required for the project:

npm init -y // we use the -y flag to skip through the questions
npm install opentok @opentok/client nexmo nexmo-stitch express ejs

Now, let’s go ahead create our server by adding the server code to the server.js file.

const OpenTok = require('opentok');
const Nexmo = require('nexmo');
const express = require('express');

const app = express();
app.use(express.static(`${__dirname}/public`));

app.get('/', (req, res) => {
 res.json({
   opentokApiKey: null,
   opentokSessionId: null,
   opentokToken: null,
   nexmoConversationId: null,
   nexmoJWT: null,
 });
});

const PORT  = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Running server on PORT: ${PORT}`));

Please note that we’ve created a server using ExpressJS and are returning empty credentials for both OpenTok and Nexmo. Don’t worry, we’ll generate the credentials in the next few steps, but before we do that let’s go ahead and create our Nexmo messaging application using the Nexmo CLI:

nexmo app:create video-messaging-app https://example.com/answer https://example.com/event --keyfile=private.key
nexmo conversation:create display_name="Nexmo In-App Messaging"
nexmo user:create name="jamie"
nexmo member:add YOUR_CONVERSATION_ID action=invite channel='{"type":"app"}' user_id=USER_ID // make sure to replace the conversation ID and the user ID

Using the app:create command, we’ll get the application ID for our video-messaging-app along with a private key which will be added to the directory. Please note that we’ve set the answer and event urls to example urls, but we can change these later. Using the conversation:create command, we have also created a conversation called Nexmo In-App Messaging. This will result in a conversation ID which we will use later to connect to the conversation. The user:create command also allows creates a user tied to the application. Please note the name of this user because we will use it as a part of our JWT generation process.

We’ll now create an OpenTok API project using the TokBox dashboard so you can get access to the API Key and Secret.

Now, let’s open our config.js file so we can store our credentials:

module.exports = {
 opentokApiKey: '',
 opentokApiSecret: '',
 nexmoApiKey: '',
 nexmoApiSecret: '',
 nexmoApplicationId: '',
 nexmoPrivateKey: '',
 nexmoConversationId: '',
};

Make sure to add the appropriate credentials to the config.js file

Importing the config variables:

In our server.js file, let's go ahead and import the config variables so we can use these to instantiate OpenTok and Nexmo classes.

const {
 opentokApiKey,
 opentokApiSecret,
 nexmoApiKey,
 nexmoApiSecret,
 nexmoApplicationId,
 nexmoPrivateKey,
 nexmoConversationId,
 } = require('./config');


const opentok = new OpenTok(opentokApiKey, opentokApiSecret);
const nexmo = new Nexmo({
 apiKey: nexmoApiKey,
 apiSecret: nexmoApiSecret,
 applicationId: nexmoApplicationId,
 privateKey: nexmoPrivateKey,
});

Now let’s go ahead and update the GET request path so we can return valid credentials:

app.get('/', (req, res) => {
 opentok.createSession({
   mediaMode: 'routed'
 }, (error, session) => {
   if (error) {
     res.status(500).send('There was an error generating an OpenTok session');
   } else {
     const opentokSessionId = session.sessionId;
     const opentokToken = opentok.generateToken(opentokSessionId);
     const nexmoJWT = nexmo.generateJwt({
       exp: new Date().getTime() + 86400,
       acl: {
          "paths": {
            "/v1/users/**": {},
            "/v1/conversations/**": {},
            "/v1/sessions/**": {},
            "/v1/devices/**": {},
            "/v1/image/**": {},
            "/v3/media/**": {},
            "/v1/applications/**": {},
            "/v1/push/**": {},
            "/v1/knocking/**": {}
          }
       },
       sub: 'jamie' // this is the name we set when creating the user with the Nexmo CLI
     });
     res.json({
       opentokApiKey,
       opentokSessionId,
       opentokToken,
       nexmoConversationId,
       nexmoJWT,
     });
   }
 });
});

Using the code above, we’ll create the following each time someone visits the / path from their browser:

  • OpenTok session ID

  • OpenTok Token for the corresponding session ID

  • JWT Token for our Nexmo application with the appropriate ACLs

Now that we’ve created a mechanism to get the credentials let’s go ahead and work on the client side of the application.

Open up the index.js file located in the js directory.

const OT = require('@opentok/client');
const ConversationClient = require('nexmo-stitch');

const session = OT.initSession(opentokApiKey, opentokSessionId);
const publisher = OT.initPublisher('publisher');

session.on({
 streamCreated: (event) => {
   const subscriberClassName = `subscriber-${event.stream.streamId}`;
   const subscriber = document.createElement('div');
   subscriber.setAttribute('id', subscriberClassName);
   document.getElementById('subscribers').appendChild(subscriber);
   session.subscribe(event.stream, subscriberClassName);
  },
 streamDestroyed: (event) => {
   console.log(`Stream ${event.stream.name} ended because ${event.reason}.`);
  },
  sessionConnected: event => {
    session.publish(publisher);
  },
});

session.connect(opentokToken, (error) => {
 if (error) {
   console.log('error connecting to session');
 }
});

In the code above, we initialize an OpenTok Session by calling the initSession method on the OT object. We then create a publisher and set the following event listeners: streamCreated, streamDestroyed, and sessionConnected. These event listeners are used to subscribe to streams when a stream is created, print a message when a stream is destroyed, and publish to the session when we're connected. We then proceed to connect to the session using the token we generated on the server.

Now that we’ve added the code for a video chat let’s add In-App Messaging.

class ChatApp {
  constructor() {
   this.messageTextarea = document.getElementById('messageTextarea');
   this.messageFeed = document.getElementById('messageFeed');
   this.sendButton = document.getElementById('send');
   this.loginForm = document.getElementById('login');
  }
}

The ChatApp class will be used to add our In-App messaging features. We will also grab the reference to a few DOM elements that we'll create in our index.ejs file.

Let's go ahead and add some helper methods to the ChatApp class for logging our events and errors to the console:

errorLogger(error) {
   console.log(`There was an error ${error}`);
 }

 eventLogger(event) {
   console.log(`This event happened: ${event}`);
 }

Moving on, we need to instantiate a ConversationClient and authenticate with the nexmoJWT token generated by our server:

joinConversation(userToken) {
   new ConversationClient({
     debug: false
   })
   .login(userToken)
   .then(app => {
     console.log('*** Logged into app', app)
     return app.getConversation(nexmoConversationId)
   })
   .then(this.setupConversationEvents.bind(this))
   .catch(this.errorLogger)
 }

Now that we have a reference to the conversation, let's go ahead and set up our conversation events:

setupConversationEvents(conversation) {
   console.log('*** Conversation Retrieved', conversation)
   console.log('*** Conversation Member', conversation.me)

   conversation.on('text', (sender, message) => {
     console.log('*** Message received', sender, message)
     const date = new Date(Date.parse(message.timestamp))
     const text = `${sender.user.name} @ ${date}: <b>${message.body.text}</b><br>`
     this.messageFeed.innerHTML = text + this.messageFeed.innerHTML
   });
   this.showConversationHistory(conversation);
 }

We can retrieve the conversation history by calling the getEvents method on the conversation object. Let's go ahead and create a helper method so we can display the chat history on the DOM. As you can see below, we're using the different types to distinguish between the events:

showConversationHistory(conversation) {
   conversation.getEvents().then((events) => {
     var eventsHistory = ""
      events.forEach((value, key) => {
       if (conversation.members.get(value.from)) {
         const date = new Date(Date.parse(value.timestamp))
         switch (value.type) {
           case 'text:seen':
             break;
           case 'text:delivered':
             break;
           case 'text':
             eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>${value.body.text}</b><br>` + eventsHistory
             break;
            case 'member:joined':
             eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>joined the conversation</b><br>` + eventsHistory
             break;
           case 'member:left':
             eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>left the conversation</b><br>` + eventsHistory
             break;
           case 'member:invited':
             eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>invited to the conversation</b><br>` + eventsHistory
             break;
            default:
             eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>unknown event</b><br>` + eventsHistory
         }
       }
     })
      this.messageFeed.innerHTML = eventsHistory + this.messageFeed.innerHTML
   })
 }

We should also set up some user events to know when the end user has triggered actions on the HTML page:

setupUserEvents() {
   this.sendButton.addEventListener('click', () => {
     this.conversation.sendText(this.messageTextarea.value).then(() => {
         this.eventLogger('text');
         this.messageTextarea.value = '';
     }).catch(this.errorLogger)
 })
 this.loginForm.addEventListener('submit', (event) => {
     event.preventDefault();
     document.getElementById('messages').style.display = 'block';
     document.getElementById('login').style.display = 'none';
     this.joinConversation(nexmoJWT);
  });
 }

Let's make sure to set call the setupUserEvents() method in our constructor:

class ChatApp {
  constructor() {
   this.messageTextarea = document.getElementById('messageTextarea');
   this.messageFeed = document.getElementById('messageFeed');
   this.sendButton = document.getElementById('send');
   this.loginForm = document.getElementById('login');
   this.setupUserEvents();
  }
}

Let's recap what we did in the code above. We’ve created a class called ChatApp that creates a ConversationClient which we authenticate using the nexmoJWT token. We also set an event listener, text, on the conversation object to listen to any incoming messages. Please note that to retrieve older messages from the conversation, we use the getEvents method. We use some event listeners on the DOM to display information when things are changed.

Now that we’ve created the ChatApp class let’s go ahead and instantiate a ChatApp class when the onload event fires so we can use the DOM elements as needed.

window.onload = () => {
 new ChatApp();
}

After completing our index.js, let's go ahead and add some information to our index.ejs file:

<style>
      #login,
      #messages {
        width: 80% ; height: 300px;
      }

      #messages {
        display: none
      }

      #conversations {
        display: none
      }
    </style>
    <script type="text/javascript">
      const opentokApiKey = '<%= opentokApiKey %>';
      const opentokSessionId = '<%= opentokSessionId %>';
      const opentokToken = '<%= opentokToken %>';
      const nexmoConversationId = '<%= nexmoConversationId %>';
      const nexmoJWT = '<%= nexmoJWT %>';
    </script>
    <script src="/js/bundle.js"></script>
  

  
    <form id="login">
      <h1>Login</h1>
      <input type="text" name="username" value="">
      <input type="submit" value="Login">
    </form>

    <section id="messages">
      <button id="leave">Leave Conversation</button>
      <h1>Messages</h1>

      <div id="messageFeed"></div>

      <textarea id="messageTextarea"></textarea>
      <br>
      <button id="send">Send</button>
    </section>

    <section id="conversations">
      <h1>Conversations</h1>
    </section>

The code above is rendered by our server when someone visits the / path. As you can see, we pass in our credentials which we use for the OpenTok Session and Nexmo Conversation Client.

Lastly, let's modify our server to render the index.ejs view with the right variables:

res.render('index.ejs', {
    opentokApiKey,
    opentokSessionId,
    opentokToken,
    nexmoConversationId,
    nexmoJWT,
  });

Now that we have everything set up, let's add a start script to our package.json file so we can easily start the server:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "browserify public/js/index.js -o public/js/bundle.js && node server.js"
  }

Run npm start in your terminal and run the application!

Conclusion

In this blog, we've covered important OpenTok and Nexmo In-App Messaging concepts showcasing the ability to add live video and in-app messaging to web applications. To see the full code, please refer the following repo.

Manik SachdevaVonage Alumni

Manik is a Senior Software Engineer. He enjoys working with developers and crafting APIs. When he's not building APIs or SDKs, you can find him speaking at conferences and meetups.

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.