This documentation is for reference only. We are no longer onboarding new customers to Programmable Video. Existing customers can continue to use the product until December 5, 2026.
We recommend migrating your application to the API provided by our preferred video partner, Zoom. We've prepared this migration guide to assist you in minimizing any service disruption.
This is the second part of the tutorial for getting started with Twilio Video using Python/Flask and JavaScript. In this part, you'll create a JavaScript frontend that can connect to the backend Flask web server you created in Part One. If you haven't already created a backend server, refer back to the server code and setup in Part One, and then come back to complete this section.
You can also view the completed code from both parts of the tutorial in GitHub.
Before beginning this tutorial, you should ensure you are using one of the supported browsers for the Twilio Video JavaScript SDK.
In the project's root directory (video_tutorial
or whatever you named the project's main directory), create a templates
directory. This is where you'll store your HTML files. To make the new directory, run the following command in your terminal:
_10mkdir templates
In this application, you'll only need one HTML file. Create a new file called index.html
in the templates
directory. This file will contain the HTML structure for the video application, which should include:
<div>
where Participants' videos will be displayed after they've joined.
main.js
file, which is where you'll write the code for controlling the video application.
Add the following code to templates/index.html
:
_17<!DOCTYPE html>_17<html>_17 <head>_17 <meta charset="utf-8" />_17 <title>Twilio Video Demo</title>_17 {/* Twilio Video CDN */}_17 <script src="https://sdk.twilio.com/js/video/releases/2.15.2/twilio-video.min.js"></script>_17 </head>_17 <body>_17 <form id="room-name-form">_17 Enter a Room Name to join: <input name="room_name" id="room-name-input" />_17 <button type="submit">Join Room</button>_17 </form>_17 <div id="video-container"></div>_17 <script src="static/main.js"></script>_17 </body>_17</html>
Note that this index.html
file links to version 2.15.2 of the Twilio Video JavaScript SDK. You can find the CDN link for the most recent version of the Twilio Video JavaScript SDK here.
Next, you'll want to have your Flask server render this index.html
template when someone goes to your web app, instead of showing the string "In progress!" as it does right now. In your app.py
file, change the line that returns "In progress!" to return render_template("index.html")
.
_10@app.route("/")_10def serve_homepage():_10 return render_template("index.html")
If your server is not running, make sure you are in your virtual environment by running source venv/bin/activate
(MacOS/Unix) or venv\Scripts\activate.bat
(Windows). Then, start up the server by running the following command:
_10python app.py
After the server starts, navigate to localhost:5000
in your browser. You should see the form with an input box and a submit button.
In index.html
, you linked to a JavaScript file that doesn't exist yet. In this step, you'll create it and start adding code.
In a second terminal window, navigate to your main project directory, and create a new directory called static
in the root directory of the project:
_10mkdir static
Then, create a file called main.js
within the static
directory, and open that file in a text editor.
First, you'll create variables for the form
, video container div
, and input
HTML elements, to identify them later. Copy and paste the following code into the static/main.js
file:
_10const form = document.getElementById("room-name-form");_10const roomNameInput = document.getElementById("room-name-input");_10const container = document.getElementById("video-container");
Next, you'll create a function called startRoom
that you'll call when the form is submitted. Copy and paste the following code into the static/main.js
file, under the variable declarations:
_20const startRoom = async (event) => {_20 // prevent a page reload when a user submits the form_20 event.preventDefault();_20 // hide the join form_20 form.style.visibility = "hidden";_20 // retrieve the room name_20 const roomName = roomNameInput.value;_20_20 // fetch an Access Token from the join-room route_20 const response = await fetch("/join-room", {_20 method: "POST",_20 headers: {_20 Accept: "application/json",_20 "Content-Type": "application/json",_20 },_20 body: JSON.stringify({ room_name: roomName }),_20 });_20 const { token } = await response.json();_20 console.log(token);_20};
The startRoom
function hides the Join Room form. Then it submits a request to the /join-room
route with the value the user entered in room-name-input
. The /join-room
route either finds an existing Video room with that name or creates one, and gives back an Access Token that this participant can use to join the room.
For now, the code just retrieves the Access Token and prints the token to the console with console.log
. In the next step, you'll use the token to join the video room.
At the bottom of the main.js
file, below the startRoom
function, add an event listener on the form so that when it's submitted, startRoom
runs:
_10form.addEventListener("submit", startRoom);
You can now navigate to localhost:5000
in your browser, enter a room name in the form, click "Join Room", and see the Access Token in the browser's console. (You'll need to open the the Developer Tools for your browser to see the console.)
Here's the full code for static/main.js
so far:
_26const form = document.getElementById("room-name-form");_26const roomNameInput = document.getElementById("room-name-input");_26const container = document.getElementById("video-container");_26_26const startRoom = async (event) => {_26 // prevent a page reload when a user submits the form_26 event.preventDefault();_26 // hide the join form_26 form.style.visibility = "hidden";_26 // retrieve the room name_26 const roomName = roomNameInput.value;_26_26 // fetch an Access Token from the join-room route_26 const response = await fetch("/join-room", {_26 method: "POST",_26 headers: {_26 Accept: "application/json",_26 "Content-Type": "application/json",_26 },_26 body: JSON.stringify({ room_name: roomName }),_26 });_26 const { token } = await response.json();_26 console.log(token);_26};_26_26form.addEventListener("submit", startRoom);
This tutorial is using WebRTC Go rooms, which are free rooms that allow a maximum of two participants in the room at one time. If you request Access Tokens for more than the maximum number of participants in one room, you'll receive an error that there are too many participants in the room. This tutorial doesn't cover error handling in these cases — for now, if you run into this error, you can try joining a new room with a different name. You'll see the error appear in your browser's console if it happens.
Now that your application can retrieve an Access Token, you can use that token to join the video room in the browser. The Twilio Video JavaScript SDK has a connect
method that you can call to connect to a Twilio Video room.
Copy and paste the following code underneath the startRoom
function in static/main.js
.
_10const joinVideoRoom = async (roomName, token) => {_10 // join the video room with the Access Token and the given room name_10 const room = await Twilio.Video.connect(token, {_10 room: roomName,_10 });_10 return room;_10};
This creates a function called joinVideoRoom
, which passes the Access Token and an object of ConnectOptions
to the Twilio video connect
method. The only ConnectOption
you pass in is the name of the room that you're connecting to.
The connect
method returns a Promise that will eventually either resolve to a Room object or be rejected with an error.
Then, call the joinVideoRoom
function after you've retrieved the Access Token in the startRoom
function. For now, you can console.log
the video room object. In the next step, you'll start adding code to display participants' video streams.
Update the startRoom
function with the following lines of code. Note that you're removing the console.log(token)
line and replacing it with a call to the joinVideoRoom
function:
_15 // fetch an Access Token from the join-room route_15 const response = await fetch("/join-room", {_15 method: "POST",_15 headers: {_15 Accept: "application/json",_15 "Content-Type": "application/json",_15 },_15 body: JSON.stringify({ room_name: roomName }),_15 });_15 const { token } = await response.json();_15_15 // join the video room with the token_15 const room = await joinVideoRoom(roomName, token);_15 console.log(room);_15};
Now if you navigate to localhost:5000
in your browser, enter a room name, and click "Join Room", you'll connect to a Twilio Video room. If you open your browser's console, you should see a log line with the room object. You might also be prompted to grant browser access to your camera and microphone devices, because once you connect to a video room, the room will start receiving your audio and video data.
Here's the full main.js
with these additions up to now.
_37const form = document.getElementById("room-name-form");_37const roomNameInput = document.getElementById("room-name-input");_37const container = document.getElementById("video-container");_37_37const startRoom = async (event) => {_37 // prevent a page reload when a user submits the form_37 event.preventDefault();_37 // hide the join form_37 form.style.visibility = "hidden";_37 // retrieve the room name_37 const roomName = roomNameInput.value;_37_37 // fetch an Access Token from the join-room route_37 const response = await fetch("/join-room", {_37 method: "POST",_37 headers: {_37 Accept: "application/json",_37 "Content-Type": "application/json",_37 },_37 body: JSON.stringify({ room_name: roomName }),_37 });_37 const { token } = await response.json();_37_37 // join the video room with the token_37 const room = await joinVideoRoom(roomName, token);_37 console.log(room);_37};_37_37const joinVideoRoom = async (roomName, token) => {_37 // join the video room with the Access Token and the given room name_37 const room = await Twilio.Video.connect(token, {_37 room: roomName,_37 });_37 return room;_37};_37_37form.addEventListener("submit", startRoom);
Once a web client joins a video room with an Access Token, it becomes a Participant in the room. The Twilio Video JavaScript SDK distinguishes between local and remote participants; a web client's local participant is the one who joined the video room from that browser, and all other participants are considered remote participants.
After you've successfully connected to a video room, you'll want to display each participant's video and audio on the page. The room object you got back from the connect
method has an attribute called localParticipant
, which represents the local participant, and an attribute called participants
, which is a list of the remote participants that are connected to the room.
The next step in this code is to display the video and audio data for the participants. This will involve:
The logic for adding the video and audio data on the page will come later; right now, you can create an empty function called handleConnectedParticipant
under the startRoom
function. You'll fill out the body of the function in the next section.
_10const handleConnectedParticipant = (participant) => {};
Next, you'll add calls to the handleConnectedParticipant
function. In main.js
, add the following lines to the startRoom
function, right after the call the joinVideoRoom
function. Note that you're removing the console.log(room)
line and replacing it with a call to handleConnectedParticipant
on the local participant.
_10 // join the video room with the token_10 const room = await joinVideoRoom(roomName, token);_10_10 // render the local and remote participants' video and audio tracks_10 handleConnectedParticipant(room.localParticipant);_10 room.participants.forEach(handleConnectedParticipant);_10 room.on("participantConnected", handleConnectedParticipant);_10};
In these new lines of code, you call handleConnectedParticipant
on the local participant and any remote participants in the room. (In this application, you're using WebRTC Go Rooms, which have a maximum of two participants, so there will only be one remote participant.)
The room will send a participantConnected event whenever a new participant connects to the room. The code also listens for this event and calls the handleConnectedParticipant
function whenever that event triggers.
Here's the full main.js
code up to this point:
_43const form = document.getElementById("room-name-form");_43const roomNameInput = document.getElementById("room-name-input");_43const container = document.getElementById("video-container");_43_43const startRoom = async (event) => {_43 // prevent a page reload when a user submits the form_43 event.preventDefault();_43 // hide the join form_43 form.style.visibility = "hidden";_43 // retrieve the room name_43 const roomName = roomNameInput.value;_43_43 // fetch an Access Token from the join-room route_43 const response = await fetch("/join-room", {_43 method: "POST",_43 headers: {_43 Accept: "application/json",_43 "Content-Type": "application/json",_43 },_43 body: JSON.stringify({ room_name: roomName }),_43 });_43 const { token } = await response.json();_43_43 // join the video room with the token_43 const room = await joinVideoRoom(roomName, token);_43_43 // render the local and remote participants' video and audio tracks_43 handleConnectedParticipant(room.localParticipant);_43 room.participants.forEach(handleConnectedParticipant);_43 room.on("participantConnected", handleConnectedParticipant);_43};_43_43const handleConnectedParticipant = (participant) => {};_43_43const joinVideoRoom = async (roomName, token) => {_43 // join the video room with the Access Token and the given room name_43 const room = await Twilio.Video.connect(token, {_43 room: roomName,_43 });_43 return room;_43};_43_43form.addEventListener("submit", startRoom);
In this section, you'll complete the logic for showing each participant's audio and video. Before doing this, you should understand the concept of participant tracks
.
All participants have tracks. There are three types of tracks:
By default, when a participant connects to a video room, Twilio will request their local audio and video data. You can use ConnectOptions
when you join a video room with the connect
method to control whether or not a local participant sends their local video and audio data. For example, if you wanted to create an audio-only chat room, you could create a room and set video: false
in ConnectOptions
, and then the room would not get participants' video data:
_10const room = await Twilio.Video.connect(token, {_10 room: roomName,_10 video: false,_10 });
In this application, you'll receive both the audio and video data from participants.
Video room tracks follow a publish/subscribe pattern. A participant publishes their video, audio, and/or data tracks, and all other participants can subscribe to those published tracks. A participant cannot subscribe to an unpublished track.
When a participant publishes a track, it means they are making the data from that track available to other participants. By default, participants will publish their video and audio tracks when they join a room. A local participant can choose to unpublish or disable certain tracks as well. You can take advantage of this to build something like mute functionality; you could write a function that allows a local participant to stop publishing their audio track. This would make the audio track no longer available to other participants in the room, while still keeping their video track available.
You can update the default track subscription setting with the Track Subscriptions API, which allows you to specify which tracks to subscribe to automatically. For example, if you were building a video presentation application, you might want participants to only subscribe to one participant's audio track (the presenter), and not subscribe to anyone else's (the audience).
In this tutorial, you'll stick with the default track publication setting, with every participant publishing their audio and video tracks, and other participants automatically subscribing to those tracks.
Given these details, you can now complete the handleConnectedParticipant
function in main.js
. You'll separate the logic into a few different pieces.
Remove the empty handleConnnectedParticipant
function and replace it with the code below.
_17const handleConnectedParticipant = (participant) => {_17 // create a div for this participant's tracks_17 const participantDiv = document.createElement("div");_17 participantDiv.setAttribute("id", participant.identity);_17 container.appendChild(participantDiv);_17_17 // iterate through the participant's published tracks and_17 // call `handleTrackPublication` on them_17 participant.tracks.forEach((trackPublication) => {_17 handleTrackPublication(trackPublication, participant);_17 });_17_17 // listen for any new track publications_17 participant.on("trackPublished", handleTrackPublication);_17};_17_17const handleTrackPublication = () => {}
In the body of handleConnectedParticipant
, you first create a new <div>
element for a participant, where you'll put all of this participant's tracks. (This will be helpful when a participant disconnects from the video app — you can remove all of their tracks from the page by removing this <div>
.)
Then, you loop through all of the participant's published tracks by looping over the participant's tracks
attribute, which contains the participant's TrackPublication
objects. A TrackPublication
object represents a track that has been published.
TrackPublication
objects can be either LocalTrackPublication
or RemoteTrackPublication
objects. In this application's case, you don't need to distinguish between local or remote objects, because they're treated the same. You might want to distinguish between local and remote tracks if you wanted to do something like display the local participant's video differently than other participants' videos.
You pass those TrackPublication
objects to a new function called handleTrackPublication
, which will eventually add the track onto the page. handleTrackPublication
is an empty function underneath the handleConnectedParticipant
function now, but you'll complete it in the next step.
You also listen for any new track publications from a participant. Whenever there's a trackPublished event, the handleTrackPublication
function runs.
This section is complex, as you're dealing with several different objects: Participants
, TrackPublications
, and Tracks
.
It can be helpful to console.log
these objects as you're starting to understand what they do. Feel free to add console.log
statements throughout the code and take time to look at what the objects include. As an example, you might add console.log
lines to the handleConnectedParticipant
function and inspect the participant
and trackPublication
objects:
_10 // iterate through the participant's published tracks and_10 // call `handleTrackPublication` on them_10 console.log("participant: ", participant);_10 participant.tracks.forEach((trackPublication) => {_10 console.log("trackPublication: ", trackPublication);_10 handleTrackPublication(trackPublication, participant);_10 });
In the next function, handleTrackPublication
, you'll be working with individual trackPublication
s from a participant.
Remove the empty handleTrackPublication
function, and replace it with the following code:
_10const handleTrackPublication = (trackPublication, participant) => {_10 function displayTrack(track) {_10 // append this track to the participant's div and render it on the page_10 const participantDiv = document.getElementById(participant.identity);_10 // track.attach creates an HTMLVideoElement or HTMLAudioElement_10 // (depending on the type of track) and adds the video or audio stream_10 participantDiv.append(track.attach());_10 }_10};
First, you create an internal function, displayTrack
, to handle rendering the data on the page. It will receive a track and find the <div>
that you created earlier for containing all of a participant's tracks. Then, you'll call track.attach()
and append that to the participant's <div>
.
track.attach()
creates either an HTMLVideoElement
, if the track is a VideoTrack
, or an HTMLAudioElement
for an AudioTrack
. It then adds the video or audio data stream to that HTML element. This is how video will ultimately show up on the page, and how audio will be played.
You'll only call displayTrack
on tracks that you are subscribed to. Update the handleTrackPublication
function with the following code, underneath the displayTrack
function:
_18const handleTrackPublication = (trackPublication, participant) => {_18 function displayTrack(track) {_18 // append this track to the participant's div and render it on the page_18 const participantDiv = document.getElementById(participant.identity);_18 // track.attach creates an HTMLVideoElement or HTMLAudioElement_18 // (depending on the type of track) and adds the video or audio stream_18 participantDiv.append(track.attach());_18 }_18_18 // check if the trackPublication contains a `track` attribute. If it does,_18 // we are subscribed to this track. If not, we are not subscribed._18 if (trackPublication.track) {_18 displayTrack(trackPublication.track);_18 }_18_18 // listen for any new subscriptions to this track publication_18 trackPublication.on("subscribed", displayTrack);_18};
You can identify whether or not you are subscribed to a published track by seeing if the trackPublication
has a track
attribute. The track
is the stream of audio, video, or data that you'll add to the page. If the trackPublication
does have a track
, this participant is subscribed and you can display the data. Otherwise, this local participant has not been subscribed to the published track.
This code also adds an event listener so that any time you subscribe to a new track, displayTrack
runs.
Here's the full code for the main.js
file:
_76const form = document.getElementById("room-name-form");_76const roomNameInput = document.getElementById("room-name-input");_76const container = document.getElementById("video-container");_76_76const startRoom = async (event) => {_76 // prevent a page reload when a user submits the form_76 event.preventDefault();_76 // hide the join form_76 form.style.visibility = "hidden";_76 // retrieve the room name_76 const roomName = roomNameInput.value;_76_76 // fetch an Access Token from the join-room route_76 const response = await fetch("/join-room", {_76 method: "POST",_76 headers: {_76 Accept: "application/json",_76 "Content-Type": "application/json",_76 },_76 body: JSON.stringify({ room_name: roomName }),_76 });_76 const { token } = await response.json();_76_76 // join the video room with the token_76 const room = await joinVideoRoom(roomName, token);_76_76 // render the local and remote participants' video and audio tracks_76 handleConnectedParticipant(room.localParticipant);_76 room.participants.forEach(handleConnectedParticipant);_76 room.on("participantConnected", handleConnectedParticipant);_76};_76_76const handleConnectedParticipant = (participant) => {_76 // create a div for this participant's tracks_76 const participantDiv = document.createElement("div");_76 participantDiv.setAttribute("id", participant.identity);_76 container.appendChild(participantDiv);_76_76 // iterate through the participant's published tracks and_76 // call `handleTrackPublication` on them_76 participant.tracks.forEach((trackPublication) => {_76 handleTrackPublication(trackPublication, participant);_76 });_76_76 // listen for any new track publications_76 participant.on("trackPublished", handleTrackPublication);_76};_76_76const handleTrackPublication = (trackPublication, participant) => {_76 function displayTrack(track) {_76 // append this track to the participant's div and render it on the page_76 const participantDiv = document.getElementById(participant.identity);_76 // track.attach creates an HTMLVideoElement or HTMLAudioElement_76 // (depending on the type of track) and adds the video or audio stream_76 participantDiv.append(track.attach());_76 }_76_76 // check if the trackPublication contains a `track` attribute. If it does,_76 // we are subscribed to this track. If not, we are not subscribed._76 if (trackPublication.track) {_76 displayTrack(trackPublication.track);_76 }_76_76 // listen for any new subscriptions to this track publication_76 trackPublication.on("subscribed", displayTrack);_76};_76_76const joinVideoRoom = async (roomName, token) => {_76 // join the video room with the Access Token and the given room name_76 const room = await Twilio.Video.connect(token, {_76 room: roomName,_76 });_76 return room;_76};_76_76form.addEventListener("submit", startRoom);
You should now be able to go to localhost:5000
, join a video room, and see your video. Then, from a separate tab, you can join the same room and see both participants' video and hear audio.
You now have a working Video application! The last piece is to clean up after a participant closes their browser or disconnects. In those cases, you should disconnect them from the room and stop displaying their video.
Copy and paste the following handleDisconnectedParticipant
function in static/main.js
under the handleTrackPublication
function.
_10const handleDisconnectedParticipant = (participant) => {_10 // stop listening for this participant_10 participant.removeAllListeners();_10 // remove this participant's div from the page_10 const participantDiv = document.getElementById(participant.identity);_10 participantDiv.remove();_10};
In this function, when a participant disconnects, you remove all the listeners you had on the participant, and remove the <div>
containing their video and audio tracks.
Then, add an event listener in the startRoom
function, underneath the calls to handleConnectedParticipant
. When a participantDisconnected
event happens, call the handleDisconnectedParticipant
function that you wrote.
_10 // render the local and remote participants' video and audio tracks_10 handleConnectedParticipant(room.localParticipant);_10 room.participants.forEach(handleConnectedParticipant);_10 room.on("participantConnected", handleConnectedParticipant);_10_10 // handle cleanup when a participant disconnects_10 room.on("participantDisconnected", handleDisconnectedParticipant);_10};
You'll also want to detect when someone closes the browser or navigates to a new page, so that you can disconnect them from the room. This way, other participants can be alerted that the participant left the room. Underneath the event listener for participantDisconnected
, add two more event listeners: one for pagehide
, and one for beforeunload
. When either of these events occur, the local participant should disconnect from the room.
_10 // handle cleanup when a participant disconnects_10 room.on("participantDisconnected", handleDisconnectedParticipant);_10 window.addEventListener("pagehide", () => room.disconnect());_10 window.addEventListener("beforeunload", () => room.disconnect());_10};
Here's the final main.js
with all of this functionality:
_89const form = document.getElementById("room-name-form");_89const roomNameInput = document.getElementById("room-name-input");_89const container = document.getElementById("video-container");_89_89const startRoom = async (event) => {_89 // prevent a page reload when a user submits the form_89 event.preventDefault();_89 // hide the join form_89 form.style.visibility = "hidden";_89 // retrieve the room name_89 const roomName = roomNameInput.value;_89_89 // fetch an Access Token from the join-room route_89 const response = await fetch("/join-room", {_89 method: "POST",_89 headers: {_89 Accept: "application/json",_89 "Content-Type": "application/json",_89 },_89 body: JSON.stringify({ room_name: roomName }),_89 });_89 const { token } = await response.json();_89_89 // join the video room with the token_89 const room = await joinVideoRoom(roomName, token);_89_89 // render the local and remote participants' video and audio tracks_89 handleConnectedParticipant(room.localParticipant);_89 room.participants.forEach(handleConnectedParticipant);_89 room.on("participantConnected", handleConnectedParticipant);_89_89 // handle cleanup when a participant disconnects_89 room.on("participantDisconnected", handleDisconnectedParticipant);_89 window.addEventListener("pagehide", () => room.disconnect());_89 window.addEventListener("beforeunload", () => room.disconnect());_89};_89_89const handleConnectedParticipant = (participant) => {_89 // create a div for this participant's tracks_89 const participantDiv = document.createElement("div");_89 participantDiv.setAttribute("id", participant.identity);_89 container.appendChild(participantDiv);_89_89 // iterate through the participant's published tracks and_89 // call `handleTrackPublication` on them_89 participant.tracks.forEach((trackPublication) => {_89 handleTrackPublication(trackPublication, participant);_89 });_89_89 // listen for any new track publications_89 participant.on("trackPublished", handleTrackPublication);_89};_89_89const handleTrackPublication = (trackPublication, participant) => {_89 function displayTrack(track) {_89 // append this track to the participant's div and render it on the page_89 const participantDiv = document.getElementById(participant.identity);_89 // track.attach creates an HTMLVideoElement or HTMLAudioElement_89 // (depending on the type of track) and adds the video or audio stream_89 participantDiv.append(track.attach());_89 }_89_89 // check if the trackPublication contains a `track` attribute. If it does,_89 // we are subscribed to this track. If not, we are not subscribed._89 if (trackPublication.track) {_89 displayTrack(trackPublication.track);_89 }_89_89 // listen for any new subscriptions to this track publication_89 trackPublication.on("subscribed", displayTrack);_89};_89_89const handleDisconnectedParticipant = (participant) => {_89 // stop listening for this participant_89 participant.removeAllListeners();_89 // remove this participant's div from the page_89 const participantDiv = document.getElementById(participant.identity);_89 participantDiv.remove();_89};_89_89const joinVideoRoom = async (roomName, token) => {_89 // join the video room with the Access Token and the given room name_89 const room = await Twilio.Video.connect(token, {_89 room: roomName,_89 });_89 return room;_89};_89_89form.addEventListener("submit", startRoom);
You're done! You now have a video chat application that you can use for connecting two people with audio and video.
Right now, the application is only running locally. To make it accessible to others without deploying the application, you can use ngrok. Download ngrok and follow the setup instructions (you'll unzip the ngrok download file and can then start it up). Once it's downloaded, start ngrok on port 5000.
_10./ngrok http 5000
You should see output that looks like this:
_12ngrok by @inconshreveable (Ctrl+C to quit)_12_12Session Status online_12Update update available (version 2.3.40, Ctrl-U to update)_12Version 2.3.35_12Region United States (us)_12Web Interface http://127.0.0.1:4040_12Forwarding http://04ae1f9f6d2b.ngrok.io -> http://localhost:5000_12Forwarding https://04ae1f9f6d2b.ngrok.io -> http://localhost:5000_12_12Connections ttl opn rt1 rt5 p50 p90_12 0 0 0.00 0.00 0.00 0.00
Copy the "Forwarding" address and send that to a friend so they can join your video chat!
There's so much more that you can add on to your application now that you've built the foundation. For example, you can:
For more inspiration, check out the Twilio Video React quick deploy app, which demonstrates a wide range of video functionality, or try out more Video tutorials on the Twilio Blog.