ACCOUNT SID
on your dashboard.All Products & Services
by clicking the icon on the left as shown in the image below:Programmable Video
. The Programmable Video dashboard will be loaded for you.Tools
, then click on API Keys
, and then click on the +
icon as seen in the image below:Create API Key
.SID
key, SECRET
key, and also your ACCOUNT SID
key. We'll make use of these keys in the next section.# Install vue cli globally on your system $ npm install -g @vue/cli # Create a new vue project (In the prompt that appears, press enter to select the default preset.) $ vue create video-chat # Change your directory to the project directory $ cd video-chat # Run the app! $ npm run serve
$ npm install twilio-video
$ npm install axios
public/index.html
with the following:<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <title>video-chat</title> </head> <body> <noscript> <strong>We're sorry but video-chat doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>
App.vue
file.Video
Component. The component that we'll use to list the Local of Remote tracks.AddRoom
Component. Component for adding new rooms.Rooms
Component. Component for listing rooms.Logs
Component. For displaying logs.AddRoom
component will contain a form which will be used to create a new room and the Rooms
component needs to know when a room form is submitted so it can list it.AddRoom
component, our app will send a trigger to the Rooms
component so it knows what has happen and take action.Event.js
in the src
folder and add the following code to it:import Vue from 'vue' export const EventBus = new Vue()
Event.js
file.App.vue
which is the main entry component file.src/App.vue
with the following code:<template> <div class="container-fluid chat_container" id="app"> <div class="row" v-if="authenticated"> <Rooms /> <Video :username="username"/> <Logs /> </div> <div class="row" v-else> <div class="username"> <form class="form-inline" @submit.prevent="submitUsername(username)"> <div class="form-group mb-2"> <input type="text" class="form-control" v-model="username" > </div> <button type="submit" class="btn btn-primary mb-2 Botton">Submit</button> </form> </div> </div> </div> </template>
<Rooms />
, <Video>
, and the <Logs />
component.v-if
directive to determine which view to display to the user when they load up the app. Since we need a username to create an access token for each user, once they open the app, we'll display a form for them to supply their username.authenticated
data to determine which view to display. We haven’t declared the authenticated
data yet, we'll do this next.src/App.vue
immediately after the closing </template>
tag:<script> // import components import Rooms from './components/Rooms' import Video from './components/Video' import Logs from './components/Logs' import AddRoom from './components/AddRoom' export default { name: 'App', data() { return { username: "", authenticated: false } }, components: { Rooms, Video, Logs, AddRoom }, methods: { submitUsername(username) { if(!username) { return alert('please provide a username'); } this.authenticated = true; } } } </script> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; background: #2c3e50; } .box { border: 1px solid gray; } .username { margin: 12px auto 7px auto; color: wheat; } .Botton { color: #fff; background-color: #4d555f; border-color: #303840; padding: 8px; font-weight: bolder; } </style>
data
method, notice that username is empty and authenticated
is false. When the user enters their username, we'll update the username data. Then, if the provided username is valid (not empty, in our case), we'll set authenticated
to true.authenticated
data is set to true, the user can now start chatting.AddRoom
component which will contain a form for adding a new room.AddRoom.vue
in the src/components
folder and add the following code to it:<template> <div class="row roomForm"> <form class="form-inline" @submit.prevent="createNewRoom(room_name)"> <div class="form-group mb-2"> <input type="text" class="form-control" v-model="room_name" > </div> <button type="submit" class="btn btn-primary mb-2 createRoomBotton">Create Room</button> </form> </div> </template> <script> import { EventBus } from '../Event' export default { name: "AddRoom", // Component name data() { return { room_name: "", } }, methods: { createNewRoom(name) { if(!name) { alert("please provide a room name"); return } this.room_name = ''; EventBus.$emit('new_room', name); } } } </script> <style> .roomForm { margin-left: auto; margin-right: auto; margin-top: 30px; width: 100%; } .createRoomBotton { color: #fff; background-color: #4d555f; border-color: #303840; padding: 8px; font-weight: bolder; } </style>
<template>
, <script>,
and <style>
. All HTML elements goes to the <template>
tag, all JavaScript code goes to the <script>
tag, and the styling for the component goes to the <style>
tag.v-model
directive to create a two-way data bindings for the form input. The @submit.prevent
directive will prevent the form from submitting normally, and thereby call the createNewRoom()
method.createNewRoom()
method to trigger an event named new_room
using EventBus.$emit('new_room', name);
.$emit()
method is the name of the event while the second parameter is the value we want to pass along, which in our case is the room name. We'll listen to this event in the Rooms
component which we'll create next.Rooms
component to list rooms. To add the Rooms
component, create a new file named Rooms.vue
in the src/components
folder. Then add the following code to it:<template> <div class="col-md-3 rooms"> <div class="room" v-for="room in rooms" v-bind:key="room.id" @click="showRoom(room.name)"> {{room.name}} </div> <AddRoom /> <!-- Imported AddRoom component --> </div> </template> <script> import { EventBus } from '../Event' import AddRoom from '../components/AddRoom' export default { name: "Rooms", // Name of the component data() { return { rooms: [ {id: 1, name: 'PHP Room'}, {id: 2, name: 'Python Room'}, {id: 3, name: 'Daily standup'} ], roomCount: 3, // used to keep track of the number of rooms present loading: false, // indicate when tracks in a room is being loaded } }, components: { AddRoom // imported AddRoom component }, created() { }, methods: { showRoom(room) { EventBus.$emit('show_room', room); } } } </script> <style scoped> /* scoped attribute is used here so the styling applies to this component alone */ .rooms > .room { border: 1px solid rgb(124, 129, 124); padding: 13px; margin: 3px 0px; color: ghostwhite; } .rooms { border: 1px solid rgb(64, 68, 64); cursor: pointer; } </style>
v-for
directive in the <template>
tag will render the rooms. Also, we'll use the @click
directive to listen to click events from the user. When a room is clicked, we'll load up the chat for that particular room by calling showRoom()
method.showRoom()
method to trigger an event - 'show_room'. When a room is clicked, we'll call the showRoom()
method, which will be responsible for listing tracks for the particular room.data()
method which we have hard-coded to the app. We'll list these rooms once the app is loaded.new_room
event. In the AddRoom
component, there is a form for adding a new room. Once the room form is submitted, a new_room
event will be triggered. We need to know when this happens in this component (Rooms component) so we can append the newly created room to the list of rooms. To achieve this, update the created
method in src/components/Rooms.vue
, which is empty with the following code :created() { EventBus.$on('new_room', (data) => { this.roomCount++; this.rooms.push({id: this.roomCount, name: data}); }); },
created()
method is a Vue method for executing statements as soon as Vue is loaded.Logs.vue
in src/components/
folder and add the following code to it:<template> <div class="col-md-3 box"> <div class="log" v-for="log in logs" v-bind:key="log.id"> {{log.message}} </div> </div> </template> <script> import { EventBus } from '../Event' import axios from 'axios' export default { name: "Logs", data() { return { logs: [], logCount: 0 } }, created() { EventBus.$on('new_log', (message) => { this.logs.push( {id: this.logCount, message: message} ); this.logCount += 1; }) }, } </script> <style scoped> .log { border: 1px solid rgb(124, 129, 124); padding: 13px; margin: 3px 0px; color: ghostwhite; } </style>
data()
method, we've have defined an array for holding logs. When there is a new log, we'll append the log to the array.v-for
directive, we'll loop through the log array and display the logs to the view.created()
method, we are actively listening for a new_log
event. When there is a new log, we'll append the log to the logs array.Video.vue
file in src/components
and add the following code to it:<template> <div class="col-md-6 box"> <div class="roomTitle"> <span v-if="loading"> Loading... {{roomName}}</span> <span v-else-if="!loading && roomName"> Connected to {{roomName}}</span> <span v-else>Select a room to get started</span> </div> <div class="row remote_video_container"> <div id="remoteTrack"></div> </div> <div class="spacing"></div> <div class="row"> <div id="localTrack"></div> </div> </div> </template> <script> import { EventBus } from '../Event' import Twilio, { connect, createLocalTracks, createLocalVideoTrack } from 'twilio-video' import axios from 'axios' export default { name: "Video", data() { return { loading: false, data: {}, localTrack: false, remoteTrack: '', activeRoom: '', previewTracks: '', identity: '', roomName: null, } }, props: ['username'], // props that will be passed to this component created() {}, methods: { // Generate access token async getAccessToken() {}, // Trigger log events dispatchLog(message) {}, // Attach the Tracks to the DOM. attachTracks(tracks, container) {}, // Attach the Participant's Tracks to the DOM. attachParticipantTracks(participant, container) {}, // Detach the Tracks from the DOM. detachTracks(tracks) {}, // Detach the Participant's Tracks from the DOM. detachParticipantTracks(participant) {}, // Leave Room. leaveRoomIfJoined() {}, // Create a new chat createChat(room_name) {}, } } </script> <style > .remote_video_container { left: 0; margin: 0; border: 1px solid rgb(124, 129, 124); } #localTrack video { border: 3px solid rgb(124, 129, 124); margin: 0px; max-width: 50% !important; background-repeat: no-repeat; } .spacing { padding: 20px; width: 100%; } .roomTitle { border: 1px solid rgb(124, 129, 124); padding: 4px; color: dodgerblue; } </style>
Loading... {{roomName}}
. If we are not loading a chat and the roomName
has been set, we'll display Connected to {{roomName}}
. Otherwise, we'll display Select a room to get started
.data()
method, we have initiated some variables which we are going to use soon.getAccessToken()
block in src/components/Video.vue
:async getAccessToken() { return await axios.get(`http://localhost:3000/token?identity=${this.username}`); },
this.username
is passed as a props to the Video
component. We'll ask a user to provide a username before they can access the chat. Then, we'll use this username to uniquely identify each user using our application. We'll implement this soon.dispatchLog(message)
function in src/components/Video.vue
:// Trigger log events dispatchLog(message) { EventBus.$emit('new_log', message); },
attachTracks(tracks, container)
function in src/components/Video.vue
:// Attach the Tracks to the DOM. attachTracks(tracks, container) { tracks.forEach(function(track) { container.appendChild(track.attach()); }); },
attachParticipantTracks(participant, container)
function block in src/components/Video.vue
:// Attach the Participant's Tracks to the DOM. attachParticipantTracks(participant, container) { let tracks = Array.from(participant.tracks.values()); this.attachTracks(tracks, container); },
methods: { }
block in src/components/Video.vue
:// ... <script> import { EventBus } from '../Event' import Twilio, { connect, createLocalTracks, createLocalVideoTrack } from 'twilio-video' import axios from 'axios' export default { name: "Video", data() { return { loading: false, data: {}, localTrack: false, remoteTrack: '', activeRoom: '', previewTracks: '', identity: '', roomName: null, } }, props: ['username'], // props that will be passed to this component created() {}, methods: { async getAccessToken() { return await axios.get(`http://localhost:3000/token?identity=${this.username}`); }, // Trigger log events dispatchLog(message) { EventBus.$emit('new_log', message); }, // Attach the Tracks to the DOM. attachTracks(tracks, container) { tracks.forEach(function(track) { container.appendChild(track.attach()); }); }, // Attach the Participant's Tracks to the DOM. attachParticipantTracks(participant, container) { let tracks = Array.from(participant.tracks.values()); this.attachTracks(tracks, container); }, // Detach the Tracks from the DOM. detachTracks(tracks) { tracks.forEach( (track) => { track.detach().forEach((detachedElement) => { detachedElement.remove(); }); }); }, } } </script>
detachParticipantTracks(participant)
function block in src/components/Video.vue
:// Detach the Participant's Tracks from the DOM. detachParticipantTracks(participant) { let tracks = Array.from(participant.tracks.values()); this.detachTracks(tracks); },
leaveRoomIfJoined()
function block in src/components/Video.vue
:// Leave Room. leaveRoomIfJoined() { if (this.activeRoom) { this.activeRoom.disconnect(); } },
createChat(room_name)
function block of src/components/Video.vue
:createChat(room_name) { this.loading = true; const VueThis = this; this.getAccessToken().then( (data) => { VueThis.roomName = null; const token = data.data.token; let connectOptions = { name: room_name, // logLevel: 'debug', audio: true, video: { width: 400 } }; // before a user enters a new room, // disconnect the user from they joined already this.leaveRoomIfJoined(); // remove any remote track when joining a new room document.getElementById('remoteTrack').innerHTML = ""; Twilio.connect(token, connectOptions).then(function(room) { // console.log('Successfully joined a Room: ', room); VueThis.dispatchLog('Successfully joined a Room: '+ room_name); // set active toom VueThis.activeRoom = room; VueThis.roomName = room_name; VueThis.loading = false; // Attach the Tracks of all the remote Participants. // When a Participant joins the Room, log the event. // When a Participant adds a Track, attach it to the DOM. // When a Participant removes a Track, detach it from the DOM. // When a Participant leaves the Room, detach its Tracks. // if local preview is not active, create it }); }) },
this.getAccessToken()
will fetch access token for a user from the Node.js server we created earlier.connectOptions
is a variable that holds the option for creating a new chat in Twilio. Here we passed in the room name, video frame width, etc.Twilio.connect
method which will create the room which requires a token.Twilio.connect
method block in src/components/Video.vue
file just after the // Attach the Tracks of all the remote Participants.
comment. room.participants.forEach(function(participant) { let previewContainer = document.getElementById('remoteTrack'); VueThis.attachParticipantTracks(participant, previewContainer); });
Twilio.connect
method block in src/components/Video.vue
file just below the // When a Participant joins the Room, log the event.
comment:room.on('participantConnected', function(participant) { VueThis.dispatchLog("Joining: '" + participant.identity + "'"); });
Twilio.connect
method block in src/components/Video.vue
file just below the // When a Participant adds a Track, attach it to the DOM.
comment:room.on('trackAdded', function(track, participant) { VueThis.dispatchLog(participant.identity + " added track: " + track.kind); let previewContainer = document.getElementById('remoteTrack'); VueThis.attachTracks([track], previewContainer); });
Twilio.connect
method block in src/components/Video.vue
file just below the // When a Participant removes a Track, detach it from the DOM.
comment:room.on('trackRemoved', function(track, participant) { VueThis.dispatchLog(participant.identity + " removed track: " + track.kind); VueThis.detachTracks([track]); });
Twilio.connect
method block in src/components/Video.vue
file just below the // When a Participant leaves the Room, detach its Tracks.
:room.on('participantDisconnected', function(participant) { VueThis.dispatchLog("Participant '" + participant.identity + "' left the room"); VueThis.detachParticipantTracks(participant); });
Twilio.connect
method block in src/components/Video.vue
file below the // if local preview is not active, create it
comment:if(!VueThis.localTrack) { createLocalVideoTrack().then(track => { let localMediaContainer = document.getElementById('localTrack'); localMediaContainer.appendChild(track.attach()); VueThis.localTrack = true; }); }
created()
method block of src/components/Video.vue
component:// ... EventBus.$on('show_room', (room_name) => { this.createChat(room_name); }) // When a user is about to transition away from this page, // disconnect from the room, if joined. window.addEventListener('beforeunload', this.leaveRoomIfJoined); // ...
show_room
event, we'll call the function we just created above - createChat
, to create a chat for that room.