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
Vonage API Account
To complete this tutorial, you will need a Vonage 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 Vonage API Dashboard.
A TokBox account and the API Key and Secret from an API project
Structure of the App
Create a directory and name it whatever you’d like:
We’ll go ahead and create a few files and subfolders inside the directory using the following commands:
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.