In this guide we will build a video chat application using python+flask in the back-end and React + WebRTC and Metered Video SDK in front-end to build a video calling application.

Our video chat application would allow users to have group video chat, with the ability to share their screen.

The application would run on all modern browsers as well iOS Safari and in Android web browser.

Prerequisite

To build the application we would use Metered Video API and SDK, if you don't have an account, you can signup for account.

Go to https://www.metered.ca/ and click "Signup and Start Building" button.

After  you have create the account, come back here for the next steps.

Application Structure - Backend

Our application would have Python+Flask backend and React Front-End, the backend would provide API to our front-end React Application.

The Application structure of our backend code is very simple, as shown in the screenshot below.

WebRTC with Python Application Structure

We are creating a simple flask application, the project directory contains

flaskr/ - This folder will contain the python code of our flask application

__init__.py - This file contains our Python+Flask Application Code.

venv - Virtual environment folder created using the venv command

.env - This file contains our METERED_SECRET_KEY AND METERED_DOMAIN (I will share more info on how to obtain those below)

requirements.txt - Contains a list of python dependencies required for our project

Building the Backend

We will first build out our Python+Flask backend and then move on to building our front-end using React.

In the backend we will build the our API's that will be required by our front-end application. We will call the Metered REST API from the backend.

We do not want to call the Metered REST API directly from our front-end application because we do not want to expose our METERED_SECRET_KEY in the front-end.

Installing Dependencies

We will use virtual environment to manage dependencies, we will create our project directory and initialize the virtual environment in the project directory.

mkdir myapp
cd myapp
mkdir backend
cd backend
python3 -m venv venv

Create file requirements.txt and add the following

flask
requests
python-dotenv
flask-cors

Run the command to install dependencies

pip install -r requirements.txt

Creating .env file

Create a .env in the root of your project directory and add the following

export FLASK_APP=./flaskr
export METERED_DOMAIN=yourappname.metered.live
export METERED_SECRET_KEY=hoHqpIkn8MqONVIZvwHReHt8tm_6K0SRMgg6vHwPrBoKz

To obtain your METERED_DOMAIN and METERED_SECRET_KEY go to Metered Dashboard -> Developers

Building the Backend REST API

We will create a file named __init__.py inside the flaskr/ folder.

This file will contain our flask code with our REST API that would be needed by our front-end React Application.

We need our backend service to provide primarily 2 services:

  1. Able to create a new meeting room
  2. Validate existing meeting room

So we will be creating the following routes:

  1. /api/create/room - This Endpoint will allow us to create a new meeting room and get the ID of the meeting room
  2. /api/validate-meeting - This Endpoint will accept the roomId and will check if the room exists or not
  3. /api/metered-domain - We will use this endpoint to fetch our Metered Domain from the backed. This is as optional endpoint, you can directly add the Metered Domain in your front-end application, but we are creating an endpoint for flexibility.

Here is the boilerplate code for our backend server, we will go through each route and build it as we go along.

import os
import requests

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

# Metered Secret Key
METERED_SECRET_KEY = os.environ.get("METERED_SECRET_KEY")
# Metered Domain
METERED_DOMAIN = os.environ.get("METERED_DOMAIN")


# API Route to create a meeting room
@app.route("/api/create/room", methods=['POST'])
def create_room():
    return "Create Meeting Room"


# API Route to validate meeting
@app.route("/api/validate-meeting")
def validate_meeting():
    return "Validate Meeting"


# API Route to fetch the Metered Domain
@app.route("/api/metered-domain")
def get_metered_domain():
    return {"METERED_DOMAIN": METERED_DOMAIN}


@app.route("/")
def index():
    return "Backend"
__init__.py

Creating API to Create a Meeting Room

We will use the Metered Create Room API to create a meeting room. Which is /api/v1/room

# API Route to create a meeting room
@app.route("/api/create/room", methods=['POST'])
def create_room():
    r = requests.post("https://"+METERED_DOMAIN + "/api/v1/room" +
                      "?secretKey="+METERED_SECRET_KEY)
    return r.json()
API to Create a Meeting Room

This endpoint returns the following response

{
    "__v": 0,
    "_id": "62a1218be0a28612ff36a9f5",
    "app": "61002fccfa1937440e5d1134",
    "archived": false,
    "audioOnlyRoom": false,
    "autoJoin": false,
    "compositionLayout": "grid",
    "compositionOrientation": "wide",
    "created": "2022-06-08T22:24:11.259Z",
    "deleteOnExp": false,
    "ejectAtRoomExp": false,
    "enableChat": true,
    "enableComposition": false,
    "enableLiveStreaming": false,
    "enableRTMPOut": false,
    "enableRecording": false,
    "enableRequestToJoin": true,
    "enableScreenSharing": true,
    "enableWatermark": false,
    "joinAudioOn": true,
    "joinVideoOn": true,
    "lang": "en",
    "newChatForMeetingSession": true,
    "ownerOnlyBroadcast": false,
    "privacy": "public",
    "recordComposition": false,
    "recordRoom": false,
    "roomName": "jfbkg78pca",
    "showInviteBox": true,
    "watermarkPosition": "bottom_right"
}

For us roomName is the property of interest, each time we will call this API, and if we do not provide a roomName it will create a new room with a unique room name.

If we specify the roomName then it will create a new room of the specified roomName.

But for our use case, the unqiue auto-generated roomName is sufficient.

Creating an API to Validate a Meeting Room

After we have created a meeting room, we need an  API to validate the Meeting Room.

This endpoint will be used validate the room name entered by the user when they are trying to join a room.

Using the API we will check if the room is valid, and if it is valid then we will allow the user to join the room.

# API Route to validate meeting
@app.route("/api/validate-meeting")
def validate_meeting():
    roomName = request.args.get("roomName")
    if roomName:
        r = requests.get("https://" + METERED_DOMAIN + "/api/v1/room/" +
                         roomName + "?secretKey=" + METERED_SECRET_KEY)
        data = r.json()
        if (data.get("roomName")):
            return {"roomFound": True}
        else:
            return {"roomFound": False}
    else:
        return {
            "success": False,
            "message": "Please specify roomName"
        }

API to Fetch Metered Domain

The API to fetch Metered Domain is very straightforward, we will just send the METERED_DOMAIN variable as response.

# API Route to fetch the Metered Domain
@app.route("/api/metered-domain")
def get_metered_domain():
    return {"METERED_DOMAIN": METERED_DOMAIN}

Putting it all together

Here is our final backend service __init__.py

import os
import requests

from flask import Flask, request

app = Flask(__name__)

# Metered Secret Key
METERED_SECRET_KEY = os.environ.get("METERED_SECRET_KEY")
# Metered Domain
METERED_DOMAIN = os.environ.get("METERED_DOMAIN")


# API Route to create a meeting room
@app.route("/api/create/room", methods=['POST'])
def create_room():
    r = requests.post("https://"+METERED_DOMAIN + "/api/v1/room" +
                      "?secretKey="+METERED_SECRET_KEY)
    return r.json()


# API Route to validate meeting
@app.route("/api/validate-meeting")
def validate_meeting():
    roomName = request.args.get("roomName")
    if roomName:
        r = requests.get("https://" + METERED_DOMAIN + "/api/v1/room/" +
                         roomName + "?secretKey=" + METERED_SECRET_KEY)
        data = r.json()
        if (data.get("roomName")):
            return {"roomFound": True}
        else:
            return {"roomFound": False}
    else:
        return {
            "success": False,
            "message": "Please specify roomName"
        }


# API Route to fetch the Metered Domain
@app.route("/api/metered-domain")
def get_metered_domain():
    return {"METERED_DOMAIN": METERED_DOMAIN}


@app.route("/")
def index():
    return "Backend"
__init__.py

Using Metered Pre-Built UI

Instead of building the custom front-end in React we can use the Metered Pre-built UI to Embed Video Chat into your web application.

Metered Embed Video Chat

Your roomURL is simply <your_metered_domain>.metered.live/<your_room_name

Each Room you create in Metered Video can be used with the pre-built UI. Just open the roomURL in your browser and you will be presented with the pre-built UI.

The Metered Pre-Built UI has built-in Chat, Video Calling, and Screen Sharing capabilities and the options can be enabled/disabled using dashboard or using the API.

To Embed the Pre-Built UI into an existing application you can use the following Embed Code.

Just replace the roomURL with your own roomURL.

<div id="metered-frame"></div>
<script src="https://cdn.metered.ca/sdk/frame/1.4.2/sdk-frame.min.js"></script>
<script>
    var frame = new MeteredFrame(); 
    frame.init({
        roomURL: "<your-app-name>.metered.live/<your-room-name>",
    }, document.getElementById("metered-frame"));
</script>

Build the Custom Front-End in React

If you choose to build you custom front-end in React then follow along.

Our front-end application would allow 3 main area:

  1. Join/Create Meeting: Here we will allow the user to join an existing meeting or create a new meeting
  2. Meeting Area: The main meeting interface
  3. Meeting Ended Screen: We will take the user to this area after the meeting has ended.

Installing the dependencies

We will use Create React App to scaffold our single page React application.

cd myapp
npx create-react-app react-frontend

Scaffolding the Application UI

We will create 3 components one for each of the areas:

React Front-End Scaffolding

App.js - Will be the main container of the application

Join.js - UI to Join and Existing Meeting or Create a new Meeting

Meeting.js - Will contain the main Meeting Screen

MeetingEnded.js - Interface to show when the meeting ends

Including the Metered JavaScript SDK

We will include the latest Metered JavaScript in our application.

To add the Metered SDK open public/index.html and paste the SDK before closing the of the head tag

    <script src="//cdn.metered.ca/sdk/video/1.4.3/sdk.min.js"></script>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
      
      <!-- METERED VIDEO SDK -->
     <script src="//cdn.metered.ca/sdk/video/1.4.3/sdk.min.js"></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
index.html

Initializing the SDK

We will initialize the Metered SDK in App.js and handle all the meeting events in App.js

import { useEffect, useState } from "react";
import Join from "./Join";
import Meeting from "./Meeting";

// Initializing the SDK
const meteredMeeting = new window.Metered.Meeting();

function App() {
  // Will set it to true when the user joins the meeting
  // and update the UI.
  const [meetingJoined, setMeetingJoined] = useState(false);
  // Storing onlineUsers, updating this when a user joins
  // or leaves the meeting
  const [onlineUsers, setOnlineUsers] = useState([]);

  // This useEffect hooks will contain all
  // event handler, like participantJoined, participantLeft etc.
  useEffect(() => {}, []);

  // Will call the API to create a new
  // room and join the user.
  function handleCreateMeeting(username) {}

  // Will call th API to validate the room
  // and join the user
  function handleJoinMeeting(roomName, username) {}

  return (
    <div className="App">
      {meetingJoined ? (
        <Meeting onlineUsers={onlineUsers} />
      ) : (
        <Join
          handleCreateMeeting={handleCreateMeeting}
          handleJoinMeeting={handleJoinMeeting}
        />
      )}
    </div>
  );
}

export default App;

Join Meeting Component

Lets build the Join Meeting Component, the Join Meeting component is very simple, it will allow the user to join an existing meeting by entering the roomName or creating a new meeting.

import { useState } from "react";

function Join({ handleCreateMeeting, handleJoinMeeting }) {
  const [username, setUsername] = useState("");
  const [roomName, setRoomName] = useState("");

  return (
    <div id="joinView" className="w-full items-center justify-center flex">
      <div className="bg-base-300 w-11/12 max-w-screen-md  rounded mt-48 p-10">
        <div>
          <label className="label">
            <span className="label-text">Name:</span>
          </label>
          <input
            value={username}
            onChange={(e) => {
              setUsername(e.target.value);
            }}
            type="text"
            className="w-full input input-primary input-bordered"
            placeholder="Enter your name"
          />
        </div>

        <div className="divider">AND</div>

        <div className="form-control">
          <label className="label">
            <span className="label-text">Meeting ID</span>
          </label>
          <div className="relative">
            <input
              value={roomName}
              onChange={(e) => {
                setRoomName(e.target.value);
              }}
              id="meetingId"
              type="text"
              placeholder="Meeting ID"
              className="w-full pr-16 input input-primary input-bordered"
            />
            <button
              id="joinExistingMeeting"
              className="absolute top-0 right-0 rounded-l-none btn btn-primary text-xs"
            >
              <span
                onClick={() => {
                  handleJoinMeeting(roomName, username);
                }}
                className="hidden sm:block"
              >
                Join Existing Meeting
              </span>
              <span className="sm:hidden">Join</span>
            </button>
          </div>
        </div>
        <div className="divider">OR</div>
        <div className="flex justify-center">
          <button
            onClick={() => {
              handleCreateMeeting(username);
            }}
            id="createANewMeeting"
            className="btn btn-primary"
          >
            Create a new meeting
          </button>
        </div>
      </div>
    </div>
  );
}

export default Join;
Join.js

Join.js component

In the Join Meeting Component we are just handling the events and calls the props which has methods from the App Component, and the logic to handle "Join Existing Meeting" and "Create a new Meeting" will be handled in the App Component

Implementing Logic to Create and Join the Meeting

In the App.js we will add the logic to handle the events triggered by pressing the "Join Existing Meeting" and "Create a New Meeting" buttons in the Join Component.

The logic to handleCreateMeeting is very simple, we call our backend API /api/create/room to create a room.

Then we call /api/metered-domain to fetch our Metered Domain.

And finally we call the join method of the Metered Javascript SDK.

  // Will call the API to create a new
  // room and join the user.
  async function handleCreateMeeting(username) {
    // Calling API to create room
    const { data } = await axios.post(API_LOCATION + "/api/create/room");
    // Calling API to fetch Metered Domain
    const response = await axios.get(API_LOCATION + "/api/metered-domain");
    // Extracting Metered Domain and Room Name
    // From responses.
    const METERED_DOMAIN = response.data.METERED_DOMAIN;
    const roomName = data.roomName;

    // Calling the join() of Metered SDK
    const joinResponse = await meteredMeeting.join({
      name: username,
      roomURL: METERED_DOMAIN + "/" + roomName,
    });
    
    // Updating the state meetingJoined to true
    setMeetingJoined(true);
  }
💡
For complete list of options provided by the join() method you can read here

The logic for handleJoinMeeting is also very straightforward, here we already have the roomName which will be provided by the user, we need to validate the roomName and if the roomName is valid then we will call the join method of the Metered JavaScript SDK.

  // Will call th API to validate the room
  // and join the user
  async function handleJoinMeeting(roomName, username) {
    // Calling API to validate the roomName
    const response = await axios.get(
      API_LOCATION + "/api/validate-meeting?roomName=" + roomName
    );

    if (response.data.roomFound) {
      // Calling API to fetch Metered Domain
      const { data } = await axios.get(API_LOCATION + "/api/metered-domain");

      // Extracting Metered Domain and Room Name
      // From responses.
      const METERED_DOMAIN = data.METERED_DOMAIN;

      // Calling the join() of Metered SDK
      const joinResponse = await meteredMeeting.join({
        name: username,
        roomURL: METERED_DOMAIN + "/" + roomName,
      });
      setMeetingJoined(true);
    } else {
      alert("Invalid roomName");
    }
  }

To validate the roomName we will call our backend API /api/validate-meeting?roomName=

The we will be checking if the roomFound is True, if it is True then we will fetch our Metered Domain and call the join() method and update the meetingJoined state variable.

Handling Events

We need to handle the following events in our application:

  1. participantJoined: When a paritcipant joins the meeting this event is triggered, we will add the user to the onlineUsers array.
  2. participantLeft: When a participant leaves the meeting this event is triggered, we will remove the user from the onlineUsers array.
  3. remoteTrackStarted: When a remote participant shares their camera/microphone/screen this event is emitted.
  4. remoteTrackStopped: When a remote participant stops sharing their camera/microphone/screen this event is emitted.
  5. onlineParticipants: This event is emitted multiple times during the lifecycle of the meeting. It contains that array of users currently in the meeting.

We will create a useEffect hook and in the hook to handle the events and return a function that will do the cleanup of the event listener.

  useEffect(() => {
    meteredMeeting.on("remoteTrackStarted", (trackItem) => {});

    meteredMeeting.on("remoteTrackStopped", (trackItem) => {});

    meteredMeeting.on("participantJoined", (localTrackItem) => {});

    meteredMeeting.on("participantLeft", (localTrackItem) => {});

    meteredMeeting.on("onlineParticipants", (onlineParticipants) => {});

    return () => {
      meteredMeeting.removeListener("remoteTrackStarted");
      meteredMeeting.removeListener("remoteTrackStopped");
      meteredMeeting.removeListener("participantJoined");
      meteredMeeting.removeListener("participantLeft");
      meteredMeeting.removeListener("onlineParticipants");
    };
  });

We will create two array as state variables, one array will store the list of onlineParticipants and another array will store the list of remote video and audio tracks.

  const [onlineUsers, setOnlineUsers] = useState([]);

  const [remoteTracks, setRemoteTracks] = useState([]);

  // This useEffect hooks will contain all
  // event handler, like participantJoined, participantLeft etc.
  useEffect(() => {
    meteredMeeting.on("remoteTrackStarted", (trackItem) => {
      remoteTracks.push(trackItem);
      setRemoteTracks([...remoteTracks]);
    });

    meteredMeeting.on("remoteTrackStopped", (trackItem) => {
      for (let i = 0; i < remoteTracks.length; i++) {
        if (trackItem.streamId === remoteTracks[i].streamId) {
          remoteTracks.splice(i, 1);
        }
      }
      setRemoteTracks([...remoteTracks]);
    });

    meteredMeeting.on("participantJoined", (localTrackItem) => {});

    meteredMeeting.on("participantLeft", (localTrackItem) => {});

    meteredMeeting.on("onlineParticipants", (onlineParticipants) => {
      setOnlineUsers([...onlineParticipants]);
    });

    return () => {
      meteredMeeting.removeListener("remoteTrackStarted");
      meteredMeeting.removeListener("remoteTrackStopped");
      meteredMeeting.removeListener("participantJoined");
      meteredMeeting.removeListener("participantLeft");
      meteredMeeting.removeListener("onlineParticipants");
    };
  });

We can show a notification and play a sound when a participant enters or leaves the meeting in the participantJoined and participantLeft event handlers.

The onlineParticipants event handler is triggered each time a participant enters or leaves and meeting and returns the array of participants, so we can use just that event handler to load the list of online participants.

The remoteTrackStarted event handler we are just pushing the remoteTrack item to the remoteVideoTracks array and setting the state.

In the remoteTrackStopped event handler, we are looping through the array to find the remoteTrackItem that was stoppped and and removing it from the array and setting the state.

Displaying the Remote Streams

We have handled the remoteTrackStarted event and we are storing the remote tracks in the remoteTracks state variable. The remote tracks can be played in a videoTag.

The videoTag has an srcObject attribute and we can pass the MediaStream to the srcObject attribute the play the remote streams.

We will create a custom VideoTag component that will accept our mediaStream as prop and create a videoTag with srcObject attribute and will play the video when the stream is ready.

Creating Component to Display MediaStream

The video and audio stream, can be added to a video tag, but they have to added to the srcObject property, to handle this we will create our own <VideoTag /> component where we can provide srcObject as prop and it handles the reset.

import classNames from "classnames";
import { useEffect, useRef } from "react";

function VideoTag(props) {
  const video = useRef();
  const srcObject = props.srcObject;
  const src = props.src;
  const style = props.style;

  const className = classNames(
    "static shadow-lg bg-slate-900 max-w-full max-h-full",
    props.className
  );
  function handleCanPlay() {
    video.current.play();
  }

  useEffect(() => {
    if (srcObject && video.current) {
      video.current.srcObject = srcObject;
    }
  });

  return (
    <>
      <video
        style={style}
        ref={video}
        onCanPlay={handleCanPlay}
        playsInline
        className={className}
        autoPlay={true}
        src={src}
      />
    </>
  );
}

export default VideoTag;
VideoTag.js

This component is very simple, here we have created a useEffect hook and in the hook we can see if srcObject prop has a value, if it has then we are assigning it to the video tag, and we are handling the onCanPlay event emiited by the video tag, and when that event is emitted we are calling play() method of the video tag.

Implementing the Meeting Area

Now we have added the logic to handle the onlineParticipants and their remote tracks, now let's build the Meeting

The Meeting Area is saved in the Meeting.js file.

In the Meeting Area we will show the video/audio of the remote participants, add the ability to allow the user to share his/her microphone, camera and screen, and show the user their own video if they are sharing camera/screen.

In our App.js component we will check if the user has joined the Meeting, if yes then we will show the Meeting component. If the user has not joined the meeting then we will show the Join Component.

We will also pass the onlineUsers and remoteTracks as props to the Meeting.js component, and also methods to handle the camera, screen, microphone button click events.

  return (
    <div className="App">
      {meetingJoined ? (
        <Meeting
          handleMicBtn={handleMicBtn}
          handleCameraBtn={handleCameraBtn}
          handelScreenBtn={handelScreenBtn}
          handleLeaveBtn={handleLeaveBtn}
          localVideoStream={localVideoStream}
          onlineUsers={onlineUsers}
          remoteTracks={remoteTracks}
          username={username}
          roomName={roomName}
          meetingInfo={meetingInfo}
        />
      ) : (
        <Join
          handleCreateMeeting={handleCreateMeeting}
          handleJoinMeeting={handleJoinMeeting}
        />
      )}
    </div>
  );
App.js render

We have scaffold out the Meeting.js Component

import VideoTag from "./VideoTag";

function Meeting({
  handleMicBtn,
  handleCameraBtn,
  handelScreenBtn,
  handleLeaveBtn,
  localVideoStream,
  onlineUsers,
  remoteTracks,
  username,
  roomName,
  meetingInfo,
}) {
  let userStreamMap = {};
  for (let trackItem of remoteTracks) {
    if (!userStreamMap[trackItem.participantSessionId]) {
      userStreamMap[trackItem.participantSessionId] = [];
    }
    userStreamMap[trackItem.participantSessionId].push(trackItem);
  }

  let remoteParticipantTags = [];
  for (let user of onlineUsers) {
    // Skip if self
    if (user._id === meetingInfo.participantSessionId) {
      continue;
    }
    let videoTags = [];
    if (userStreamMap[user._id] && userStreamMap[user._id].length > 0) {
      // User has remote tracks
      for (let trackItem of userStreamMap[user._id]) {
        let stream = new MediaStream();
        stream.addTrack(trackItem.track);

        if (trackItem.type === "video") {
          videoTags.push(<VideoTag srcObject={stream} />);
        }

        if (trackItem.type === "audio") {
          videoTags.push(
            <VideoTag
              key={trackItem.streamId}
              srcObject={stream}
              style={{ display: "none" }}
            />
          );
        }
      }
    }

    remoteParticipantTags.push(
      <div key={user._id}>
        <div id="remoteVideos">{videoTags}</div>
        <div id="username">{user.name}</div>
      </div>
    );
  }

  return (
    <div id="meetingView" className="flex flex-col">
      <div className="h-8 text-center bg-black">MeetingID: {roomName}</div>
      <div
        className="flex-1 grid grid-cols-2 grid-rows-2"
        id="remoteParticipantContainer"
        style={{ display: "flex" }}
      >
        {remoteParticipantTags}
      </div>

      <div className="flex flex-col bg-base-300" style={{ width: "150px" }}>
        {localVideoStream ? (
          <VideoTag
            id="meetingAreaLocalVideo"
            muted={true}
            srcObject={localVideoStream}
            style={{
              padding: 0,
              margin: 0,
              width: "150px",
              height: "100px",
            }}
          />
        ) : (
          ""
        )}

        <div
          id="meetingAreaUsername"
          className="bg-base-300 bg-black"
          style={{
            textAlign: "center",
          }}
        >
          {username}
        </div>
      </div>

      <div
        style={{
          display: "flex",
          justifyContent: "center",
          marginTop: "20px",
        }}
        className="space-x-4"
      >
        <button
          id="meetingViewMicrophone"
          className="btn"
          onClick={handleMicBtn}
        >
          <svg
            className="w-6 h-6"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
            />
          </svg>
        </button>

        <button
          id="meetingViewCamera"
          className="btn"
          onClick={handleCameraBtn}
        >
          <svg
            className="w-6 h-6"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
            />
          </svg>
        </button>

        <button
          id="meetingViewScreen"
          className="btn"
          onClick={handelScreenBtn}
        >
          <svg
            className="w-6 h-6"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
            />
          </svg>
        </button>

        <button id="meetingViewLeave" className="btn" onClick={handleLeaveBtn}>
          <svg
            className="w-6 h-6"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
            />
          </svg>
        </button>
      </div>
    </div>
  );
}

export default Meeting;

Handling the sharing of camera, microphone and screen

In App.js we create the methods to handle the click events on Microphone, Camera, Screen and Leave Meeting Buttons.

Microphone, Camera, Screen and Leave Meeting Buttons

We will call the methods from the Metered Video SDK to handle the click events:

  async function handleMicBtn() {
    if (micShared) {
      await meteredMeeting.stopAudio();
      setMicShared(false);
    } else {
      await meteredMeeting.startAudio();
      setMicShared(true);
    }
  }

  async function handleCameraBtn() {
    if (cameraShared) {
      await meteredMeeting.stopVideo();
      setLocalVideoStream(null);
      setCameraShared(false);
    } else {
      await meteredMeeting.startVideo();
      var stream = await meteredMeeting.getLocalVideoStream();
      setLocalVideoStream(stream);
      setCameraShared(true);
    }
  }

  async function handelScreenBtn() {
    if (!screenShared) {
      await meteredMeeting.startScreenShare();
      setScreenShared(false);
    } else {
      await meteredMeeting.stopVideo();
      setCameraShared(false);
      setScreenShared(true);
    }
  }

  async function handleLeaveBtn() { }

Building Meeting Ended/Leave Meeting Screen

To build the Meeting Ended screen, we will create a state variable called meetingEnded and in the handleLeaveBtn() method we will set it to true, and call the leaveMeeting() method of Metered Video SDK.

  async function handleLeaveBtn() {
    await meteredMeeting.leaveMeeting();
    setMeetingEnded(true);
  }

Then we will check if meetingEnded is true and if it is true then we will hide the Meeting component and show the MeetingEnded.js component instead.

That's it!

This is how our final App.js file looks like:

import axios from "axios";
import { useEffect, useState } from "react";
import Join from "./Join";
import Meeting from "./Meeting";
import MeetingEnded from "./MeetingEnded";

// Initializing the SDK
const meteredMeeting = new window.Metered.Meeting();

const API_LOCATION = "http://localhost:5000";

function App() {
  // Will set it to true when the user joins the meeting
  // and update the UI.
  const [meetingJoined, setMeetingJoined] = useState(false);
  // Storing onlineUsers, updating this when a user joins
  // or leaves the meeting
  const [onlineUsers, setOnlineUsers] = useState([]);

  const [remoteTracks, setRemoteTracks] = useState([]);

  const [username, setUsername] = useState("");

  const [localVideoStream, setLocalVideoStream] = useState(null);

  const [micShared, setMicShared] = useState(false);
  const [cameraShared, setCameraShared] = useState(false);
  const [screenShared, setScreenShared] = useState(false);
  const [meetingEnded, setMeetingEnded] = useState(false);
  const [roomName, setRoomName] = useState(null);
  const [meetingInfo, setMeetingInfo] = useState({});
  // This useEffect hooks will contain all
  // event handler, like participantJoined, participantLeft etc.
  useEffect(() => {
    meteredMeeting.on("remoteTrackStarted", (trackItem) => {
      remoteTracks.push(trackItem);
      setRemoteTracks([...remoteTracks]);
    });

    meteredMeeting.on("remoteTrackStopped", (trackItem) => {
      for (let i = 0; i < remoteTracks.length; i++) {
        if (trackItem.streamId === remoteTracks[i].streamId) {
          remoteTracks.splice(i, 1);
        }
      }
      setRemoteTracks([...remoteTracks]);
    });

    meteredMeeting.on("participantJoined", (localTrackItem) => {});

    meteredMeeting.on("participantLeft", (localTrackItem) => {});

    meteredMeeting.on("onlineParticipants", (onlineParticipants) => {
      setOnlineUsers([...onlineParticipants]);
    });

    meteredMeeting.on("localTrackUpdated", (item) => {
      const stream = new MediaStream(item.track);
      setLocalVideoStream(stream);
    });

    return () => {
      meteredMeeting.removeListener("remoteTrackStarted");
      meteredMeeting.removeListener("remoteTrackStopped");
      meteredMeeting.removeListener("participantJoined");
      meteredMeeting.removeListener("participantLeft");
      meteredMeeting.removeListener("onlineParticipants");
      meteredMeeting.removeListener("localTrackUpdated");
    };
  });

  // Will call the API to create a new
  // room and join the user.
  async function handleCreateMeeting(username) {
    // Calling API to create room
    const { data } = await axios.post(API_LOCATION + "/api/create/room");
    // Calling API to fetch Metered Domain
    const response = await axios.get(API_LOCATION + "/api/metered-domain");
    // Extracting Metered Domain and Room Name
    // From responses.
    const METERED_DOMAIN = response.data.METERED_DOMAIN;
    const roomName = data.roomName;

    // Calling the join() of Metered SDK
    const joinResponse = await meteredMeeting.join({
      name: username,
      roomURL: METERED_DOMAIN + "/" + roomName,
    });

    setUsername(username);
    setRoomName(roomName);
    setMeetingInfo(joinResponse);
    setMeetingJoined(true);
  }

  // Will call th API to validate the room
  // and join the user
  async function handleJoinMeeting(roomName, username) {
    // Calling API to validate the roomName
    const response = await axios.get(
      API_LOCATION + "/api/validate-meeting?roomName=" + roomName
    );

    if (response.data.roomFound) {
      // Calling API to fetch Metered Domain
      const { data } = await axios.get(API_LOCATION + "/api/metered-domain");

      // Extracting Metered Domain and Room Name
      // From responses.
      const METERED_DOMAIN = data.METERED_DOMAIN;

      // Calling the join() of Metered SDK
      const joinResponse = await meteredMeeting.join({
        name: username,
        roomURL: METERED_DOMAIN + "/" + roomName,
      });

      setUsername(username);
      setRoomName(roomName);
      setMeetingInfo(joinResponse);

      setMeetingJoined(true);
    } else {
      alert("Invalid roomName");
    }
  }

  async function handleMicBtn() {
    if (micShared) {
      await meteredMeeting.stopAudio();
      setMicShared(false);
    } else {
      await meteredMeeting.startAudio();
      setMicShared(true);
    }
  }

  async function handleCameraBtn() {
    if (cameraShared) {
      await meteredMeeting.stopVideo();
      setLocalVideoStream(null);
      setCameraShared(false);
    } else {
      await meteredMeeting.startVideo();
      var stream = await meteredMeeting.getLocalVideoStream();
      setLocalVideoStream(stream);
      setCameraShared(true);
    }
  }

  async function handelScreenBtn() {
    if (!screenShared) {
      await meteredMeeting.startScreenShare();
      setScreenShared(false);
    } else {
      await meteredMeeting.stopVideo();
      setCameraShared(false);
      setScreenShared(true);
    }
  }

  async function handleLeaveBtn() {
    await meteredMeeting.leaveMeeting();
    setMeetingEnded(true);
  }

  return (
    <div className="App">
      {meetingJoined ? (
        meetingEnded ? (
          <MeetingEnded />
        ) : (
          <Meeting
            handleMicBtn={handleMicBtn}
            handleCameraBtn={handleCameraBtn}
            handelScreenBtn={handelScreenBtn}
            handleLeaveBtn={handleLeaveBtn}
            localVideoStream={localVideoStream}
            onlineUsers={onlineUsers}
            remoteTracks={remoteTracks}
            username={username}
            roomName={roomName}
            meetingInfo={meetingInfo}
          />
        )
      ) : (
        <Join
          handleCreateMeeting={handleCreateMeeting}
          handleJoinMeeting={handleJoinMeeting}
        />
      )}
    </div>
  );
}

export default App;

Conclusion

We have successfully built the group video calling application with Python Backend and React front-end.

You can grab the complete source code from Github:  https://github.com/metered-ca/python-react-video-chat-app

The application is also avaliable as Docker Containers:

Backend: https://hub.docker.com/r/metered/python-video-demo

Frontend: https://hub.docker.com/r/metered/react-video-demo

Metered TURN Servers

Metered TURN servers

  1. API: TURN server management with powerful API. You can do things like Add/ Remove credentials via the API, Retrieve Per User / Credentials and User metrics via the API, Enable/ Disable credentials via the API, Retrive Usage data by date via the API.
  2. Global Geo-Location targeting: Automatically directs traffic to the nearest servers, for lowest possible latency and highest quality performance. less than 50 ms latency anywhere around the world
  3. Servers in 12 Regions of the world: Toronto, Miami, San Francisco, Amsterdam, London, Frankfurt, Bangalore, Singapore,Sydney (Coming Soon: South Korea, Japan and Oman)
  4. Low Latency: less than 50 ms latency, anywhere across the world.
  5. Cost-Effective: pay-as-you-go pricing with bandwidth and volume discounts available.
  6. Easy Administration: Get usage logs, emails when accounts reach threshold limits, billing records and email and phone support.
  7. Standards Compliant: Conforms to RFCs 5389, 5769, 5780, 5766, 6062, 6156, 5245, 5768, 6336, 6544, 5928 over UDP, TCP, TLS, and DTLS.
  8. Multi‑Tenancy: Create multiple credentials and separate the usage by customer, or different apps. Get Usage logs, billing records and threshold alerts.
  9. Enterprise Reliability: 99.999% Uptime with SLA.
  10. Enterprise Scale: With no limit on concurrent traffic or total traffic. Metered TURN Servers provide Enterprise Scalability
  11. 50 GB/mo Free: Get 50 GB every month free TURN server usage with the Free Plan
  12. Runs on port 80 and 443
  13. Support TURNS + SSL to allow connections through deep packet inspection firewalls.
  14. Support STUN
  15. Supports both TCP and UDP