Live Streaming with Twilio and Node.js

Today, We will learn the how to create the live streaming with Twilio and Node.js

Live Streaming with Twilio:

Step 1: Create node project:

npm init

Step 2: Install the following packages:

npm i dotenv url path express crypto twilio

Step 3: Create .env file and add the following in it:

TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY_SID=SKXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Step 4: Create server.js  file and add the following in it:

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 = 5000;

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 });

const WHITE_LIST = ["http://localhost:8080", "http://localhost:8081"];

app.use(express.json());

// Serve static files from the public directory
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: "group",
    });

    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) {
    console.log(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
    var playerStreamerId = "";
    if (!req.query.id) {
      const playerStreamerList = await twilioClient.media.playerStreamer.list({
        status: "started",
      });
      const playerStreamer = playerStreamerList.length
        ? playerStreamerList[0]
        : null;
      playerStreamerId = playerStreamer.sid;
      // If no one is streaming, return a message
      if (!playerStreamer) {
        return res.status(200).send({
          message: `No one is streaming right now`,
        });
      }
    }
    else{
      playerStreamerId = req.query.id;
    }
    // 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(playerStreamerId)
      .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 the Express server
app.listen(port, async () => {
  console.log(`Express server running on port ${port}`);
});

Step 5: Create the public/livePlayer folder and add the following files in it:

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

Step 6: Open index.html file and add the following in it:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <title>Livestream Demo</title>
    <link href='https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css' rel='stylesheet'>
  </head>
  <body>
    <div class='container mx-auto mt-20 text-center'>
      <div class='mx-auto'>
          <a href='/stream' class='hover:no-underline hover:text-blue-500 text-xl'>start a livestream</a>
      </div>
      <div class='mx-auto mt-10'>
        <a href='/watch' class='hover:no-underline hover:text-blue-500 text-xl'>watch a livestream</a>
      </div>
    </div>
  </body>
</html>

Step 7: Open stremer.html file and add the following in it:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <title>Livestream Demo | Streamer</title>
    <link href='https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css' rel='stylesheet'>
    <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>
  </head>
  <body>
    <div id='container' class='container mx-auto mt-10 justify-center items-center text-center'>
      <div id='stream' class='flex items-center justify-center w-full'>
        <!-- video will be added here -->
      </div>
      <div id='controls' class='mt-10'>

        <input class='bg-gray-200 appearance-none border-2 border-gray-200 rounded  py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500'
                id='identity'
                type='text'
                placeholder='Your name'
                required>

        <input class='bg-gray-200 appearance-none border-2 border-gray-200 rounded  py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500'
                id='streamName'
                type='text'
                placeholder='Livestream name'
                required>

        <button class='bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-6 mr-2 rounded' id='streamStartEnd'>
          start stream
        </button>

      </div>
    </div>
  </body>
</html>

Step 8: Open streamer.js file and add the following in it:

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];

let streaming = false;
let room;
let streamDetails;

let liveNotification = document.createElement('div');
liveNotification.innerHTML = 'LIVE';
liveNotification.id = 'liveNotification';
liveNotification.classList.add('absolute', 'top-10', 'left-48', 'p-2', 'bg-red-500', 'text-white');

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;
  startEndButton.classList.replace('bg-green-500', 'bg-red-500');
  startEndButton.classList.replace('hover:bg-green-500', 'hover:bg-red-700');
}

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';
      startEndButton.classList.replace('bg-red-500', 'bg-green-500');
      startEndButton.classList.replace('hover:bg-red-500', 'hover:bg-green-700');
      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();

Step 9: Open audience.html file and add following in it:

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <title>Livestream Demo | Audience</title>
    <link href='https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css' rel='stylesheet'>
    <script defer src='./livePlayer/twilio-live-player.min.js'></script>
    <script defer src='audience.js' type='text/javascript'></script>
  </head>
  <body>
    <div class='container mx-auto mt-10 justify-center items-center text-center'>
      <div id='player' class='mx-auto bg-gray-200 h-96 max-w-2xl'>
        <!-- livestream will appear here -->
      </div>

      <button class='bg-green-500 hover:bg-green-700 text-white font-bold mt-10 py-2 px-6 mr-2 rounded' id='streamStartEnd'>
        watch stream
      </button>

    </div>
  </body>
</html>

Step 10: Open audience.html file and following in it:

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';
    startEndButton.classList.replace('bg-green-500', 'bg-red-500');
    startEndButton.classList.replace('hover:bg-green-500', 'hover:bg-red-700');

  } catch (error) {
    console.log(error);
    alert('Unable to connect to livestream');
  }
}

const leaveStream = () => {
  player.disconnect();
  watchingStream = false;
  startEndButton.innerHTML = 'watch stream';
  startEndButton.classList.replace('bg-red-500', 'bg-green-500');
  startEndButton.classList.replace('hover:bg-red-500', 'hover:bg-green-700');
}

const watchOrLeaveStream = async (event) => {
  event.preventDefault();
  if (!watchingStream) {
    await watchStream();
  }
  else {
    leaveStream();
  }
};

startEndButton.addEventListener('click', watchOrLeaveStream);

Code in action:

live-streaming-with-twilio-and-nodejs

 

Submit a Comment

Your email address will not be published. Required fields are marked *

Subscribe

Select Categories