npm init --yes
from the command line..env
file at the root of the project where we’ll store your Twilio account credentials..env
file..env
file as well.TWILIO_API_SECRET="xxxxxx" TWILIO_API_KEY="SKxxxxxx" TWILIO_ACCOUNT_SID="ACxxxxx"
npm install express
to add Express as a project dependency. While you’re at it, run npm install dotenv
to install the library we’ll be using to read our credentials from the .env
file. For more information about working with environment variables in Node.js, check out this blog post. One last dependency we’ll need is the Twilio SDK, so run npm install twilio
too.server.js
where our server code will live. Copy the following code into it:require("dotenv").config(); const http = require("http"); const express = require("express"); const path = require("path"); const app = express(); const AccessToken = require("twilio").jwt.AccessToken; const VideoGrant = AccessToken.VideoGrant; const ROOM_NAME = "telemedicineAppointment"; // Max. period that a Participant is allowed to be in a Room (currently 14400 seconds or 4 hours) const MAX_ALLOWED_SESSION_DURATION = 14400; app.get("/token", function (request, response) { const identity = request.query.identity; // Create an access token which we will sign and return to the client, // containing the grant we just created. const token = new AccessToken( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_API_KEY, process.env.TWILIO_API_SECRET, { ttl: MAX_ALLOWED_SESSION_DURATION } ); // Assign the generated identity to the token. token.identity = identity; // Grant the access token Twilio Video capabilities. const grant = new VideoGrant({ room: ROOM_NAME }); token.addGrant(grant); // Serialize the token to a JWT string and include it in a JSON response. response.send({ identity: identity, token: token.toJwt(), }); }); http.createServer(app).listen(1337, () => { console.log("express server listening on port 1337"); });
identity
string and generates a Twilio Video access token.token
route takes a GET request, we can test it right in the browser.node server.js
http://localhost:1337/token?identity=tilde
in your browser. You should see a response similar to the following:{ "identity": "tilde", "token": "<YOUR_TOKEN_HERE>" }
public
folder at the root of your project, which is where the front end will live.public
folder:patient.html
: the page where the patient will join fromprovider.html
: the page where the provider will join fromindex.js
: JavaScript that is shared between both pages will live hereindex.css
: sprinkling a lil’ styling on this bad boyprovider.html
:<!DOCTYPE html> <html> <head> <title>Owl Hospital Telemedicine App</title> <link rel="stylesheet" href="index.css" /> </head> <body> <h1>🦉 Welcome to Owl Hospital Telemedicine 🦉</h1> <h3>Thanks for caring for our patients <3</h3> </body> <button id="join-button">Join Room</button> <button id="leave-button" class="hidden">Leave Room</button> <div id="local-media-container"></div> </html>
server.js
add the following code:const providerPath = path.join(__dirname, "./public/provider.html"); app.use("/provider", express.static(providerPath)); const patientPath = path.join(__dirname, "./public/patient.html"); app.use("/patient", express.static(patientPath)); // serving up some fierce CSS lewks app.use(express.static(__dirname + "/public"));
public
folder as static assets so we can apply our CSS.http://localhost:1337/provider
in the browser and you should see something like the following:public/index.css
and add the following code:* { background: #252d26; color: #a1ceb6; font-family: "Gill Sans", sans-serif; } button { background: #6a7272; font-size: 20px; border-radius: 5px; } button:hover { background: #694d3c; } .hidden { display: none; }
hidden
class to our HTML.public/index.js
and add the following code (I’ll explain what’s going on next):let room; const joinRoom = async (event, identity) => { const response = await fetch(`/token?identity=${identity}`); const jsonResponse = await response.json(); const token = jsonResponse.token; const Video = Twilio.Video; const localTracks = await Video.createLocalTracks({ audio: true, video: { width: 640 }, }); try { room = await Video.connect(token, { name: "telemedicineAppointment", tracks: localTracks, }); } catch (error) { console.log(error); } // display your own video element in DOM // localParticipants are handled differently // you don't need to fetch your own video/audio streams from the server const localMediaContainer = document.getElementById("local-media-container"); localTracks.forEach((localTrack) => { localMediaContainer.appendChild(localTrack.attach()); }); // display video/audio of other participants who have already joined room.participants.forEach(onParticipantConnected); // subscribe to new participant joining event so we can display their video/audio room.on("participantConnected", onParticipantConnected); room.on("participantDisconnected", onParticipantDisconnected); toggleButtons(); event.preventDefault(); }; // when a participant disconnects, remove their video and audio from the DOM. const onParticipantDisconnected = (participant) => { const participantDiv = document.getElementById(participant.sid); participantDiv.parentNode.removeChild(participantDiv); }; const onParticipantConnected = (participant) => { const participantDiv = document.createElement("div"); participantDiv.id = participant.sid; // when a remote participant joins, add their audio and video to the DOM const trackSubscribed = (track) => { participantDiv.appendChild(track.attach()); }; participant.on("trackSubscribed", trackSubscribed); participant.tracks.forEach((publication) => { if (publication.isSubscribed) { trackSubscribed(publication.track); } }); document.body.appendChild(participantDiv); const trackUnsubscribed = (track) => { track.detach().forEach((element) => element.remove()); }; participant.on("trackUnsubscribed", trackUnsubscribed); }; const onLeaveButtonClick = (event) => { room.localParticipant.tracks.forEach((publication) => { const track = publication.track; // stop releases the media element from the browser control // which is useful to turn off the camera light, etc. track.stop(); const elements = track.detach(); elements.forEach((element) => element.remove()); }); room.disconnect(); toggleButtons(); }; const toggleButtons = () => { document.getElementById("leave-button").classList.toggle("hidden"); document.getElementById("join-button").classList.toggle("hidden"); };
connect
method.attach
their local audio and video tracks, which means turning them into HTML media elements with the Twilio Video SDK. After we’ve done that, we can append them to the DOM.toggleButtons
method is a little helper function to show and hide the Join Room
and Leave Room
buttons, saving the user the trouble of fumbling for the correct one.public/provider.html
, add the following lines:<script src="//media.twiliocdn.com/sdk/js/video/releases/2.3.0/twilio-video.min.js"></script> <script src="./index.js"></script> <script> const joinButton = document.getElementById("join-button"); joinButton.addEventListener("click", async (event) => { await joinRoom(event, "provider"); }); const leaveButton = document.getElementById("leave-button"); leaveButton.addEventListener("click", onLeaveButtonClick); </script> </html>
http://localhost:1337/provider
in your browser again and clicking the Join Room
button:public/patient.html
and add the following code into it:<!DOCTYPE html> <html> <head> <title>Owl Hospital Telemedicine App</title> <link rel="stylesheet" href="index.css" /> </head> <body> <h1>🦉 Welcome to Owl Hospital Telemedicine 🦉</h1> </body> <button id="join-button">Join Room</button> <button id="leave-button" class="hidden">Leave Room</button> <div id="waiting-room" class="hidden"> <p>Thanks! Your provider will be with you shortly.</p> <p>In the meantime enjoy this soothing campfire.</p> <iframe width="640" height="315" src="https://www.youtube.com/embed/E77jmtut1Zc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen ></iframe> </div> <div id="local-media-container"></div> <script src="./index.js"></script> <script src="//media.twiliocdn.com/sdk/js/video/releases/2.3.0/twilio-video.min.js"></script>
public/patient.html
:<script> const providerIdentity = "provider"; async function onJoinButtonClick(event) { await joinRoom(event, "patient"); // is there a doctor in the house?? // if not, show the waiting room if (!isProviderPresent(room.participants)) { showWaitingRoom(); } // if the provider joins, hide the waiting room room.on("participantConnected", (participant) => { if (participant.identity === providerIdentity) { hideWaitingRoom(); } }); // hide the waiting room if the patient disconnects room.on("disconnected", () => { hideWaitingRoom(); }); event.preventDefault(); } const isProviderPresent = (participantMap) => { for (const participant of participantMap.values()) { if (participant.identity === providerIdentity) { return true; } } return false; }; const hideWaitingRoom = () => { const waitingRoom = document.getElementById("waiting-room"); // check that the waiting room is visible, before hiding // just to avoid weird state bugs if (!waitingRoom.classList.contains("hidden")) { waitingRoom.classList.toggle("hidden"); stopWaitingRoomVideo(); } }; const showWaitingRoom = () => { const waitingRoom = document.getElementById("waiting-room"); // check that the waiting room is hidden, before showing // just to avoid weird state bugs if (waitingRoom.classList.contains("hidden")) { waitingRoom.classList.toggle("hidden"); } }; const stopWaitingRoomVideo = () => { const iframe = document.querySelector("iframe"); const video = document.querySelector("video"); if (iframe !== null) { const iframeSrc = iframe.src; iframe.src = iframeSrc; } if (video !== null) { video.pause(); } }; const button = document.getElementById("join-button"); button.addEventListener("click", onJoinButtonClick); const leaveButton = document.getElementById("leave-button"); leaveButton.addEventListener("click", onLeaveButtonClick); </script> </html>
Join Room
button.http://localhost:1337/provider
and join as a provider from another tab or browser, vóila! The waiting room disappears and the video stops.