Hosting Live Webinars with JavaScript, Node.js, AdonisJs and Twilio Programmable Video

April 23, 2018
Written by
Roger Stringer
Contributor
Opinions expressed by Twilio contributors are their own

Twilio-Video-hi-res

We’re going to use Twilio Video with the AdonisJs framework to create a system where a user can host a video, and viewers can watch their presentation. AdonisJs is a full-stack, open-source MVC framework for Node.js that was inspired by the Laravel framework and borrows some of its concepts. AdonisJs saves you time and effort becauses it ships with a lot of features out of the box.

This system can be extended that users can sign up and schedule talks on, even pay to use. But we’re going to keep our project simple so it is easier to build your initial application.

What is Twilio Video?

We will use Twilio Programmable Video to build our app. Twilio’s Video SDK is a fast way to build a full featured WebRTC video solution across web, Android and iOS. We’ll be using the JavaScript SDK but you can also use the iOS and Android SDKs to extend this project to mobile apps later on.

Twilio Video and WebRTC is best used for small groups. Peer to peer Twilio Video Rooms can have a maximum of 10 participants and Group Rooms can have up to 50 participants.  If you are hoping to host webinars larger than 50 participants then for now I would advise against this solution due to current WebRTC limitations.

To get started, you’ll need a Twilio account so sign up for one now if you haven’t already.

Once you get signed up or are signed into your Twilio account, go to the Twilio Console. Go to Programmable Video under the sidebar navigation panel. Under Tools you’ll see an option for API Keys.

Create a new API Key, copy the API Key and the API Secret to a safe place so you can use them in your app.

Install Node.js and AdonisJs

You can follow along or copy and paste code from this open source repository named groupinar.

Make sure you have Node.js and npm installed, I’m using Node.js version 8 at the moment.
Install the AdonisJs CLI:

 

npm i -g @adonisjs/cli

 

Create our video app:

 

adonis new group-video

 

This will create a folder named group-video which will contain our application’s code. You can now test it:

 

cd group-video
adonis serve --dev

 

You should see output similar to this:

 

info: serving app on http://localhost:3333

 

Open http://localhost:3333 in your web browser and you’ll see the welcome page.
Now that we’ve got the barebones, let’s add Twilio Video to our app.
First install the following modules:

 

npm i —save twilio randomstring

 

Add our Twilio configurations, so open .env from the project directory and add the following three lines and add your account sid and API key and secret you created earlier:

 

SESSION_DRIVER=
TWILIO_ACCOUNT_SID=
TWILIO_API_KEY=
TWILIO_API_SECRET=

 

We can now create our Controller and set up our Routes using the adonis command:

 

adonis make:controller Talk

 

It will prompt you to choose the controller type:

 

? Generating a controller for ? (Use arrow keys)
❯ Http Request
  For WebSocket Channel

 

Choose Http Request.
If you look in the app/Controllers/Http/ folder, you’ll see you now have a file called TalkController.js.
In our editor, add the following two lines directly below the 'use strict' line:

 

'use strict'

const Env = use('Env');
const RandomString = use('randomstring');
const AccessToken = use('twilio').jwt.AccessToken;

 

Inside the TalkController class add three new functions:

 

async token ({ params, response }) {
    const identity = RandomString.generate({ length: 10, capitalization: 'uppercase' });

    const VideoGrant = AccessToken.VideoGrant;

    const token = new AccessToken(
        Env.get('TWILIO_ACCOUNT_SID', null),
        Env.get('TWILIO_API_KEY', null),
        Env.get('TWILIO_API_SECRET', null)
    );
    token.identity = identity;
    token.addGrant( new VideoGrant() );
    return response.json({
        identity: identity,
        token: token.toJwt()
    });
}


async host ({ params, view }) {
	const slug = params.slug;
	return client.video.rooms( slug ).fetch().then( (room) => {
		return view.render('talk', {
			slug: slug,
			pageTitle: "Green room",
			hostOrGuest: "on"
		})
	}).catch( err => {
		return client.video.rooms.create({
			uniqueName: slug,
		}).then( room => {
			return view.render('talk', {
				slug: slug,
				pageTitle: "Green room",
				hostOrGuest: "on"
			})
		});
	});
}

async guest ({ params, view }) {
	const slug = params.slug;
	return client.video.rooms( slug ).fetch().then( (room) => {
		return view.render('talk', {
			slug: slug,
			pageTitle: "Guest",
			hostOrGuest: "off"
		})
	}).catch( err => {
		return client.video.rooms.create({
			uniqueName: slug,
		}).then( room => {
			return view.render('talk', {
				slug: slug,
			pageTitle: "Guest",
				hostOrGuest: "off"
			})
		});
	});
}

 

In the token function, we called:

 

const identity = RandomString.generate({ length: 10, capitalization: 'uppercase' });

 

This will assign each user, regardless of being a host or a guest with a random 10 digit name.
The other two functions are close in what they do, the difference is the host function sets the hostOrGuest variable to “on”, and the guest function sets the hostOrGuest to “off”. This will be used later in our view.
Within those functions we also retrieve a group room using the slug from the URL. If the room doesn’t yet exist, we create it.
This lets us add up to 50 people to a room rather than the usual limit of 10.

Open the start/routes.js file, and look for this line:

 

Route.on('/').render('welcome')

 

Add these three new routes directly below it:

 

Route.group(() => {
    Route.get('token','TalkController.token')
    Route.get('host/:slug', 'TalkController.host')
    Route.get(':slug', 'TalkController.guest')
}).prefix('talk')

 

We’ve created a route group, which tells our app that any route beginning with /talk is part of the group. The routes inside this group then line up with the three functions we created earlier:

  • /talk/token will call the token function in TalkController and return our token for each user.
  • /talk/host/:slug will call our host function, creating a room based on the slug we passed.
  • /talk/:slug will call our guest function and display the room.

Run your app using:

 

adonis serve --dev

 

Go to http://localhost:3333/talk/token and you will see a generated token.

Creating our Views

We need to create our views to present something to the user and make our video system do something. Run:

 

adonis make:view talk

 

This will create a new file called talk.edge inside the resources/views folder. Edge is the template language used by AdonisJs.
We’re only creating one view that will be used by both hosts and guests, the difference will be if we broadcast video or not.

 

<html>
<head>
    <title>Group Video</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <link rel="stylesheet" href="https://bootswatch.com/4/sketchy/bootstrap.min.css">
</head>
<body>
    <div class="container">
        <br />
        <div class="alert alert-secondary" role="alert">
            <h4>{{ slug }} {{ pageTitle }}</h4>
            <p>http://localhost:3333/talk/{{ slug }}</p>
        </div>
        <div class="row">
            <div class="col-10">
                <h3>Hosts</h3>
                <div id="media-div"></div>
            </div>
            <div class="col-2">
                <h3>Attendees</h3>
                <div id="people" class="list-group">
            </div>
        </div>
    </div>
    <script src="https://media.twiliocdn.com/sdk/js/common/v0.1/twilio-common.min.js"></script>
    <script src="//media.twiliocdn.com/sdk/js/video/v1/twilio-video.min.js"></script>
    <script>
        const talk = {
            slug: "{{slug}}",
            host: "{{hostOrGuest}}"
        };
    </script>
    <script src="/app.js"></script>
</body>
</html>

 

This view sets up how we want our site to look, it will display videos on the left, and a list of attendees on the right.
Towards the bottom, you can see where we specify an object called talk, in this object we pass the chat room as slug and whether we are in host mode or not.

Ok, quick recap:

  1. We created our controller, and set up routing to said controller
  2. We set up our view to display our videos

We are almost done, all we’ve got left is to set up the final piece of our application to establish the actual connection.
In the public folder, create a file called app.js:

 

if( typeof talk === "undefined" ) alert("please set your talk object");
    let username;
    let accessToken;
    let activeRoom;
    let previewTracks;
    const roomName = talk.slug;

    window.addEventListener('beforeunload', leaveRoomIfJoined);


fetch('/talk/token')
.then(resp => resp.json())
.then(data => {
        accessToken = data.token;
        username = data.identity;

        let videoOptions = {
            audio: false,
            video: false
        };
	   // we only enable video if this is a host.
        if( talk.host === "on" ){
            videoOptions = {
                audio: true,
                video: { width: 300 }
            };
        }
      return Twilio.Video.createLocalTracks(videoOptions);
}).then( localTracks => {
            return Twilio.Video.connect(accessToken, {
                name: talk.slug,
                tracks: localTracks,
                video: { width: 300 }
            });
}).then( room => {
            activeRoom = room;
            room.participants.forEach(participantConnected);
            const previewContainer = document.getElementById(room.localParticipant.sid);
            if (!previewContainer || !previewContainer.querySelector('video')) {
                participantConnected(room.localParticipant);
            }
            room.on('participantConnected', participant => {
                participantConnected(participant);
            });
            room.on('participantDisconnected', participant => {
                participantDisconnected(participant);
            });
}).catch(err => {
	console.log(err);
});

 

In the first piece of our file, we’re calling that /talk/token route and getting a token that is allowed to use video, then we’re checking if talk.host was set to "on" for host mode, or "off" for guest mode.
If host is set to "on", then we don’t enable video broadcasting and we just watch instead.
Next we tell Twilio to connect to the room that is specified by talk.slug.
As part of this, we display any other video windows, and participants, and set up listeners to watch for connections and disconnections. Now for the last part of the script, which are the functions we call when participants join or leave:

 

function participantConnected(participant) {
        const div = document.createElement('div');
        div.id = "video-"+participant.sid;
        const div2 = document.createElement("a");
        div2.setAttribute("class", "list-group-item list-group-item-action");
        div2.id = participant.sid;
        div2.innerHTML = participant.identity;

        participant.tracks.forEach(function(track) {
            trackAdded(div, track)
        });
        participant.on("trackAdded", function(track) {
            trackAdded(div, track)
        });
        participant.on("trackRemoved", trackRemoved);
        document.getElementById("media-div").appendChild(div);
        document.getElementById("people").appendChild(div2);
    }
    function participantDisconnected(participant) {
        participant.tracks.forEach(trackRemoved);
        document.getElementById("video-"+participant.sid).remove();
        document.getElementById(participant.sid).remove();
    }
    function trackAdded(div, track) {
        div.appendChild(track.attach());
        const video = div.getElementsByTagName("video")[0];
        if (video) {
            video.setAttribute("class", "videobox");
        }
    }
    function trackRemoved(track) {
        track.detach().forEach( function(element) { element.remove() });
    }
    function leaveRoomIfJoined() {
        if (activeRoom) {
            activeRoom.disconnect();
        }
    }

 

That’s our video app, this can be used to host webinars pretty easily, or any other type of video chat where you want to use a one-to-many type situation.
Make sure your application is still running with:

 

adonis serve --dev

 

Hosting and attending a talk

Hosting a talk is pretty straight forward, you just open the URL with a slug for it:
http://localhost:3333/talk/host/slug

This will create a webinar with the slug of slug,  if you want other users to broadcast a video feed as well, then you can share this url to them.
To attend a talk, you would just share the guest URL:
http://localhost:3333/talk/slug

Any visitors who use this URL would see all video hosts on the talk and can watch the broadcast while not sharing video.

What’s next?

You’ve gotten a nice intro to both AdonisJs and Twilio Video, and we’ve made video chats a little unconventional with the host and guest mode.

This app can be extended pretty quickly to include user accounts, Q&A, or chat directly so that guests can interact with hosts using Twilio Programmable Chat.

You can find the sample code here on GitHub. If you encounter errors while working through this post or with the finished source code, please leave a comment or open an issue on GitHub.

You can extend it and if you’d like to show me what extra functionality you added, got questions, or want to send a thank you tweet, you can reach me via email at roger@datamcfly.com