This tutorial will walk you through the steps of building a simple application that uses OpenTok Video Embeds to generate dynamic chat rooms for meetings between a doctor and a patient.
OpenTok video embeds are simple embeddable widgets that can be added to web pages to get ready-made video conference with upto 3 participants. Video Embeds can generate dynamic rooms by changing a single URL parameter. This makes it ideal for simple use cases without requiring too much programming.
This demo does not require using any of OpenTok SDK. It only requires code snippets of OpenTok video embeds. The application is just a proof-of-concept to demonstrate that video embeds can be used for interesting purposes.
This tutorial will cover:
- Workflow
- Requirements
- Setting up development environment and dependencies
- Creating a simple in-memory data store
- Setting up ExpressJS app
- Setting up routes
- Creating user dashboards
- Creating and booking meetings
- Generating dynamic rooms using Video Embeds
- Creating script for launching server
- Next steps
This tutorial is modelled after a basic telehealth use case, with patients meeeting doctors online. The same overall method can be applied to other one-to-one use cases, like tutor:student or agent:customer.
To keep things simple, this tutorial will assume there is only one doctor and one patient and not build any user authentication system. Here is the workflow we will build in this tutorial
- Enter as Doctor
- Creates meetings for the times when they are available
- Doctor's dashboard shows upcoming meeetings
- When a meeting is about to start, it will show up as "Current Meeting"
- Doctor can click on corresponding meeting link to join the meeting.
- Once on the meeting page, doctor clicks "Start Call" button to join.
- Once call duration is over, page reloads mentioning meeting is over.
- Enter as Patient
- Searches for available appointment slots and books the one that they want.
- Patient's dashboard shows upcoming meeetings
- When a meeting is about to start, it will show up as "Current Meeting"
- Patient can click on corresponding meeting link to join the meeting.
- Once on the meeting page, patient clicks "Start Call" button to join.
- Once call duration is over, page reloads mentioning meeting is over.
To complete this tutorial, you will need:
- NodeJS v6.9+ - We use NodeJS for this example. Make sure you have NodeJS installed and
node
andnpm
binaries are onPATH
. - An OpenTok Video Embed code - Follow the instructions on the OpenTok Video Embed page to create a Video Embed and copy the generated code. We'll need this once we have launched the application.
- A text editor.
The application server uses ExpressJS framework to create routes and serve views written in ejs templating language. The page for creating meeting uses Flatpickr to handle date-time input.
This project will use a directory structure like this:
/bin/ - Scripts to launch the application
/routes/ - Handles application URL routes
/static/ - Mounted statically on web root
- css/ - Contains CSS files
- js/ - Contains JavaScript files for frontend
/views/ - Contains views served by routes.
Create a new directory called "opentok-video-embed-demo" and create the directory structure shown above in it. We'll use this directory as our project root going ahead.
$ mkdir -p opentok-video-embed-demo/{bin,routes,static,views}
$ cd opentok-video-embed-demo
$ mkdir -p static/{css,js}
Next, initiate a npm
project. This will create a package.json
file with default values:
$ npm init -y
Edit the generated package.json
file to tweak the name
, version
and description
fields as needed.
Then, install the required NodeJS module dependencies:
$ npm install --save express ejs express-session body-parser cookie-parser
Download Flatpickr, extract the zip file and copy the following files over:
- copy
dist/flatpickr.min.js
to./static/js/flatpickr.min.js
- copy
dist/flatpickr.min.css
to./static/css/flatpickr.min.css
Now we are all set to start writing some code.
Each meeting entry will use this data structure:
{
id: number, // Auto-incremented ID of the meeting, used as unique key when joining meetings
start_time: Date, // Start time of the meeting
end_time: Date, // End time of the meeting
booked: false // Set to `true` if patient has booked this meeeting
}
Create a file called db.js
in the project root. This file will hold a simple data structure that we will use to store application data in memory.
Add the following code to the file:
// This is our simple DB in memory. A real-world use case would use an actual database.
let DB = {
// Used to store an array of meetings created by Doctor
meetings: [],
// Used to store embed code
embed_code: ""
};
Add a few convenience methods to db.js
to make it easier to query the DB
object:
/**
* Find a meeting by its id
*/
DB.meetings_get = function (id) {
// Return `null` if meeting id is not found, else return the meeting object
return this.meetings.find(m => m.id === id) || null;
}
/**
* Add/Update a meeting entry
*/
DB.meetings_put = function (new_meeting) {
const key = this.meetings.findIndex(m => m.id === new_meeting.id);
if (key < 0) {
this.meetings.push(new_meeting);
} else {
this.meetings[key] = new_meeting;
}
}
Then, add this code to db.js
to export the DB
object:
module.exports = DB;
We'll a few more methods to db.js
later on to sort and filter entries. Let's set up an ExpressJS app before that.
Create a file called app.js
in the project root. This file will create, set up and export an ExpressJS app instance.
Start by adding this code to app.js
:
/**
* Main app module
*/
// Load dependencies
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
// Expose in-memory DB as `global`.
global.DB = require('./db');
// Initiate express application
const app = express();
// view engine setup
app.set('view engine', 'ejs');
Add code to enable body-parser
middleware to parse HTTP request body data. We need this to process form data. In app.js
, add these lines:
// Set up body-parser to parse request body. Used for form data
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
Once these are done, add routes for express. First, mount the ./static/
directory on web root and then load the ./routes/
module (details on ./routes
module in the next section):
// Mount routes
app.use(express.static(path.join(__dirname, 'static')));
app.use('/', require('./routes'));
Finally, add a catchall middleware to trap errors in routes and export the app
instance:
// error handler
// no stacktraces leaked to user unless in development environment
app.use(function (err, req, res, next) {
res.status(err.status || 500);
if (!err.status || err.status !== 404) {
console.log(err);
}
res.render('error', {
message: err.message
});
});
// Export `app`
module.exports = app;
We'll build the ./routes/
directory as a module to separate individual HTTP route segments in different files.
Create file routes/index.js
. This file exports the route that is loaded and mounted in app.js
. It also loads other routes in the same directory and mounts them as sub-routes. Each of these routes are instances of Express Router.
Add this in routes/index.js
:
// File: routes/index.js
const router = require('express').Router();
// Serve `home` view. This renders `views/home.ejs`
router.get('/', (req, res) => {
res.render('home');
});
// Load other routes and mount them
router.use('/setup', require('./setup_route'));
router.use('/dashboard', require('./dashboard_route'));
router.use('/meetings', require('./meetings_route'));
module.exports = router;
This loads 3 other files in the routes/
directory - setup_route.js
, dashboard_route.js
and meetings_route.js
.
Let's create our first view - a rather simple view for the homepage. This view will only contain links to enter dashboards for doctor and patient, and a link to set up video embed code.
Create file views/home.ejs
. EJS templates use .ejs
file extension by default. Add this HTML to views/home.ejs
<!doctype html>
<html>
<head>
<title>OpenTok Video Embed demo</title>
</head>
<body>
<div>
<a href="dashboard/doctor">Enter as Doctor</a>
</div>
<div>
<a href="dashboard/patient">Enter as Patient</a>
</div>
<div>
<a href="/setup">Setup/Update embed code</a>
</div>
</body>
</html>
Create file routes/setup_route.js
. This file manages routes for a form that saves OpenTok Video Embed code in the in-memory database.
const router = require('express').Router();
// Serve the view `setup.ejs` for embed code setup form
router.get('/', (req, res) => {
// We pass current embed code as `data` as property to the view
res.render('setup', { data: DB.embed_code || "" });
});
// Handle POST data from the form in the view.
// We only set the `embed_code` property in `DB` and redirect to homepage.
router.post('/', (req, res) => {
DB.embed_code = req.body.embed_code_value.trim();
res.redirect('/');
});
// Export the router
module.exports = router;
Create the view file for this route: views/setup.ejs
. Add this HTML content to the file:
<!doctype html>
<html>
<head>
<title>Set up OpenTok Video embed code</title>
</head>
<body>
<h1>Set up OpenTok embed code</h1>
<p>Create and paste an <a href="https://tokbox.com/developer/embeds" target="_blank">OpenTok video chat embed</a> code.</p>
<form method="POST">
<div>
<textarea id="embed_code_value" name="embed_code_value" rows="10" cols="20"
required autofocus><%= data %></textarea>
</div>
<div>
<input type="submit" value="Set up">
<a href="/">Cancel</a>
</div>
</form>
</body>
</html>
The dashboard route manages routes for both patients' and doctors' dashboards. Create file routes/dashboard_route.js
and add this code in it:
const router = require('express').Router();
/**
* Doctor's dashboard
*/
router.get('/doctor', (req, res) => {
// Render view with meetings that have a future end time
res.render('dashboard_doctor', { meetings: DB.meetings_filter() })
});
/**
* Patient's dashboard
*/
router.get('/patient', (req, res) => {
res.locals.user = { role: 'Patient' };
// Render view only with meetings that were booked and have future end time
res.render('dashboard_patient', { meetings: DB.meetings_filter(true) });
});
module.exports = router;
Notice the use of DB.meetings_filter()
method in the previous example. We need a method to filter existing meetings in the database so that we can keep our router code DRY. We hadn't created it earlier, so let's add it now. Edit db.js
and add this method before the line with module.exports
:
// db.js
// Sort a given array of meetings by their start time in ascending order.
// When using an actual database, you would use the database's ordering methods
let sort = m_list => {
return m_list.sort(function (a, b) {
return a.start_time > b.start_time;
});
}
/**
* Filters through given list of meetings and split it into upcoming and current meetings based on time.
*
* @param {null|boolean} is_booked - If `null` or `undefined`, filter on all items in `mlist`. If true, filter only on
* meetings that are booked. If false, filter only on meetings that have not been booked.
* @return {object} Object containing `upcoming` and `current` meetings
*/
DB.meetings_filter = function (is_booked=null) {
const currtime = Date.now();
let mlist;
if (is_booked != null) {
if (is_booked) {
mlist = () => this.meetings.filter(m => m.booked);
} else {
mlist = () => this.meetings.filter(m => !m.booked);
}
} else {
mlist = () => this.meetings;
}
return {
// Starting after 5 minutes
upcoming: sort(mlist().filter(i => i.start_time.getTime() >= currtime + 300000 )),
// Starting in 5 minutes or has already started but not ended
current: sort(mlist().filter(i => i.start_time.getTime() < currtime + 300000 && i.end_time.getTime() >= currtime))
}
};
The doctor's dashboard shows upcoming meeetings for the doctor - both that patient has booked and not booked.
Create file views/dashboard_doctor.ejs
and add this section of code in it:
<!doctype html>
<html>
<head>
<title>Doctor's dashboard</title>
</head>
<body>
<div>
<h1>Doctor Dashboard</h1>
<a href="/meetings/create">+ Add meeting slot</a>
</div>
Notice that the code above has a link for Doctor to create meeting page. We'll create that in a while.
Next, show current meeting(s) in the view. This code uses EJS conditionals. EJS conditionals are written as regular JavaScript inside EJS tags <%
and >
. Append this code to views/dashboard_doctor.ejs
:
<% if (meetings.current.length > 0) { %>
<h3>Current meeting</h3>
<div>
<% for (var m of meetings.current) { %>
<% if (m.booked) { %>
<div>
<time><%= m.start_time %></time> -
<a href="/meetings/join/<%= m.id %>">Join meeting</a>
</div>
<% } else { %>
<div>
<time><%= m.start_time %></time> -
<span>Unclaimed</span>
</div>
<% } %>
<% } %>
</div>
<% } %>
This block of code shows up only if there is a current meeting: meetings.current.length > 0
. If there is any, it iterates over the meetings.current
array, which was passed from the /dashboard/doctor
route. For each meeting entry in that array, it shows a "Join meeting" link if the meeting entry has been booked. Else, it shows meeting as "Unclaimed". The URL to a meeting is created using the meeting id
.
Next, we'll show upcoming meetings - meetings that don't start in 5 minutes. The template logic is similar to current meeting, except if there are no upcoming meetings, we show a link to create a meeting. Append this final piece of code to views/dashboard_doctor.ejs
:
<h3>Upcoming meetings</h3>
<% if (meetings.upcoming.length > 0) { %>
<div>
<% for (var m of meetings.upcoming) { %>
<% if (m.booked) { %>
<div>
<time><%= m.start_time %></time> -
<span>Booked</span>
</div>
<% } else { %>
<div>
<time><%= m.start_time %><time> -
<span>Unclaimed</span>
</div>
<% } %>
<% } %>
</div>
<% } else { %>
<p>You don't have any upcoming meetings. You can <a href="/meetings/create">create a meeting</a>.</p>
<% } %>
</body>
</html>
Template logic and layout for Patient's dashboard is quite similar to Doctor's dashboard. The only difference is that Patient is asked to book a meeting if they don't have any meeting booked and Patient is only shown meetings that Patient has booked.
Create file views/dashboard_patient.ejs
and add this content:
<!doctype html>
<html>
<head>
<title>Patient's dashboard</title>
</head>
<body>
<div>
<h1>Patient Dashboard</h1>
<a href="/meetings/book">+ Book meeting slot</a>
</div>
<% if (meetings.current.length > 0) { %>
<h3>Current meeting</h3>
<div>
<% for (var m of meetings.current) { %>
<div>
<time><%= m.start_time %><time> -
<a href="/meetings/join/<%= m.id %>">Join meeting</a>
</div>
<% } %>
</div>
<% } %>
<h2>Upcoming meetings</h2>
<% if (meetings.upcoming.length > 0) { %>
<div>
<% for (var m of meetings.upcoming) { %>
<div>
<time><%= m.start_time %></time>
</div>
<% } %>
</div>
<% } else { %>
<p>You don't have any upcoming meetings. You can <a href="/meetings/book">book a meeting</a>.</p>
<% } %>
</body>
</html>
The meetings route handles creating and booking meetings. Create file routes/meetings_route.js
and add this code to serve the view for create meeting:
const router = require('express').Router();
/**
* View for creating meeting
*/
router.get('/create', (req, res) => {
res.render('create_meeting');
});
Next, add route to serve view for booking meeting:
/**
* Render page for booking appointments
*/
router.get('/book', (req, res) => {
// Load only meetings with future data that haven't been booked yet
let m_list = DB.meetings_filter(false);
res.render('book_meeting', { meetings: [].concat(m_list.current, m_list.upcoming) });
});
Next, add logic for handling create meeting form:
/**
* Handle POST request to create new meeeting for doctor
*/
router.post('/create', (req, res) => {
// Create a `Date` object from the `start_date` input
const start_time = new Date(req.body.start_date);
// Create a `Date` object by calculating `start_date` + `duration` specified by user
const end_time = new Date(start_time.getTime() + (parseInt(req.body.duration) * 60000));
// Create meeting object that we will put in DB
const m = {
id: DB.meetings.length + 1, // Auto increment ID
start_time: start_time,
end_time: end_time,
booked: false
};
// Put the meeting object in DB
DB.meetings_put(m);
// Redirect to doctor's dashboard
res.redirect('/dashboard/doctor')
});
Then, add logic for handling book meeting form:
/**
* Handle form for booking appointment for patient
*/
router.post('/book', (req, res, next) => {
// Retrieve meeting by meeting_id
let m = DB.meetings_get(parseInt(req.body.meeting_id));
// If meeting_id is not found, return 404
if (m == null) {
next();
return;
};
// Else mark meeting as booked
m.booked = true;
// And save it
DB.meetings_put(m);
// Redirect to patient's dashboard
res.redirect('/dashboard/patient');
});
Finally, export the router
object:
module.exports = router;
routes/meetings_route.js
should now have necessary routes for displaying and processing forms for creating meetings and booking meetings. Let's create the two views required for these.
Create file views/create_meeting.ejs
with the following content:
<!doctype html>
<html>
<head>
<title>Create meeting slot</title>
<link rel="stylesheet" type="text/css" href="/css/flatpickr.min.css">
</head>
<body>
<h1>Create meeting slot</h1>
<p>Meeting slots that you create here can be booked by patients.</p>
<form method="POST">
<div>
<label for="field-start_date">Start date and time</label>
<input id="field-start_date" type="date" class="start_date_input" autocomplete="off" name="start_date" maxlength="64" autofocus required>
</div>
<div>
<label for="field-duration">Duration (minutes)</label>
<input id="field-duration" type="number" autocomplete="off" name="duration" maxlength="2" required value="15">
</div>
<div>
<input type="submit" value="Create">
<a href="/dashboard/doctor">Cancel</a>
</div>
</form>
<script src="/js/flatpickr.min.js"></script>
<script src="/js/scheduling_ui.js"></script>
</body>
</html>
We need to create the script for scheduling UI. Create file ./static/js/scheduling_ui.js
with the following content to activate flatpickr
on the input field:
/* global flatpickr */
window.addEventListener('load', function () {
flatpickr('.start_date_input', {
// Enable date+time input
enableTime: true,
// Set default date in input to 5 minutes in future
defaultDate: new Date(Date.now() + 300000),
// Set minimum date in input to 1 minute in future
minDate: new Date(Date.now() + 60000)
});
});
This view for booking meeting will contain a list of available upcoming meetings. Each meeting entry will have a <form>
with the meeting id
as a hidden field. This meeting id
is then used the POST
handler route for booking meetings to update the meeting with booked: true
:
Create file views/book_meeting.ejs
and add this content
<!doctype html>
<html>
<head>
<title>Book meeting slot</title>
</head>
<body>
<h1>Book meeting slots</h1>
<% for (var m of meetings) { %>
<div>
<time><%= m.start_time %></time> -
<form method="POST">
<input type="hidden" name="meeting_id" value="<%= m.id %>">
<input type="submit" value="Book">
</form>
</div>
<% } %>
<% if (meetings.length === 0) { %>
<p>No meetings available.</p>
<% } %>
</body>
</html>
Now that we have the rest of the application set up, we need to serve the actual meetings. Each meeting will use the same OpenTok Video Embed, but change the value of room
parameter in the URL of the Video Embed to a different value for each meeting. In this tutorial, we will use the meeting ID as the unique key for creating rooms. To do this, we will replace the default value of room=DEFAULT_ROOM
with the meeting ID.
Let's create the route for meeting. Edit the route/meetings_route.js
file that we created before and add these lines of code before the module.exports
line:
// Our meeting URLs will be in the form of `/meetings/join/:meeting_id`
/**
* View for joining meeting
*/
router.get('/join/:meeting_id', (req, res, next) => {
// Get meeting details from DB
const m = DB.meetings_get(parseInt(req.params.meeting_id));
// If meeting does not exist of meeting is not booked, send 404
if (m == null || !m.booked) {
next();
return;
}
// This is the key area where we create custom rooms using the same embed code.
// We do a simple string replace.
const embed_code = DB.embed_code.replace('DEFAULT_ROOM', `meeting${m.id}`);
// We redirect to URL to set up embed code if embed code is not set up
if (!embed_code) {
res.redirect('/setup');
return;
}
// Then, we figure out whether meeting is already over and bind it as
// a boolean property for the view
if (Date.parse(m.end_time) < Date.now()) {
res.locals.meeting_over = true;
} else {
res.locals.meeting_over = false;
}
// Finally, we render the view by passing it the embed_code and meetingg details
res.render('meeting', { embed_code: embed_code, meeting: m });
});
Create views/meeting.ejs
with the following content:
<!doctype html>
<head>
<title>Meeting <%= meeting.id %></title>
</head>
<body>
<% if (!meeting_over) { %>
<h1>Meeting</h1>
<div>
<div>Start: <time><%= meeting.start_time %></time></div>
<div id="message"></div>
</div>
<div id="ot_embed_demo_container"><%- embed_code %></div>
<p><a href="/">Exit</a></p>
<script>
window.addEventListener('load', function () {
var message_container = document.getElementById('message');
var end_time = Date.parse("<%= meeting.end_time %>");
var time_left = end_time - Date.now();
var update_time_remaining = function () {
var remaining = Math.round((end_time - Date.now()) / 60000);
message_container.innerHTML = '<p><strong>Time left: ' + remaining + ' minute(s).</strong></p>';
};
update_time_remaining();
setInterval(update_time_remaining, 60000);
setTimeout(function () {
window.location.reload();
}, time_left);
});
</script>
<% } else { %>
<p><strong>Meeting is over.</strong></p>
<p><a href="/" class="button">Exit</a></p>
<% } %>
</body>
</html>
This view loads the embed code only if meeting is not over. This results in the embed code showing up as a video chat widget when the page is loaded. If meeting is over, it prints a message saying "Meeting is over".
The <script>
section of the view prints a message using setInterval
every minute showing how many minutes are left in the meeting. It also sets a timer using setTimeout
which force reloads the page once the meeting time is over. Since the route pre-checks if meeting is over, such a reload will result in showing the "Meeting is over" message.
Now, the only thing left is to bootstrap our app
and launch a HTTP server.
Create file ./bin/www
and add this content:
#!/usr/bin/env node
/**
* Bootstraps and launches app listening on specified HTTP port
*/
const app = require('../app');
const http = require('http');
/**
* Get port from environment and store in Express.
*/
const port = process.env.PORT || '3000';
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
server.listen(port);
server.on('listening', () => {
console.log('Listening on port ' + port);
});
This will load ExpressJS app
instance exported from app.js
and launch it using a HTTP server. By default, it uses port 3000
, but it can be modified by specifying PORT
environment variable.
Now, you can launch the application from the project root. Run:
$ node ./bin/www
Open the browser and point to http://localhost:3000.
To run on a different port, say 8080
, run:
$ PORT=8080 node ./bin/www
Now that the application is running, go the "Setup" page and paste in the video embed code obtained from OpenTok. Then, play around by creating a few appointments by entering in as doctor and then booking them by entering as patient (maybe, in another tab).
Notes:
- This tutorial did not configure SSL. WebRTC requires pages to be served over HTTPS with the exception of
http://localhost
. To set up a reverse proxy with SSL termination, see nginx as a reverse proxy with SSL termination. - This tutorial also simplified code to a large extent. You may want to add styles or better UI/UX by improving on the basic code given here.
- OpenTok Video Embeds have their limitations. If they do not solve your use case, you should try doing a deeper integration using OpenTok SDKs. See TokBox Developer Portal for details.