Back to blog

Building Your Own Live Streaming Platform: A Step-by-Step Guide to Using DyteSDK

Building Your Own Live Streaming Platform: A Step-by-Step Guide to Using DyteSDK

TL;DR

By the end of this guide, you'll have your own live-streaming platform up and running, powered by DyteSDK.

Introduction

Since the launch of Twitch in 2011, all major social media platforms including Facebook, Instagram, YouTube, etc. have been integrating livestreaming capabilities into their applications.

However, building a live streaming platform involves too many moving parts for a small team to handle effectively.

In this tutorial, we will use DyteSDK to create our own Twitch-like livestreaming platform. Whether the aim is to create a niche platform for gamers, a hub for professional webinars, or a creative space for artists, DyteSDK makes developing live-streaming applications effortlessly.

Source: devteamlife.com

High-Level Design of the application

Our aim is to create an application in which the user can scroll through his feed and watch the live streams. He would be able to interact in various ways with the stream. Moreover, he would be able to create his own livestream with a custom thumbnail.

User Journey Diagram
High Level Overview

Folder Structure

After completing the tutorial, the folder structure will look like this .

.
├── app.py
├── frontend
│   ├── package.json
│   ├── public
│   ├── src
│   │   ├── App.css
│   │   ├── App.jsx
│   │   ├── Heading.jsx
│   │   ├── Home.jsx
│   │   ├── ImageInput.jsx
│   │   ├── Livestreams
│   │   │   ├── LiveStreamInteraction.jsx
│   │   │   ├── LivestreamBody.jsx
│   │   │   ├── LivestreamHeader.jsx
│   │   │   ├── LivestreamHome.jsx
│   │   │   └── assets
│   │   │       └── dytestream.png
│   │   ├── Meet.jsx
│   │   ├── Proctor.jsx
│   │   ├── Stage.jsx
│   │   ├── index.css
│   │   ├── index.tsx
│   │   ├── logo.svg
│   │   └── utils.js
│   ├── tsconfig.json
│   └── yarn.lock
├── imgur.py
├── requirements.txt
├── setEnvs.py
└── utils.py

Creating the Backend

Let's first start with setting up our backend.

Step 1: Setting Up the Environment

First, we need to create a virtual environment and activate it using venv:

python -m venv venv
source venv/bin/activate

Next, we will add the necessary dependencies to our requirements.txt file:

cmake
fastapi
uvicorn
face_recognition
numpy
python-multipart
psycopg2-binary
httpx
python-dotenv
pydantic
requests

Now let's go ahead and install all these dependencies

pip install -r requirements.txt
Setting up the backend

Step 2: External APIs Integration

You would now be required to create a Dyte account and get your API keys.

Once signed up, you will be able to access your Dyte API keys from the "API Keys" tab in the left sidebar. Remember to keep these keys secure, as we will use them later 🤫.

Creating a Dyte Account

📝 NOTE

You would also need to create accounts on the following platforms before proceeding:


Next, we will create a .env file in our root directory and add the following environment variables to it:

.env

DYTE_ORG_ID=********-****-****-****-************
DYTE_API_KEY=********************
IMGUR_CLIENT_ID=***************
DB_USER=********
DB_PASSWORD=********************************
DB_HOST=xyz.db.elephantsql.com

Once we are good with our API keys, we will go ahead and create a module to set up Imgur API. 🔌

imgur.py

import base64
from fastapi import FastAPI, UploadFile, HTTPException
from httpx import AsyncClient
from dotenv import load_dotenv
import os

load_dotenv()

app = FastAPI()
IMGUR_CLIENT_ID = os.getenv("IMGUR_CLIENT_ID")

async def upload_image(img_data):
    headers = {
        "Authorization": f"Client-ID {IMGUR_CLIENT_ID}"
    }
    data = {
        "image": img_data
    }

    async with AsyncClient() as client:
        response = await client.post("https://api.imgur.com/3/image", headers=headers, data=data)

    if response.status_code != 200:
        raise HTTPException(status_code=500, detail="Could not upload image.")

    print(response.json())
    return response.json()["data"]["link"]

Step 3: Crating Backend Routes

Now we will go ahead and write the following API routes in our app.py file, which would serve as the entry point of our backend.

GET / - Basic health check, ensuring the server is up and running.

POST /is_admin - Validates if a user is an admin.

POST /meetings - Handles the creation of new meetings using Dyte API using the payload sent from frontend.

POST /meetings/{meetingId}/participants : Adds a participant to a meeting.

POST /get_livestreams : Fetches available live streams.

POST /vote/{meeting_id} : Creates votes table and increments likes count for the given meeting_id in it.

GET /stats : Fetches number of likes for all the meeting.

POST /viewers_count/{meeting_id} : Creates viewers_count table increments views count for the given meeting_id in it.

GET /viewers_count : Retrieves views count for all the meetings.

POST /img_link_upload/{meeting_id} : Creates ls_metadata table and adds metadata such as thumbnail url (img_url) and title

GET /img_link_upload : Fetch metadata of all livestreams from ls_metadata table.

app.py

import base64
import io
import logging
import random
import requests

import uvicorn
from fastapi import FastAPI, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from imgur import upload_image
from utils import generate_a_name
import psycopg2

import os
import base64
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from dotenv import load_dotenv
from httpx import AsyncClient
import uuid

load_dotenv()

DYTE_API_KEY = os.getenv("DYTE_API_KEY")
DYTE_ORG_ID = os.getenv("DYTE_ORG_ID")

API_HASH = base64.b64encode(f"{DYTE_ORG_ID}:{DYTE_API_KEY}".encode('utf-8')).decode('utf-8')

DYTE_API = AsyncClient(base_url='https://api.cluster.dyte.in/v2', headers={'Authorization': f"Basic {API_HASH}"})

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

fh = logging.FileHandler("app.log")
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
fh.setFormatter(formatter)
logger.addHandler(fh)


class ParticipantScreen(BaseModel):
    audio_file: UploadFile
    participant_id: str
    meeting_id: str
    participant_name: str

class ProctorPayload(BaseModel):
    meeting_id: str
    admin_id: str

class AdminProp(BaseModel):
    meeting_id: str
    admin_id: str

class Meeting(BaseModel):
    title: str

class Participant(BaseModel):
    name: str
    preset_name: str
    meeting_id: str

origins = [
    # allow all
    "*",
]

app = FastAPI()

# enable cors
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],  # allow all
    allow_headers=["*"],  # allow all
)

def connect_to_db():
    conn = psycopg2.connect(
            dbname=os.getenv('DB_USER'),
            user=os.getenv('DB_USER'),
            password=os.getenv('DB_PASSWORD'),
            host=os.getenv('DB_HOST'),
            port=5432
    )
    return conn

@app.get("/")
async def root():
    return {"message": "Hello World"}

@app.post("/is_admin/")
async def multiple_faces_list(admin: AdminProp):
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("SELECT count(1) FROM meeting_host_info WHERE meeting_id = %s AND admin_id = %s", (admin.meeting_id, admin.admin_id,))

    count = cur.fetchone()[0]

    if(count > 0):
        return { "admin": True }
    else:
        return { "admin": False }

@app.post("/meetings")
async def create_meeting(meeting: Meeting):
    payload = meeting.dict()
    # payload.update({"live_stream_on_start": True})
    response = await DYTE_API.post('/meetings', json=payload)
    if response.status_code >= 300:
        raise HTTPException(status_code=response.status_code, detail=response.text)
    admin_id = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=32)) + '@@' + generate_a_name()
    resp_json = response.json()
    resp_json['admin_id'] = admin_id
    meeting_id = resp_json['data']['id']

    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("INSERT INTO meeting_host_info (ts, meeting_id, admin_id) VALUES (CURRENT_TIMESTAMP, %s, %s)", (meeting_id, admin_id))
    conn.commit()
    cur.close()
    conn.close()

    return resp_json


@app.post("/meetings/{meetingId}/participants")
async def add_participant(meetingId: str, participant: Participant):
    client_specific_id = f"react-samples::{participant.name.replace(' ', '-')}-{str(uuid.uuid4())[0:7]}"
    payload = participant.dict()
    payload.update({"client_specific_id": client_specific_id})
    del payload['meeting_id']
    resp = await DYTE_API.post(f'/meetings/{meetingId}/participants', json=payload)
    if resp.status_code > 200:
        raise HTTPException(status_code=resp.status_code, detail=resp.text)
    return resp.text

class LivestreamsPayload(BaseModel):
    offset: str

@app.post("/get_livestreams")
async def get_livestreams(offset: LivestreamsPayload):
    path = '/livestreams?limit=100&'
    if offset.offset != '0':
        path = path + f'offset={offset.offset}'
    response = await DYTE_API.get(path)
    if response.status_code > 200:
        raise HTTPException(status_code=response.status_code, detail=response.text)
    return response.json()

@app.post("/vote/{meeting_id}")
async def increment_vote(meeting_id: str):
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS votes(ts TIMESTAMP DEFAULT current_timestamp, meeting_id VARCHAR(100) UNIQUE NOT NULL, likes INT DEFAULT 0, dislikes INT DEFAULT 0)")
    payload = {}
    cur.execute("INSERT INTO votes(ts, meeting_id, likes, dislikes) VALUES(current_timestamp, %s, 1, 0) ON CONFLICT(meeting_id) DO UPDATE SET likes = votes.likes + 1 WHERE votes.meeting_id = %s", (meeting_id, meeting_id,))
    payload = {"message": "Vote incremented successfully"}
    conn.commit()
    cur.close()
    conn.close()
    return payload

@app.get("/stats")
async def stats():
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS votes(ts TIMESTAMP DEFAULT current_timestamp, meeting_id VARCHAR(100) UNIQUE NOT NULL, likes INT DEFAULT 0, dislikes INT DEFAULT 0)")
    cur.execute("SELECT likes, meeting_id FROM votes")
    row = cur.fetchall()
    return row

@app.post("/viewers_count/{meeting_id}")
async def viewers_count(meeting_id: str):
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS viewers_count(ts TIMESTAMP DEFAULT current_timestamp, meeting_id VARCHAR(100) UNIQUE NOT NULL, views INT DEFAULT 0)")
    cur.execute("INSERT INTO viewers_count(ts, meeting_id, views) VALUES(current_timestamp, %s, 1) ON CONFLICT(meeting_id) DO UPDATE SET views = viewers_count.views + 1 WHERE viewers_count.meeting_id = %s", (meeting_id, meeting_id,))
    conn.commit()
    cur.close()
    conn.close()
    return {"message": "success"}

@app.get("/viewers_count")
async def viewers_count_get():
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS viewers_count(ts TIMESTAMP DEFAULT current_timestamp, meeting_id VARCHAR(100), views INT DEFAULT 0)")
    cur.execute("SELECT meeting_id, views FROM viewers_count")
    rows = cur.fetchall()
    if rows == None:
        rows =[[]]
    return rows


class ImageLinkUploads(BaseModel):
    image_url: str
    title: str

@app.post("/img_link_upload/{meeting_id}")
async def upload_metadata(meeting_id: str, image_props: ImageLinkUploads):
    image_url = image_props.image_url
    title = image_props.title
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS ls_metadata(ts TIMESTAMP DEFAULT current_timestamp, meeting_id VARCHAR(100), img_url VARCHAR(256), title VARCHAR(100))")
    cur.execute("INSERT INTO ls_metadata (ts, meeting_id, img_url, title) VALUES (current_timestamp, %s, %s, %s)", (meeting_id, image_url, title,))
    conn.commit()
    cur.close()
    conn.close()
    return {"success": True}


@app.get("/img_link_upload")
async def fetch_metadata():
    conn = connect_to_db()
    cur = conn.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS ls_metadata(ts TIMESTAMP DEFAULT current_timestamp, meeting_id VARCHAR(100), img_url VARCHAR(256), title VARCHAR(100))")
    cur.execute("SELECT img_url, meeting_id, title FROM ls_metadata")
    rows = cur.fetchall()
    conn.commit()
    cur.close()
    conn.close()
    return rows


if __name__ == "__main__":
    uvicorn.run("app:app", host="localhost", port=8000, log_level="debug", reload=True)

Creating the Frontend

Step 1: Initiating the project

Let's start with setting up a new React project in frontend directory.

npx create-react-app frontend
cd frontend

Now we would need to install the required packages

npm install @dytesdk/react-web-core @dytesdk/react-ui-kit @chakra-ui/react react-icons react-router react-router-dom dotenv
Initiating Frontend

Step 2: Adding Components

Now let's move towards our components. First, we would create the Home component.

The Home component in our platform helps our users with creating livestreams.

Home.jsx

import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import ImageUploader from "./ImageInput";

const SERVER_URL = process.env.REACT_APP_SERVER_URL || "http://localhost:8000";

function Home() {
  const [meetingId, setMeetingId] = useState();
  const [isThumbNailUploaded, setThumbnailUploaded] = useState(null);

  const createMeeting = async () => {
    const res = await fetch(`${SERVER_URL}/meetings`, {
      method: "POST",
      body: JSON.stringify({ title: "Dyte Stream" }),
      headers: { "Content-Type": "application/json" },
    });
    const resJson = await res.json();
    window.localStorage.setItem("adminId", resJson.admin_id);
    setMeetingId(resJson.data.id);
  };

  useEffect(() => {
    const id = window.location.pathname.split("/")[2];
    if (!!!id) {
      createMeeting();
    }
  }, []);

  return (
    <div
      style={{
        height: "100vh",
        width: "100vw",
        fontSize: "x-large",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        color: "gray",
        backgroundColor: "white",
      }}
    >
      {meetingId && !window.location.pathname.split("/")[2] && (
        <>
          <div>
            {isThumbNailUploaded ? (
              meetingId && (
                <Link to={`/meeting/${meetingId}`}>
                  Enter Meeting and start livestream
                </Link>
              )
            ) : meetingId ? (
              <ImageUploader
                meetingId={meetingId}
                setImgUploadedStatus={setThumbnailUploaded}
              />
            ) : (
              <>Something bad happened, try reloading</>
            )}
          </div>
        </>
      )}
    </div>
  );
}

export default Home;
Creating a new livestream

Let's now create ImageInput component, which will help users to upload the thumbnail and then to store it's URL.

ImageInput.jsx


import { useState } from "react";
import { Input } from "@chakra-ui/react";

const SERVER_URL = process.env.REACT_APP_SERVER_URL || "http://localhost:8000";

function ImageUploader({ setImgUploadedStatus, meetingId }) {
  const [selectedFile, setSelectedFile] = useState(null);
  const [uploading, setUploading] = useState(false);
  const [uploadError, setUploadError] = useState(null);
  const [uploadSuccess, setUploadSuccess] = useState(false);
  const [title, setTitle] = useState("demo livestream");

  const handleFileChange = (event) => {
    setSelectedFile(event.target.files[0]);
    setUploadSuccess(false);
    setUploadError(null);
  };

  const sendImgLinkToServer = async (imageUrl) => {
    await fetch(`${SERVER_URL}/img_link_upload/${meetingId}`, {
      method: "POST",
      body: JSON.stringify({ image_url: imageUrl, title: title }),
      headers: { "Content-Type": "application/json" },
    });
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    setUploading(true);
    setUploadError(null);

    try {
      const formData = new FormData();
      formData.append("image", selectedFile);

      const response = await fetch("https://api.imgur.com/3/image", {
        method: "POST",
        headers: {
          Authorization: "Client-ID 48f0caef7256b40",
        },
        body: formData,
      });

      if (!response.ok) {
        throw new Error("Failed to upload image");
      }

      const data = await response.json();
      sendImgLinkToServer(data.data.link);
      setUploadSuccess(data.data.link);
      setImgUploadedStatus(true);
    } catch (error) {
      setUploadError(error.message);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        flexDirection: "column",
        alignItems: "center",
        backgroundColor: "white",
      }}
    >
      <div style={{ marginBottom: "10px", color: "black" }}>
        Create a new livestream{" "}
      </div>

      <div>
        <span style={{ fontSize: "0.6em", color: "black" }}>
          {"Upload Thumbnail* "}
        </span>
        <br />

        <input
          type="file"
          id="file"
          onChange={handleFileChange}
          style={{ marginBottom: "20px" }}
        />
        <br />
        <span style={{ fontSize: "0.6em", marginTop: "20px", color: "black" }}>
          {"Livestream Title* "}
        </span>
        <br />
        <Input
          placeholder="Title of the livestream"
          sx={{ color: "white", marginTop: "10px", color: "black" }}
          value={title}
          onInput={(e) => {
            setTitle(e.target.value);
          }}
        />
        <form
          onSubmit={handleSubmit}
          style={{
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            marginTop: "1rem",
          }}
        >
          <button
            type="submit"
            disabled={!selectedFile || uploading || uploadSuccess}
            style={{
              marginTop: "10px",
              padding: "0.5rem 1rem",
              backgroundColor: "#2060FD",
              color: "#fff",
              border: "none",
              borderRadius: "0.25rem",
              cursor: "pointer",
            }}
          >
            {uploading ? "Starting..." : "Start"}
          </button>
        </form>
      </div>
      {uploadError && (
        <div style={{ marginTop: "10px", color: "red" }}>{uploadError}</div>
      )}
    </div>
  );
}

export default ImageUploader;

Next, we will create a component for the header of our application which will have our logo and an option to start a new livestream.

LiveStreamHeader.jsx

Moving on to the LiveStreamBody component which is responsible for displaying the feed on the main page, including a list of available livestreams with thumbnails, titles, and viewer interaction data.

LiveStreamBody.jsx

/* eslint-disable jsx-a11y/alt-text */
/* eslint-disable no-unused-vars */
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { getLivestreams } from "../utils";

import { Spinner } from "@chakra-ui/react";

const SERVER_URL = process.env.REACT_APP_SERVER_URL || "http://localhost:8000";

const LivestreamBody = () => {
  const [offset, setOffset] = useState(0);
  const [streams, setStreams] = useState([]);

  const setLivestreamsToState = async () => {
    const { data } = await getLivestreams(offset);
    const rawThumbnailsDataRes = await fetch(`${SERVER_URL}/img_link_upload`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
    });
    const thumbnails = await rawThumbnailsDataRes.json();
    const upvotesRawRes = await fetch(`${SERVER_URL}/stats`, {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    });
    const upvotes = await upvotesRawRes.json();
    console.log(upvotes);
    const livestreamWithThumbnails = data.map((item) => ({
      ...item,
      upvotes: upvotes.filter(
        (subItem) => item.meeting_id === subItem[1]
      )[0] || [0],
      name: thumbnails.filter(
        (subItem) => item.meeting_id === subItem[1]
      )[0] || [undefined, undefined, undefined],
      thumbnail: thumbnails.filter(
        (subItem) => item.meeting_id === subItem[1]
      )[0] || [undefined],
    }));
    const rawViewsCountData = await fetch(`${SERVER_URL}/viewers_count`, {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    });
    const views = await rawViewsCountData.json();
    const streams = livestreamWithThumbnails.map((item) => ({
      ...item,
      views: views.filter((subItem) => item.meeting_id === subItem[0])[0] || [
        0,
      ],
    }));
    console.log(streams);
    setStreams(streams);
    setOffset((cur) => {
      return cur + 20 < data.total ? cur + 20 : "END";
    });
  };

  const handleClick = (meetingId) => {
    fetch(`${SERVER_URL}/viewers_count/${meetingId}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
    });
  };

  useEffect(() => {
    setLivestreamsToState();
  }, []);

  return (
    <div style={{ backgroundColor: "white" }}>
      <div
        style={{
          fontSize: "1.3em",
          textAlign: "left",
          paddingLeft: "50px",
          fontWeight: "bold",
          paddingTop: "20px",
          color: "black",
        }}
      >
        Newest <span style={{ color: "red" }}>Live</span> Videos
      </div>
      <div
        style={{
          display: "flex",
          padding: "0px 20px 10px 40px",
          justifyContent: "flex-start",
          flexWrap: "wrap",
        }}
      >
        {streams.length ? (
          streams.map((item) => {
            return (
              item.meeting_id && (
                <Link
                  style={{ textDecoration: "none" }}
                  onClick={() => handleClick(item.meeting_id)}
                  to={`/meeting/${item.meeting_id}`}
                >
                  <div style={{ margin: "20px 10px" }}>
                    <div style={{ position: "relative" }}>
                      <img
                        src={
                          item.thumbnail[0] ||
                          "https://m.media-amazon.com/images/M/MV5BYzBiYjlhNGEtNjJkNi00NDc0LWIyMDMtMTg0NDUwZjcxNmY4XkEyXkFqcGdeQXVyMjg2MTMyNTM@._V1_.jpg"
                        }
                        height={"200px"}
                        style={{
                          aspectRatio: "1920/1080",
                          borderRadius: "0px",
                          border: "solid 0.5px gray",
                        }}
                      />
                      <div
                        style={{
                          margin: "4px 4px 4px 6px",
                          fontSize: "small",
                          backgroundColor:
                            item.status === "LIVE" ? "red" : "gray",
                          padding: "1px 8px",
                          borderRadius: "2px",
                          position: "absolute",
                          top: "5px",
                          left: "5px",
                          fontWeight: "bold",
                          color: "white",
                        }}
                      >
                        {item.status}
                      </div>
                    </div>
                    <div
                      style={{
                        display: "flex",
                        alignItems: "center",
                        color: "black",
                      }}
                    >
                      <div style={{ margin: "4px 4px 4px 0px" }}>
                        {!item.name[2]
                          ? `Meeting: ${item.meeting_id}`.length > 18
                            ? `Meeting: ${item.meeting_id}`.substring(0, 15) +
                              "..."
                            : `Meeting: ${item.meeting_id}`
                          : item.name.length > 18
                          ? item.name[2].substring(0, 15) + "..."
                          : item.name[2]}
                      </div>
                    </div>
                    <div
                      style={{
                        display: "flex",
                        fontSize: "small",
                        color: "gray",
                      }}
                    >
                      <div style={{ margin: "4px 4px 4px 0px" }}>
                        {item.views[1] || 0} views
                      </div>
                      <div style={{ margin: "4px 0px" }}>&#8226;</div>
                      <div style={{ margin: "4px" }}>
                        {item.upvotes[0] || 0} upvotes
                      </div>
                    </div>
                  </div>
                </Link>
              )
            );
          })
        ) : (
          <Spinner
            size="xl"
            width="60px"
            height="60px"
            color="black"
            alignItems="center"
            marginLeft="50px"
            marginTop="50px"
            marginBottom="50px"
          />
        )}
      </div>

      <div
        style={{
          fontSize: "1.3em",
          textAlign: "left",
          paddingLeft: "50px",
          fontWeight: "bold",
          paddingTop: "20px",
          color: "black",
        }}
      >
        Trending <span style={{ color: "red" }}>Live</span> Videos
      </div>
      <div
        style={{
          display: "flex",
          padding: "10px 40px",
          justifyContent: "flex-start",
          flexWrap: "wrap",
        }}
      >
        {streams.length ? (
          streams
            .map((item) => item)
            .sort((a, b) => b.upvotes[0] - a.upvotes[0])
            .map((item) => {
              return (
                item.meeting_id && (
                  <Link
                    style={{ textDecoration: "none" }}
                    to={`/meeting/${item.meeting_id}`}
                  >
                    <div style={{ margin: "15px" }}>
                      <div style={{ position: "relative" }}>
                        <img
                          src={
                            item.thumbnail[0] ||
                            "https://m.media-amazon.com/images/M/MV5BYzBiYjlhNGEtNjJkNi00NDc0LWIyMDMtMTg0NDUwZjcxNmY4XkEyXkFqcGdeQXVyMjg2MTMyNTM@._V1_.jpg"
                          }
                          height={"200px"}
                          style={{
                            aspectRatio: "1920/1080",
                            borderRadius: "0px",
                            border: "solid 0.5px gray",
                          }}
                        />
                        <div
                          style={{
                            margin: "4px 4px 4px 6px",
                            fontSize: "small",
                            backgroundColor:
                              item.status === "LIVE" ? "red" : "gray",
                            padding: "1px 8px",
                            borderRadius: "2px",
                            position: "absolute",
                            top: "5px",
                            left: "5px",
                            fontWeight: "bold",
                            color: "white",
                          }}
                        >
                          {item.status}
                        </div>
                      </div>
                      <div
                        style={{
                          display: "flex",
                          alignItems: "center",
                          color: "black",
                          position: "relative",
                        }}
                      >
                        <div style={{ margin: "4px 4px 4px 0px" }}>
                          {!item.name[2]
                            ? `Meeting: ${item.meeting_id}`.length > 18
                              ? `Meeting: ${item.meeting_id}`.substring(0, 15) +
                                "..."
                              : `Meeting: ${item.meeting_id}`
                            : item.name.length > 18
                            ? item.name[2].substring(0, 15) + "..."
                            : item.name[2]}
                        </div>
                      </div>
                      <div
                        style={{
                          display: "flex",
                          fontSize: "small",
                          color: "gray",
                        }}
                      >
                        <div style={{ margin: "4px 4px 4px 0px" }}>
                          {item.views[1] || 0} views
                        </div>
                        <div style={{ margin: "4px 0px" }}>&#8226;</div>
                        <div style={{ margin: "4px" }}>
                          {item.upvotes[0] || 0} upvotes
                        </div>
                      </div>
                    </div>
                  </Link>
                )
              );
            })
        ) : (
          <Spinner
            size="xl"
            width="60px"
            height="60px"
            color="black"
            alignItems="center"
            marginLeft="50px"
            marginTop="50px"
            marginBottom="50px"
          />
        )}
      </div>
    </div>
  );
};

export default LivestreamBody;

Home Component

Now we will create likes feature for our livestream. For this, we would add a simple thumbs-up button below our livestream, which could be used to like it ��.

LiveStreamInteraction.jsx

import { useState } from "react";
import { Flex, Box, Text } from "@chakra-ui/react";
import { Icon } from "@chakra-ui/react";
import { FiThumbsUp, FiThumbsDown } from "react-icons/fi";
import { FaThumbsUp, FaThumbsDown } from "react-icons/fa";

const SERVER_URL = process.env.REACT_APP_SERVER_URL || "http://localhost:8000";

const Voting = () => {
  const handleVote = async (type) => {
    const meetingId = window.location.pathname.split("/")[2]
    fetch(`${SERVER_URL}/vote/${meetingId}`, {
      method: "POST",
      body: JSON.stringify({ type }),
      headers: { "Content-Type": "application/json" },
    });
  };

  const [likeCount, setLikeCount] = useState(0);

  return (
    <>
      <Flex
        backgroundColor="#252525"
        paddingLeft={"40px"}
        paddingBottom={"20px"}
      >
        <Box>
          {likeCount == 0 ? (
            <Icon
              as={FiThumbsUp}
              boxSize={8}
              sx={{ cursor: "pointer" }}
              onClick={() => {
                setLikeCount(1);
                handleVote("INCR");
              }}
            />
          ) : (
            <Icon
              as={FaThumbsUp}
              boxSize={8}
              sx={{ cursor: "pointer" }}
            />
          )}
          <Text ml={1} style={{ margin: "0px", fontSize: "13px"}}>Upvote</Text>
        </Box>
      </Flex>
    </>
  );
};

const LiveStreamInteraction = ({ meetingId }) => {
  return (
    <>
      <Voting meetingId={meetingId} />
    </>
  );
};

export default LiveStreamInteraction;

And then we will put all of this together with our App component.

App.jsx

import { useEffect, useState } from "react";
import Meet from "./Meet";
import Home from "./Home";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "./App.css";

import LivestreamHeader from "./Livestreams/LivestreamHeader";
import LivestreamBody from "./Livestreams/LivestreamBody";

function App() {
  return (
    <BrowserRouter>
        <LivestreamHeader />
            <Routes>
                <Route path='/' element={<><LivestreamBody /></>} />
                <Route path='/create-meeting' element={<Home />}></Route>
                <Route path='/meeting/:meetingId' element={<Meet />}></Route>
            </Routes>
        </BrowserRouter>
  );
}

export default App;

Live Demo

You may go ahead and create your own live stream on our platform here: Live Demo Link

LiveStream

Conclusion

In this article, we dived into building our own live streaming platform (DyteStream) which has its own feed and allows multiple people at once. ✨

And we did all of this with just a few lines of code, just with the help of DyteSDK. 🙌

So why wait? Go ahead and try building your own livestreaming applications effortlessly with Dyte!

Great! Next, complete checkout for full access to Dyte.
Welcome back! You've successfully signed in.
You've successfully subscribed to Dyte.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.