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.
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.
- In this project, we will use React with Dyte UI kit and Dyte React Web Core packages for the frontend.
- For the backend, we will use FastAPI which interacts with Dyte API
- We will use ElephantSQL for our database
- Lastly, we will use Imgur for storing screenshots.
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
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 🤫.
📝 NOTE
You would also need to create accounts on the following platforms before proceeding:
- Imgur: Here is a step-by-step guide to create an account and generate API key.
- ElephantSQL: Here is a step-by-step guide to create a db on ElephantSQL.
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
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;
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" }}>•</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" }}>•</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;
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
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!