DEV Community

Cover image for Build Your Own Livestreaming App with Twilio Live
Simon Pfeiffer for Codesphere Inc.

Posted on • Edited on

8 4

Build Your Own Livestreaming App with Twilio Live

With the increase in remote work and lectures in the past two years, there has been an increase in the need for live streaming applications for online meetings, classes, and webinars. While general-purpose live-streaming applications are available, organisations prefer the use of their own internal live-streaming applications that meet their specific needs.

Creating a live-streaming application entirely from scratch can take up a lot of resources and effort. This is where Twilio Live, which provides pre-built libraries and SDKs, proves really helpful. With the help of these libraries, we can quickly deploy applications as per our needs.

In today’s tutorial, we’re going to build a live streaming application using Twilio Live, Node.js and Express(to build the server), and vanilla JavaScript(for the client-side). We will also use ngrok to stream it across multiple devices.


The Setup

Before we start writing the actual application, we need to set our environment up.

Make sure Node.js and npm are installed on the machine.

To use Twilio Live, we need to create a Twilio account. You can create a free trial account here: Try Twilio Free.

Creating a Twilio account gives us our ACCOUNT_SID. It also lets us generate the API key required in our live-streaming application.

We need to download ngrok to be able to generate a link that we can use to access the live stream on multiple devices. You can download it here: ngrok - download.

Now let’s create a folder that will be used as the project directory and run the following command to create our package.json file

npm init

Then let’s install our dependencies with:

npm install dotenv express twilio @twilio/live-player-sdk

Now let’s create our .env file with the following environment variables:

TWILIO_ACCOUNT_SID=XXXXX
TWILIO_API_KEY_SID=XXXXX
TWILIO_API_KEY_SECRET=XXXXX

You can find the first variable here in your Twilio account: Twilio console

The other two need to be generated by going to For Twilio and clicking on the “Create API key.”

Inside your project directory, create a folder named public.
Our client-side files will go here.

Inside the public folder, create a folder named livePlayer. Copy the following files from node_modules/@twilio/live-player-sdk/dist/build into this folder.

  • twilio-live-player.min.js
  • twilio-live-player-wasmworker-1-5-0.min.js
  • twilio-live-player-wasmworker-1-5-0.min.wasm

Now that we have set our environment up, we are ready to build our live-streaming application with the help of Twilio Live. To create our application, we need the following building blocks:

  1. A server that can generate access tokens for the streamer and audience, create endpoints to control the live stream, and create and update rooms.
  2. A streamer to generate the live stream.
  3. An audience to view the live stream.

The Server

Create a file named server.js inside the project directory with the following code

import dotenv from "dotenv";
import { fileURLToPath } from "url";
import { dirname } from "path";
import express from "express";
import crypto from "crypto";
import twilio from "twilio";
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const port = 3333;
const AccessToken = twilio.jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;
const PlaybackGrant = AccessToken.PlaybackGrant;
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const apiKey = process.env.TWILIO_API_KEY_SID;
const apiKeySecret = process.env.TWILIO_API_KEY_SECRET;
const twilioClient = twilio(apiKey, apiKeySecret, { accountSid: accountSid });
app.use(express.json());
app.use(express.static("public"));
app.get("/", (req, res) => {
res.sendFile("public/index.html", { root: __dirname });
});
app.get("/stream", (req, res) => {
res.sendFile("public/streamer.html", { root: __dirname });
});
app.get("/watch", (req, res) => {
res.sendFile("public/audience.html", { root: __dirname });
});
/**
* Start a new livestream with a Video Room, PlayerStreamer, and MediaProcessor
*/
app.post("/start", async (req, res) => {
const streamName = req.body.streamName;
try {
// Create the WebRTC Go video room, PlayerStreamer, and MediaProcessors
const room = await twilioClient.video.rooms.create({
uniqueName: streamName,
type: "go",
});
const playerStreamer = await twilioClient.media.playerStreamer.create();
const mediaProcessor = await twilioClient.media.mediaProcessor.create({
extension: "video-composer-v1",
extensionContext: JSON.stringify({
identity: "video-composer-v1",
room: {
name: room.sid,
},
outputs: [playerStreamer.sid],
}),
});
return res.status(200).send({
roomId: room.sid,
streamName: streamName,
playerStreamerId: playerStreamer.sid,
mediaProcessorId: mediaProcessor.sid,
});
} catch (error) {
return res.status(400).send({
message: `Unable to create livestream`,
error,
});
}
});
/**
* End a livestream
*/
app.post("/end", async (req, res) => {
const streamDetails = req.body.streamDetails;
// End the player streamer, media processor, and video room
const streamName = streamDetails.streamName;
const roomId = streamDetails.roomId;
const playerStreamerId = streamDetails.playerStreamerId;
const mediaProcessorId = streamDetails.mediaProcessorId;
try {
await twilioClient.media
.mediaProcessor(mediaProcessorId)
.update({ status: "ended" });
await twilioClient.media
.playerStreamer(playerStreamerId)
.update({ status: "ended" });
await twilioClient.video.rooms(roomId).update({ status: "completed" });
return res.status(200).send({
message: `Successfully ended stream ${streamName}`,
});
} catch (error) {
return res.status(400).send({
message: `Unable to end stream`,
error,
});
}
});
/**
* Get an Access Token for a streamer
*/
app.post("/streamerToken", async (req, res) => {
if (!req.body.identity || !req.body.room) {
return res.status(400).send({ message: `Missing identity or stream name` });
}
// Get the user's identity and the room name from the request
const identity = req.body.identity;
const roomName = req.body.room;
try {
// Create a video grant for this specific room
const videoGrant = new VideoGrant({
room: roomName,
});
// Create an access token
const token = new AccessToken(accountSid, apiKey, apiKeySecret);
// Add the video grant and the user's identity to the token
token.addGrant(videoGrant);
token.identity = identity;
// Serialize the token to a JWT and return it to the client side
return res.send({
token: token.toJwt(),
});
} catch (error) {
return res.status(400).send({ error });
}
});
/**
* Get an Access Token for an audience member
*/
app.post("/audienceToken", async (req, res) => {
// Generate a random string for the identity
const identity = crypto.randomBytes(20).toString("hex");
try {
// Get the first player streamer
const playerStreamerList = await twilioClient.media.playerStreamer.list({
status: "started",
});
const playerStreamer = playerStreamerList.length
? playerStreamerList[0]
: null;
// If no one is streaming, return a message
if (!playerStreamer) {
return res.status(200).send({
message: `No one is streaming right now`,
});
}
// Otherwise create an access token with a PlaybackGrant for the livestream
const token = new AccessToken(accountSid, apiKey, apiKeySecret);
// Create a playback grant and attach it to the access token
const playbackGrant = await twilioClient.media
.playerStreamer(playerStreamer.sid)
.playbackGrant()
.create({ ttl: 60 });
const wrappedPlaybackGrant = new PlaybackGrant({
grant: playbackGrant.grant,
});
token.addGrant(wrappedPlaybackGrant);
token.identity = identity;
// Serialize the token to a JWT and return it to the client side
return res.send({
token: token.toJwt(),
});
} catch (error) {
res.status(400).send({
message: `Unable to view livestream`,
error,
});
}
});
//Start new express server
app.listen(port, async () => {
console.log(`Listening to Express server on port ${port}`);
});
view raw server.js hosted with ❤ by GitHub

This server serves our static pages, creates endpoints to start and end the live stream, and gets access tokens for the participants. We listen to the server on port 3333.


The Client-Side

First, create a file named index.html inside the public folder and add the following code to it

<!DOCTYPE html>
<html lang="en">
<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" />
<title>Twilio Livestream Demo | Codesphere</title>
</head>
<body>
<h1>Twilio Livestream Demo</h1>
<div>
<h3><a href="/stream">Start a Livestream</a></h3>
</div>
<div>
<h3><a href="/watch">Watch a Livestream</a></h3>
</div>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

This file will be the homepage for our live-streaming application. It can be opened in the browser using localhost:3333.

Now we need to create our streamer end of the application. We will need two files for this purpose. One is streamer.html and the other streamer.js. Create both files inside the public folder using the code below:

<!DOCTYPE html>
<html lang="en">
<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" />
<script
defer
src="https://sdk.twilio.com/js/video/releases/2.18.0/twilio-video.min.js"
></script>
<script defer src="streamer.js" type="text/javascript"></script>
<title>Twilio Livestream | Streamer</title>
</head>
<body>
<h1>Streamer</h1>
<div id="container">
<div id="stream">
<!-- video will be added here dynamically -->
</div>
<div id="controls">
<input id="identity" type="text" placeholder="Your name" required />
<input
id="streamName"
type="text"
placeholder="Livestream name"
required
/>
<button id="streamStartEnd">Start Stream</button>
</div>
</div>
</body>
</html>
view raw streamer.html hosted with ❤ by GitHub
const stream = document.getElementById("stream");
const identityInput = document.getElementById("identity");
const streamNameInput = document.getElementById("streamName");
const startEndButton = document.getElementById("streamStartEnd");
// const video = document.getElementsByTagName("video")[0];
const video = document.querySelector("video");
let streaming = false;
let room;
let streamDetails;
let liveNotification = document.createElement("div");
liveNotification.innerHTML = "LIVE";
liveNotification.id = "liveNotification";
const addLocalVideo = async () => {
const videoTrack = await Twilio.Video.createLocalVideoTrack();
const trackElement = videoTrack.attach();
stream.appendChild(trackElement);
};
const startStream = async (streamName, identity) => {
// Create the livestream
const startStreamResponse = await fetch("/start", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
streamName: streamName,
}),
});
streamDetails = await startStreamResponse.json();
const roomId = streamDetails.roomId;
// Get an Access Token
const tokenResponse = await fetch("/streamerToken", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
identity: identity,
room: roomId,
}),
});
const tokenData = await tokenResponse.json();
// Connect to the Video Room
room = await Twilio.Video.connect(tokenData.token);
streaming = true;
stream.insertBefore(liveNotification, video);
startEndButton.disabled = false;
};
const endStream = async () => {
// If streaming, end the stream
if (streaming) {
try {
const response = await fetch("/end", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
streamDetails: streamDetails,
}),
});
const data = await response.json();
room.disconnect();
streaming = false;
liveNotification.remove();
startEndButton.innerHTML = "start stream";
identityInput.disabled = false;
streamNameInput.disabled = false;
} catch (error) {
console.log(error);
}
}
};
const startOrEndStream = async (event) => {
event.preventDefault();
if (!streaming) {
const streamName = streamNameInput.value;
const identity = identityInput.value;
startEndButton.innerHTML = "end stream";
startEndButton.disabled = true;
identityInput.disabled = true;
streamNameInput.disabled = true;
try {
await startStream(streamName, identity);
} catch (error) {
console.log(error);
alert("Unable to start livestream.");
startEndButton.innerHTML = "start stream";
startEndButton.disabled = false;
identityInput.disabled = false;
streamNameInput.disabled = false;
}
} else {
endStream();
}
};
startEndButton.addEventListener("click", startOrEndStream);
window.addEventListener("beforeunload", async (event) => {
event.preventDefault();
await endStream();
e.returnValue = "";
});
addLocalVideo();
view raw streamer.js hosted with ❤ by GitHub

Our Express server will serve this page at the “/stream” endpoint. You can access it at localhost:3333/stream.

We will now create the audience page for our application. This will again need two files, namely, audience.html and audience.js. Create both of the files inside the public directory. The code for the audience page is available below

<!DOCTYPE html>
<html lang="en">
<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" />
<script defer src="../livePlayer/twilio-live-player.min.js"></script>
<script defer src="audience.js" type="text/javascript"></script>
<title>Twilio Livestream | Audience</title>
</head>
<body>
<h1>Audience</h1>
<div id="container">
<div id="player">
<!-- livestream goes here -->
</div>
<button id="streamStartEnd">Watch Stream</button>
</div>
</body>
</html>
view raw audience.html hosted with ❤ by GitHub
const streamPlayer = document.getElementById("player");
const startEndButton = document.getElementById("streamStartEnd");
let player;
let watchingStream = false;
const watchStream = async () => {
try {
const response = await fetch("/audienceToken", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data.message) {
alert(data.message);
return;
}
player = await Twilio.Live.Player.connect(data.token, {
playerWasmAssetsPath: "../livePlayer",
});
player.play();
streamPlayer.appendChild(player.videoElement);
watchingStream = true;
startEndButton.innerHTML = "leave stream";
} catch (error) {
console.log(error);
alert("Unable to connect to livestream");
}
};
const leaveStream = () => {
player.disconnect();
watchingStream = false;
startEndButton.innerHTML = "watch stream";
};
const watchOrLeaveStream = async (event) => {
event.preventDefault();
if (!watchingStream) {
await watchStream();
} else {
leaveStream();
}
};
startEndButton.addEventListener("click", watchOrLeaveStream);
view raw audience.js hosted with ❤ by GitHub

Our Express server will serve this page at the “/watch” endpoint. You can access it at localhost:3333/watch.

There you have it. We have created a live-streaming application using Node.js, Express, Vanilla Javascript, and Twilio Live. Run npm start and open localhost:3333 in the browser to view your webpage. Once npm start is running open terminal and run:

ngrok http 3333

This will create a temporary URL that we can use to access our webpage on another machine. This helps us use one device as a streamer and another as the audience.

This is just a demo, but you can modify it and make it your own!

When you are ready to show off your live-streaming application to the world, you can build, deploy, and host it on Codesphere, the all-in-one development platform that supercharges your development!

Happy Coding!

Top comments (2)

Collapse
 
karfagen profile image
Sofiia Shevchuk • Edited

Useful post, thanks! I want to know more about live streaming app development, can you recommend something similar to this post:cleveroad.com/blog/how-to-build-a-... ?

Collapse
 
simoncodephere profile image
Simon Pfeiffer

Thanks! The article you mentioned is definitely on the more thorough side! I have seen people actually build live streaming apps/websites with low code i.e. on wordpress: elementor.com/resources/how-to/cre... or on bubble.io: youtube.com/watch?v=BQRk1Q2P8PM