Developers can use the Dolby.io Communications API to build real-time video and audio calls to provide an HIPAA compliant Telehealth solution for healthcare companies and their patients. We can elevate our application to the next level by integrating PubNub APIs to add user presence and messaging. By leveraging the user-presence feature from PubNub and combining it with Dolby.io, we can create applications that can serve different use cases such as virtual waiting rooms for Telehealth applications, meeting moderation for corporate events and more.
Virtual waiting room
Dolby.io helps developers create a HIPAA-compliant Telehealth application that can be customized to meet patient’s requirements. We will walk you through how to create a Telehealth application using Dolby.io and PubNub that implements a Virtual waiting room where the patient can be triaged until the doctor admits them.
Solution overview
The doctor and patient open a web application provided by the Telehealth provider. Each user joins a unique Dolby.io session. In addition, the doctor and patients subscribe to a PubNub channel to advertise their user presence and activate the messaging feature. The doctor can view the presence states of all the patients. The patient can be admitted to a virtual waiting room, where the patient can view the messages shared by the doctor. The doctor can send a message to the patient to notify them that the doctor is online and admit the patient once the doctor finishes his tasks.
Once the admit button is clicked, a unique Dolby.io conference is created between the doctor and the patient. It aids in creating individual conferences for each consultation or interaction, which can later be utilized for reporting and analytics by user administrators.
Completed Project:
Host view:

Participant view:

Prerequisites:
Dolby.io account – Signup Link
PubNub account – Signup Link
Node.js code execution environment
Project and code walkthrough:
Required NPM packages
API credentials:
The .env file at the project’s root contains sensitive values associated with the application to avoid security breaches on the client side. It includes the Dolby.io account key and secret. It also has the PubNub subscribe and publish key and the Presence URL copied from the PubNub account. The user-created PubNub Channel is stored in the .env
file. In addition, please view our security best practices link to design a secured application.
Express server configuration:
.env
file
DOLBY_IO_KEY="XXXXXXX"
DOLBY_IO_SECRET="XXXXXXX"
PUBNUB_SUBSCRIBE_KEY="sub-c-XXXXXXX"
PUBNUB_PUBLISH_KEY="pub-c-XXXXXXX"
PUBNUB_CHANNEL = "YOUR CHANNEL NAME"
PUBNUB_PRESENCE_URL = "https://ps.pndsn.com/v2/presence/sub-key/YOURSUBSCRIBEKEY/channel/YOURCHANNELNAME/leave?uuid="
The code for running the server is present in the server.js
file.
require("dotenv").config();
const http = require("http");
const https = require("https");
const express = require("express");
const path = require("path");
const app = express();
const dolbyio = require("@dolbyio/dolbyio-rest-apis-client");
http.createServer(app).listen(5501, () => {
console.log("express server listening on port 5501");
});
const hostPath = path.join(__dirname, "./public/host.html");
app.use("/host", express.static(hostPath));
const participantPath = path.join(__dirname, "./public/participant.html");
app.use("/participant", express.static(participantPath));
app.use(express.static(path.join(__dirname, "public")));
const requestURL = "https://session.voxeet.com/v1/oauth2/token";
client_key = process.env.DOLBY_IO_KEY;
client_secret = process.env.DOLBY_IO_SECRET;
pubnub_subscribe_key = process.env.PUBNUB_SUBSCRIBE_KEY;
pubnub_publish_key = process.env.PUBNUB_PUBLISH_KEY;
pubnub_channel = process.env.PUBNUB_CHANNEL;
pubnub_presence_url = process.env.PUBNUB_PRESENCE_URL;
let auth =
"Basic " + Buffer.from(client_key + ":" + client_secret).toString("base64");
const data = JSON.stringify({
grant_type: "client_credentials",
expires_in: "60000",
});
app.get("/clientAccessToken", async function (request, response) {
const APP_KEY = client_key;
const APP_SECRET = client_secret;
const jwt = await dolbyio.communications.authentication.getClientAccessToken(
APP_KEY,
APP_SECRET
);
response.send({
accessToken: jwt.access_token,
});
});
app.get("/pubnubValues", async function (request, response) {
response.send({
pubnub_subscribe_key: pubnub_subscribe_key,
pubnub_publish_key: pubnub_publish_key,
pubnub_channel: pubnub_channel,
pubnub_presence_url: pubnub_presence_url,
});
});
We have two route requests (clientAccessToken
and pubnubValues
) that take a GET request and return the Dolby.io and PubNub credentials, respectively.
Front end configuration:
It is an avenue where the doctor and the patient can virtually meet for a Telehealth consultation.
The front-end code resides in the public folder, which is located at the root of the project
Please review the Getting started project for additional information.
The public folder contains the following files and folders:
HTML files
host.html
: the webpage where the doctor will join the video conferencepatient.html
: the webpage where the patient will enter the video conference
Script folder:
Dolby.io JS scripts: These files can also be downloaded from the Getting started project.
- dvwc_impl.wasm
- voxeet-dvwc-worker.js
- voxeet-sdk.js
- voxeet-worklet.js
- scriptEvents.js: It contains the real-time Video Conferencing event update handling functions. This JS file is shared by both the webpages
- scriptHost.js: It includes the code that manages the host webpage
- scriptParticipant.js: It includes the code that manages the participant webpage
The Img
and CSS
folders contain the background image and the essential CSS files.
For the sake of this demo, we are using the name HOST for the doctor. We assign random names from a list during initialization for the patient.
Initialization and Opening of a Dolby.io session
scriptHost.js
const IntializeandOpenSession = async () => {
VoxeetSDK.conference;
let accessToken;
try {
const response = await fetch(`/clientAccessToken`);
const jsonResponse = await response.json();
accessToken = jsonResponse.accessToken;
} catch (error) {
console.log("IntializeandOpenSession: ", error);
}
VoxeetSDK.initializeToken(accessToken);
try {
await VoxeetSDK.session.open({ name: "Host" });
console.log("Host session");
} catch (error) {
console.log("====================================");
console.log(`Something went wrong ${error}`);
}
};
scriptParticipant.js
Name list
const patientList = [
"Natalie",
"Jumbo",
"Jessica",
"Michelle",
"Sarah",
"Samantha",
"Danielle",
"Ronald",
"Terry",
"Rebekah",
"Christian Avila",
];
let externalID = patientList[Math.floor(Math.random() * patientList.length)];
On the scriptParticipant.js
we have an additional field externalID
; this field is used to invite a participant to a meeting.
VoxeetSDK.session.open({
name: externalID,
externalId: externalID,
});
Pub Nub Initialization:
Please refer to the following blog post from PubNub, which explains how presence is managed
PubNub initialization is identical in both the host and participant scripts.
const initializePubnub = async () => {
try {
const response = await fetch(`/pubnubValues`);
const jsonResponse = await response.json();
console.log(jsonResponse);
pubnub_subscribe_key = jsonResponse.pubnub_subscribe_key;
console.log(pubnub_subscribe_key);
pubnub_publish_key = jsonResponse.pubnub_publish_key;
pubnub_channel = jsonResponse.pubnub_channel;
pubnub_presence_url = jsonResponse.pubnub_presence_url;
pubnub = new PubNub({
subscribeKey: pubnub_subscribe_key,
publishKey: pubnub_publish_key,
uuid: externalID,
});
// Subscribe to the PubNub Channel
pubnub.subscribe({
channels: [pubnub_channel],
withPresence: true,
});
pubnub.addListener(listener);
} catch (error) {
console.log("getpubnubValues: ", error);
}
};
We attach the listener to the Host script code to receive different responses and notifications from the PubNub server.
listener = {
status(response) {
try {
if (response.category === "PNConnectedCategory") {
hereNow(response.affectedChannels);
}
} catch (error) {
console.log(" listener response" + error);
}
},
message(response) {},
presence(response) {
if (response.action === "join") {
for (i = 0; i < response.occupancy; i++) {
if (response.uuid !== undefined) {
let uuidVCJoin = userList.indexOf(response.uuid);
if (uuidVCJoin === -1) {
userList[userList.length] = response.uuid;
console.log("Insert ", response.uuid, "in array");
} else {
console.log("UUID: ", response.uuid, "is already in the array");
}
}
}
}
if (response.action === "interval") {
if (response.join !== undefined) {
for (i = 0; i < response.occupancy; i++) {
if (response.join[i] !== undefined) {
var uuidVCIntervalJoin = userList.indexOf(response.join[i]);
if (uuidVCIntervalJoin === -1) {
console.log("Interval Add UUID: ", uuidVCIntervalJoin);
userList[userList.length] = response.join[i];
}
}
}
}
if (response.leave !== undefined) {
for (i = 0; i < response.occupancy; i++) {
let uuidVCIntervalLeave = userList.indexOf(response.leave[i]);
if (uuidVCIntervalLeave > -1) {
console.log("REMOVE USER FROM ARRAY", uuidVCIntervalLeave);
userList.splice(uuidVCIntervalLeave, 1);
if (response.uuid !== externalID) {
removeUserfromWaitingList(response.uuid);
}
}
}
}
}
if (response.action === "leave") {
for (i = 0; i < response.occupancy; i++) {
let uuidVCLeave = userList.indexOf(response.uuid);
if (uuidVCLeave > -1) {
console.log(
"REMOVE USER FROM ARRAY",
uuidVCLeave,
"with UUID: ",
response.uuid
);
userList.splice(uuidVCLeave, 1);
if (response.uuid !== externalID) {
removeUserfromWaitingList(response.uuid);
}
}
}
}
userList.forEach((user) => {
if (user != externalID) {
addUsertoWaitingList(user);
}
});
},
};
The listener on the participant end is limited to receive responses and messages.
listener = {
status(response) {
try {
if (response.category === "PNConnectedCategory") {
pubnub.hereNow({
channels: [pubnub_channel],
includeUUIDs: true,
includeState: true,
});
}
} catch (error) {
console.log("listener response: " + error);
}
},
message(response) {
if (externalID === response.userMetadata.uuid) {
let msg = response.message; // The Payload
let publisher = response.publisher; //The Publisher
waitingroom_msg.innerText = msg;
}
},
};
uniqueID
is also generated using the generateUUID
method of PubNub.
UniqueID = PubNub.generateUUID();
Host view

Participant View

On the Host script, on the listener function there is a notification of a new participant, then we invoke the addUsertoWaitingList
function, which dynamically creates a div element with the user details, along with the Notify and Invite buttons.
const addUsertoWaitingList = (user) => {
const checkIdExists = document.getElementById(user);
if (!checkIdExists) {
try {
let card = document.createElement("div");
card.id = user;
card.className = "card bg-dark mb-2";
let cardBody = document.createElement("div");
cardBody.className = "card-body";
let headingH5 = document.createElement("h5");
headingH5.className = "card-title text-white";
headingH5.innerText = user;
let btnGroup = document.createElement("div");
btnGroup.className = "btn-group";
btnGroup.role = "group";
const notifyUserBtn = document.createElement("button");
notifyUserBtn.setAttribute("type", "button");
notifyUserBtn.setAttribute("class", "btn btn-success m-1 ");
notifyUserBtn.setAttribute("name", user);
notifyUserBtn.setAttribute("onclick", "SendMessagetoParticipant(this)");
notifyUserBtn.value = "Notify";
notifyUserBtn.innerHTML = "Notify";
btnGroup.appendChild(notifyUserBtn);
const useronWaitbutton = document.createElement("button");
useronWaitbutton.setAttribute("type", "button");
useronWaitbutton.setAttribute("class", "btn btn-secondary m-1 ");
useronWaitbutton.setAttribute("name", user);
useronWaitbutton.setAttribute(
"onclick",
"InviteParticipanttotheMeeting(this)"
);
useronWaitbutton.innerHTML = "Invite";
btnGroup.appendChild(useronWaitbutton);
cardBody.appendChild(headingH5);
cardBody.appendChild(btnGroup);
card.appendChild(cardBody);
container_listGroup.appendChild(card);
} catch (error) {
console.log("addUsertoWaitingList: " + error);
}
}
};
When the “Notify” button is pressed a pre-created message is sent to the participant.
const SendMessagetoParticipant = async (participantName) => {
participantName = participantName.name;
notificationMessage = `Hello ${participantName}, Dr.Sree here! I am currently attending another patient, I will be with you shortly.`;
pubnub.publish(
{
channel: pubnub_channel,
message: notificationMessage,
meta: {
uuid: participantName,
},
},
function (status, response) {
console.log(status, response);
}
);
};

Invite button
const InviteParticipanttotheMeeting = async (participantName) => {
participantUser = participantName.name;
var participants = [{ id: "", externalId: participantUser, avatarUrl: "" }];
conferenceAliasInput = `${externalID} and ${participantUser}'s meeting`;
CreateandJoinConference(conferenceAliasInput);
await sleep(1000);
container_control.style.display = "initial";
try {
let conference = VoxeetSDK.conference.current;
VoxeetSDK.notification.invite(conference, participants);
} catch (error) {
console.log("InviteParticipanttotheMeeting: " + error);
}
htmlParentElement = document.getElementById(participantUser);
htmlParentElement.remove();
};
On the shared script scriptEvent.js
VoxeetSDK.notification.on("invitation", (e) => {
console.log("invitation event");
console.log(e.conferenceId);
admitUsertoMeeting(e.conferenceId);
});
On the Participant script code end:
admitUsertoMeeting = async (conferenceID) => {
try {
globalUnsubscribe();
container_lobby.remove();
container_media.style.display = "initial";
const conference = await VoxeetSDK.conference.fetch(conferenceID);
let options = {
leaveConferenceOnBeforeUnload: true,
constraints: {
audio: false,
video: false,
},
simulcast: false,
maxVideoForwarding: 16,
};
confObject = await VoxeetSDK.conference.join(conference, options);
startVideoBtn.disabled = false;
startAudioBtn.disabled = false;
} catch (error) {
console.log("admitUsertoMeeting: " + error);
}
};
Host view

Participant view

We have the logic to cater once the user exits the meeting or refreshes the browser window.
// If person leaves or refreshes the window, run the unsubscribe function
onbeforeunload = function () {
globalUnsubscribe();
$.ajax({
// Query to server to unsub sync
async: false,
method: "GET",
url: pubnub_presence_url + encodeURIComponent(UniqueID),
})
.done(function (jqXHR, textStatus) {
console.log("Request done: " + textStatus);
})
.fail(function (jqXHR, textStatus) {
console.log("Request failed: " + textStatus);
});
return null;
};
The details on the Presence URL are mentioned on the PubNub REST API documentation page.
Github Link to the completed project
Let us have a review of what we have learned so far:
- Signup for a developer account with Dolby.io and PubNub
- Prepare a developer environment using node.js, installation of NPM package and retrieval of access-tokens to run the project
- Code walkthrough both front-end and back-end
- Integration of PubNub with Dolby.io
- User-presence, instant message exchange, invitation of a participant to an existing conference
- Host and participant video and audio share
// Unsubscribe people from PubNub network
const globalUnsubscribe = () => {
try {
pubnub.unsubscribe({
channels: [pubnub_channel],
});
pubnub.removeListener(listener);
} catch (err) {
console.log("Failed to UnSub");
}
};
The waiting room project provides a foundational knowledge on how to provide an enhanced virtual experience for doctor-patient interactions. However, we can draw the logic from it and implement it on other applications:
- Meeting moderation, admission and removal of participants in a virtual event such as a company all-hands meeting, virtual concerts etc
- Validating the participant credential in the virtual waiting room in the form of security questionnaires, SMS validation etc
- Pivoting the meeting based on real time feedback via Instant messages from Participants, user-presence
- Enabling screen sharing feature using the Dolby.io SDKs for collaboration
- Recording of the event, and distributing it after it
- Transcoding the meeting to different file formats using Dolby.io Media APIs
- Integrating the meeting with the Dolby.io Streaming services to broadcast the meeting to a wider audience
The last three years have proved the importance of Telehealth and its benefits to the community. We are keen to learn more the about the use-case from you, so please reach out to us at [email protected] and let us chat.