- Refactor of StreamsContext:

Added `featuredCategories` section,
Added personalised variations of HomePage contents;
- Removal of redundant/unused files from backend;
- Update to README: Updated to current method for deploying;
- Known bug: StreamsContext is being called before AuthContext, leading to unpersonalised streams & categories each time, even when logged in;
This commit is contained in:
Chris-1010
2025-01-24 17:23:56 +00:00
parent b9e912af1d
commit 5c16092b1c
18 changed files with 200 additions and 121 deletions

View File

@@ -4,7 +4,7 @@
Our project is a live streaming service that enables content creators to broadcast to viewers through a web-based platform. The application consists of a React-based frontend that users access to watch streams, and a Flask backend that manages the core functionality and business logic. Our project is a live streaming service that enables content creators to broadcast to viewers through a web-based platform. The application consists of a React-based frontend that users access to watch streams, and a Flask backend that manages the core functionality and business logic.
## Access Control Levels ### Access Control Levels
The platform implements three-tier access control: The platform implements three-tier access control:
@@ -28,18 +28,21 @@ The platform implements three-tier access control:
## Technical Stack ## Technical Stack
### Frontend ### Frontend
- React with TypeScript for type safety - React with TypeScript for type safety
- Tailwind CSS for styling - Tailwind CSS for styling
- Video.js for stream playback - Video.js for stream playback
- Stripe integration for payment processing - Stripe integration for payment processing
### Backend ### Backend
- Python with Flask web framework - Python with Flask web framework
- SQLite database for data persistence - SQLite database for data persistence
- Flask-Session for user session management - Flask-Session for user session management
- Nginx RTMP module for stream handling - Nginx RTMP module for stream handling
### Infrastructure ### Infrastructure
- Docker for containerization and deployment - Docker for containerization and deployment
- Docker Compose for multi-container orchestration - Docker Compose for multi-container orchestration
- Nginx for reverse proxy and RTMP streaming server - Nginx for reverse proxy and RTMP streaming server
@@ -52,12 +55,18 @@ While our current focus is on the web platform, we've architected the system wit
This project is actively maintained on GitHub with all team members contributing through version control. We follow collaborative development practices with regular code reviews and feature branches. This project is actively maintained on GitHub with all team members contributing through version control. We follow collaborative development practices with regular code reviews and feature branches.
## Running the Current Implementation of the Project
# Running the Current Implementation of the Project
The current implementation can be run, in development mode, using the following method: The current implementation can be run, in development mode, using the following method:
1. With the Docker VSCode extension installed, right click within the editor with the `docker-compose.yaml` file active.
Select `Compose Up` - Pre-requisites:
![image](https://github.com/user-attachments/assets/d68dd3b1-f3de-4780-b957-055cb536446b) - Docker
Now the backend is up and running on `http://127.0.0.1:8080`. - Docker Compose
2. Next, having Node.js & Vite installed, `cd` into `web_server/frontend` and execute the command `npm run dev`. This should startup the frontend section of the application. However, this is only a temporary method as a Docker solution is in development for this frontend part of the project. - Node.js & npm
3. Navigate to the localhost link given in the terminal upon executing the last command (`http://localhost:5173/`).
1. Replace `.env.example` with `.env` and fill in the required environment variables
2. Launch Docker containers using either:
- `docker-compose up --build` in terminal
- Right click within the editor within `docker-compose.yml` and select `Compose Up`
This will start the frontend, backend, and database services
3. Access the frontend at `localhost:8080` in your browser

View File

Before

Width:  |  Height:  |  Size: 844 KiB

After

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -8,29 +8,29 @@ interface StreamItem {
thumbnail?: string; thumbnail?: string;
} }
interface ListEntryProps { interface StreamListEntryProps {
stream: StreamItem; stream: StreamItem;
onClick?: () => void; onClick?: () => void;
} }
interface ListRowProps { interface StreamListRowProps {
title: string; title: string;
description: string; description: string;
streams: StreamItem[]; streams: StreamItem[];
onStreamClick?: (streamerId: string) => void; onStreamClick?: (streamId: string) => void;
} }
// Individual stream entry component // Individual stream entry component
const ListEntry: React.FC<ListEntryProps> = ({ stream, onClick }) => { const StreamListEntry: React.FC<StreamListEntryProps> = ({ stream, onClick }) => {
return ( return (
<div <div
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 border border-gray-100 hover:border-purple-500 hover:border-b-4 hover:border-l-4 transition-all" className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 transition-colors"
onClick={onClick} onClick={onClick}
> >
<div className="relative w-full pt-[56.25%]"> <div className="relative w-full pt-[56.25%]">
{stream.thumbnail ? ( {stream.thumbnail ? (
<img <img
src={`images/` + stream.thumbnail} src={`images/`+stream.thumbnail}
alt={stream.title} alt={stream.title}
className="absolute top-0 left-0 w-full h-full object-cover" className="absolute top-0 left-0 w-full h-full object-cover"
/> />
@@ -48,24 +48,24 @@ const ListEntry: React.FC<ListEntryProps> = ({ stream, onClick }) => {
}; };
// Row of stream entries // Row of stream entries
const ListRow: React.FC<ListRowProps> = ({ const StreamListRow: React.FC<StreamListRowProps> = ({
title, title,
description, description,
streams, streams,
onStreamClick, onStreamClick,
}) => { }) => {
return ( return (
<div className="flex flex-col space-y-4 py-6 mx-4"> <div className="flex flex-col space-y-4 py-6">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-2xl font-bold">{title}</h2> <h2 className="text-2xl font-bold">{title}</h2>
<p className="text-gray-400">{description}</p> <p className="text-gray-400">{description}</p>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{streams.map((stream) => ( {streams.map((stream) => (
<ListEntry <StreamListEntry
key={stream.id} key={stream.id}
stream={stream} stream={stream}
onClick={() => onStreamClick?.(stream.streamer)} onClick={() => onStreamClick?.(stream.id)}
/> />
))} ))}
</div> </div>
@@ -73,4 +73,4 @@ const ListRow: React.FC<ListRowProps> = ({
); );
}; };
export default ListRow; export default StreamListRow;

View File

@@ -1,4 +1,5 @@
import { createContext, useContext, useState, useEffect } from "react"; import { createContext, useContext, useState, useEffect } from "react";
import { useAuth } from "./AuthContext";
interface StreamItem { interface StreamItem {
id: number; id: number;
@@ -10,24 +11,46 @@ interface StreamItem {
interface StreamsContextType { interface StreamsContextType {
featuredStreams: StreamItem[]; featuredStreams: StreamItem[];
featuredCategories: StreamItem[];
setFeaturedStreams: (streams: StreamItem[]) => void; setFeaturedStreams: (streams: StreamItem[]) => void;
setFeaturedCategories: (categories: StreamItem[]) => void;
} }
const StreamsContext = createContext<StreamsContextType | undefined>(undefined); const StreamsContext = createContext<StreamsContextType | undefined>(undefined);
export function StreamsProvider({ children }: { children: React.ReactNode }) { export function StreamsProvider({ children }: { children: React.ReactNode }) {
const [featuredStreams, setFeaturedStreams] = useState<StreamItem[]>([]); const [featuredStreams, setFeaturedStreams] = useState<StreamItem[]>([]);
const [featuredCategories, setFeaturedCategories] = useState<StreamItem[]>(
[]
);
const { isLoggedIn } = useAuth();
const fetch_url = isLoggedIn
? ["/api/get_recommended_streams", "/api/get_followed_categories"]
: ["/api/get_streams", "/api/get_categories"];
useEffect(() => { useEffect(() => {
fetch("/api/get_streams") fetch(fetch_url[0])
.then((response) => response.json()) .then((response) => response.json())
.then((data: StreamItem[]) => { .then((data: StreamItem[]) => {
setFeaturedStreams(data); setFeaturedStreams(data);
}); });
fetch(fetch_url[1])
.then((response) => response.json())
.then((data: StreamItem[]) => {
setFeaturedCategories(data);
});
}, []); }, []);
return ( return (
<StreamsContext.Provider value={{ featuredStreams, setFeaturedStreams }}> <StreamsContext.Provider
value={{
featuredStreams,
featuredCategories,
setFeaturedStreams,
setFeaturedCategories,
}}
>
{children} {children}
</StreamsContext.Provider> </StreamsContext.Provider>
); );

View File

@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import Navbar from "../components/Layout/Navbar"; import Navbar from "../components/Layout/Navbar";
import ListRow from "../components/Layout/ListRow"; import StreamListRow from "../components/Layout/StreamListRow";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useStreams } from "../context/StreamsContext"; import { useStreams } from "../context/StreamsContext";
const HomePage: React.FC = () => { const HomePage: React.FC = () => {
const { featuredStreams } = useStreams(); const { featuredStreams, featuredCategories } = useStreams();
const navigate = useNavigate(); const navigate = useNavigate();
const handleStreamClick = (streamerId: string) => { const handleStreamClick = (streamerId: string) => {
@@ -19,24 +19,24 @@ const HomePage: React.FC = () => {
> >
<Navbar /> <Navbar />
<ListRow <StreamListRow
title="Live Now" title="Live Now"
description="Streamers that are currently live" description="Streamers that are currently live"
streams={featuredStreams} streams={featuredStreams}
onStreamClick={handleStreamClick} onStreamClick={handleStreamClick}
/> />
<ListRow <StreamListRow
title="Trending Categories" title="Trending Categories"
description="Categories that have been 'popping off' lately" description="Categories that have been 'popping off' lately"
streams={featuredStreams} streams={featuredCategories}
onStreamClick={handleStreamClick} onStreamClick={() => {}} //TODO
/> />
</div> </div>
); );
}; };
export const PersonalisedHomePage: React.FC = () => { export const PersonalisedHomePage: React.FC = () => {
const { featuredStreams } = useStreams(); const { featuredStreams, featuredCategories } = useStreams();
const navigate = useNavigate(); const navigate = useNavigate();
const handleStreamClick = (streamerId: string) => { const handleStreamClick = (streamerId: string) => {
@@ -49,18 +49,18 @@ export const PersonalisedHomePage: React.FC = () => {
style={{ backgroundImage: "url(/images/background-pattern.svg)" }} style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
> >
<Navbar /> <Navbar />
{/*//TODO Extract StreamListRow away to ListRow so that it makes sense for categories to be there also */}
<ListRow <StreamListRow
title="Live Now - Recommended" title="Live Now - Recommended"
description="We think you might like these streams - Streamers recommended for you" description="We think you might like these streams - Streamers recommended for you"
streams={featuredStreams} streams={featuredStreams}
onStreamClick={handleStreamClick} onStreamClick={handleStreamClick}
/> />
<ListRow <StreamListRow
title="Followed Categories" title="Followed Categories"
description="Current streams from your followed categories" description="Current streams from your followed categories"
streams={featuredStreams} streams={featuredCategories}
onStreamClick={handleStreamClick} onStreamClick={() => {}} //TODO
/> />
</div> </div>
); );

View File

@@ -25,7 +25,7 @@ const VideoPage: React.FC = () => {
if (streamerName) { if (streamerName) {
// Fetch stream data for this streamer // Fetch stream data for this streamer
console.log(`Loading stream for ${streamerName}`); console.log(`Loading stream for ${streamerName}`);
// fetch(`/api/streams/${streamerName}`) // fetch(`/api/get_stream_data/${streamerName}`)
} }
}, [streamerName]); }, [streamerName]);

View File

@@ -1,2 +0,0 @@
FLASK_APP=backend.blueprints.__init__
FLASK_DEBUG=True

View File

@@ -1,6 +1,5 @@
from flask import Blueprint, session, request, url_for, redirect, g, jsonify from flask import Blueprint, session, request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from forms import SignupForm, LoginForm
from flask_cors import cross_origin from flask_cors import cross_origin
from database.database import Database from database.database import Database
from blueprints.utils import login_required from blueprints.utils import login_required

View File

@@ -12,8 +12,4 @@ def hls(stream_id):
# -------------------------------------------------------- # --------------------------------------------------------
# TODO Route for saving uploaded thumbnails to database, serving these images to the frontend upon request: →→→ @main_bp.route('/images/<path:filename>') \n def serve_image(filename): ←←← # TODO Route for saving uploaded thumbnails to database, serving these images to the frontend upon request: →→→ @main_bp.route('/images/<path:filename>') \n def serve_image(filename): ←←←

View File

@@ -2,63 +2,152 @@ from flask import Blueprint
stream_bp = Blueprint("stream", __name__) stream_bp = Blueprint("stream", __name__)
@stream_bp.route('/get_streams', methods=['GET']) @stream_bp.route('/get_streams', methods=['GET'])
def get_sample_streams(): def get_sample_streams():
""" """
Returns a list of (sample) streamers live right now Returns a list of (sample) streams live right now
""" """
# top 25, if not logged in # top 25, if not logged in
# if logged in, show streams that match user's tags # if logged in, show streams that match user's tags
# user attains tags from the tags of the streamers they follow, and streamers they've watched # user attains tags from the tags of the streamers they follow, and streamers they've watched
streamers = [ # TODO Add a category field to the stream object
{ streams = [
"id": 1, {
"title": "Gaming Stream", "id": 1,
"streamer": "Gamer123", "title": "Gaming Stream",
"viewers": 1500, "streamer": "Gamer123",
"thumbnail": "dance_game.png", "viewers": 1500,
}, "thumbnail": "dance_game.png",
{ },
"id": 2, {
"title": "Art Stream", "id": 2,
"streamer": "Artist456", "title": "Art Stream",
"viewers": 800, "streamer": "Artist456",
"thumbnail": "surface.jpeg", "viewers": 800,
}, "thumbnail": "surface.jpeg",
{ },
"id": 3, {
"title": "Music Stream", "id": 3,
"streamer": "Musician789", "title": "Music Stream",
"viewers": 2000, "streamer": "Musician789",
"thumbnail": "monkey.png", "viewers": 2000,
}, "thumbnail": "monkey.png",
{ },
"id": 4, {
"title": "Just Chatting", "id": 4,
"streamer": "Chatty101", "title": "Just Chatting",
"viewers": 1200, "streamer": "Chatty101",
"thumbnail": "elf.webp", "viewers": 1200,
}, "thumbnail": "chatting_category.jpg",
{ },
"id": 5, {
"title": "Cooking Stream", "id": 5,
"streamer": "Chef202", "title": "Cooking Stream",
"viewers": 1000, "streamer": "Chef202",
"thumbnail": "art.jpg", "viewers": 1000,
} "thumbnail": "cooking_category.jpg",
] }
return streamers ]
return streams
@stream_bp.route('/get_recommended_streams', methods=['GET']) @stream_bp.route('/get_recommended_streams', methods=['GET'])
def get_recommended_streamers(): def get_recommended_streams():
""" """
Queries DB to get a list of recommended streamers using an algorithm Queries DB to get a list of recommended streams using an algorithm
"""
return [
{
"id": 1,
"title": "Fake Game Showcase w/ Devs",
"streamer": "Gamer_boy9000",
"viewers": 15458,
"thumbnail": "game1.jpg",
}, {
"id": 2,
"title": "Game OSTs I like!",
"streamer": "GéMusicLover",
"viewers": 52911,
"thumbnail": "game_music1.jpg",
},
{
"id": 3,
"title": "Chill Stream - Cooking with Chef Ramsay",
"streamer": "HarrietDgoat",
"viewers": 120283,
# Intentionally left out thumbnail to showcase placeholder image
}]
@stream_bp.route('/get_categories', methods=['GET'])
def get_categories():
"""
Returns a list of (sample) categories being watched right now
"""
return [
{
"id": 1,
"title": "Gaming",
"viewers": 220058,
"thumbnail": "gaming_category.jpg",
},
{
"id": 2,
"title": "Music",
"viewers": 150060,
"thumbnail": "music_category.webp",
},
{
"id": 3,
"title": "Art",
"viewers": 10200,
"thumbnail": "art_category.jpg",
},
{
"id": 4,
"title": "Cooking",
"viewers": 8000,
"thumbnail": "cooking_category.jpg",
},
{
"id": 5,
"title": "Just Chatting",
"viewers": 83900,
"thumbnail": "chatting_category.jpg",
}
]
@stream_bp.route('/get_followed_categories', methods=['GET'])
def get_followed_categories():
"""
Queries DB to get a list of followed categories
"""
categories = []
if categories:
return categories
return get_categories()
@stream_bp.route('/get_streamer_data/<int:streamer_id>', methods=['GET'])
def get_streamer(streamer_id):
"""
Returns a streamer's data
""" """
return return
@stream_bp.route('/get_stream_data/<int:streamer_id>', methods=['GET'])
def get_stream(streamer_id):
"""
Returns a streamer's stream data
"""
return
@stream_bp.route('/get_followed_streams', methods=['GET']) @stream_bp.route('/get_followed_streams', methods=['GET'])
def get_followed_streamers(): def get_followed_streamers():
""" """
@@ -66,9 +155,10 @@ def get_followed_streamers():
""" """
return return
@stream_bp.route('/save_stream_thumbnail/<int:streamer_id>', methods=['POST']) @stream_bp.route('/save_stream_thumbnail/<int:streamer_id>', methods=['POST'])
def stream_thumbnail_snapshot(streamer_id): def stream_thumbnail_snapshot(streamer_id):
""" """
Function to be called periodically which saves the current live stream as an img to be used for the thumbnail to be displayed Function to be called periodically which saves the current live stream as an img to be used for the thumbnail to be displayed
""" """
return return

View File

@@ -1,31 +0,0 @@
{
"name": "web_server",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"lucide-react": "^0.473.0"
}
},
"node_modules/lucide-react": {
"version": "0.473.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.473.0.tgz",
"integrity": "sha512-KW6u5AKeIjkvrxXZ6WuCu9zHE/gEYSXCay+Gre2ZoInD0Je/e3RBtP4OHpJVJ40nDklSvjVKjgH7VU8/e2dzRw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"lucide-react": "^0.473.0"
}
}