- 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.
## Access Control Levels
### Access Control Levels
The platform implements three-tier access control:
@@ -28,18 +28,21 @@ The platform implements three-tier access control:
## Technical Stack
### Frontend
- React with TypeScript for type safety
- Tailwind CSS for styling
- Video.js for stream playback
- Stripe integration for payment processing
### Backend
- Python with Flask web framework
- SQLite database for data persistence
- Flask-Session for user session management
- Nginx RTMP module for stream handling
### Infrastructure
- Docker for containerization and deployment
- Docker Compose for multi-container orchestration
- 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.
## 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:
1. With the Docker VSCode extension installed, right click within the editor with the `docker-compose.yaml` file active.
Select `Compose Up`
![image](https://github.com/user-attachments/assets/d68dd3b1-f3de-4780-b957-055cb536446b)
Now the backend is up and running on `http://127.0.0.1:8080`.
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.
3. Navigate to the localhost link given in the terminal upon executing the last command (`http://localhost:5173/`).
- Pre-requisites:
- Docker
- Docker Compose
- Node.js & npm
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;
}
interface ListEntryProps {
interface StreamListEntryProps {
stream: StreamItem;
onClick?: () => void;
}
interface ListRowProps {
interface StreamListRowProps {
title: string;
description: string;
streams: StreamItem[];
onStreamClick?: (streamerId: string) => void;
onStreamClick?: (streamId: string) => void;
}
// Individual stream entry component
const ListEntry: React.FC<ListEntryProps> = ({ stream, onClick }) => {
const StreamListEntry: React.FC<StreamListEntryProps> = ({ stream, onClick }) => {
return (
<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}
>
<div className="relative w-full pt-[56.25%]">
{stream.thumbnail ? (
<img
src={`images/` + stream.thumbnail}
src={`images/`+stream.thumbnail}
alt={stream.title}
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
const ListRow: React.FC<ListRowProps> = ({
const StreamListRow: React.FC<StreamListRowProps> = ({
title,
description,
streams,
onStreamClick,
}) => {
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">
<h2 className="text-2xl font-bold">{title}</h2>
<p className="text-gray-400">{description}</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{streams.map((stream) => (
<ListEntry
<StreamListEntry
key={stream.id}
stream={stream}
onClick={() => onStreamClick?.(stream.streamer)}
onClick={() => onStreamClick?.(stream.id)}
/>
))}
</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 { useAuth } from "./AuthContext";
interface StreamItem {
id: number;
@@ -10,24 +11,46 @@ interface StreamItem {
interface StreamsContextType {
featuredStreams: StreamItem[];
featuredCategories: StreamItem[];
setFeaturedStreams: (streams: StreamItem[]) => void;
setFeaturedCategories: (categories: StreamItem[]) => void;
}
const StreamsContext = createContext<StreamsContextType | undefined>(undefined);
export function StreamsProvider({ children }: { children: React.ReactNode }) {
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(() => {
fetch("/api/get_streams")
fetch(fetch_url[0])
.then((response) => response.json())
.then((data: StreamItem[]) => {
setFeaturedStreams(data);
});
fetch(fetch_url[1])
.then((response) => response.json())
.then((data: StreamItem[]) => {
setFeaturedCategories(data);
});
}, []);
return (
<StreamsContext.Provider value={{ featuredStreams, setFeaturedStreams }}>
<StreamsContext.Provider
value={{
featuredStreams,
featuredCategories,
setFeaturedStreams,
setFeaturedCategories,
}}
>
{children}
</StreamsContext.Provider>
);

View File

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

View File

@@ -25,7 +25,7 @@ const VideoPage: React.FC = () => {
if (streamerName) {
// Fetch stream data for this streamer
console.log(`Loading stream for ${streamerName}`);
// fetch(`/api/streams/${streamerName}`)
// fetch(`/api/get_stream_data/${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 forms import SignupForm, LoginForm
from flask_cors import cross_origin
from database.database import Database
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): ←←←

View File

@@ -2,63 +2,152 @@ from flask import Blueprint
stream_bp = Blueprint("stream", __name__)
@stream_bp.route('/get_streams', methods=['GET'])
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
# 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 = [
{
"id": 1,
"title": "Gaming Stream",
"streamer": "Gamer123",
"viewers": 1500,
"thumbnail": "dance_game.png",
},
{
"id": 2,
"title": "Art Stream",
"streamer": "Artist456",
"viewers": 800,
"thumbnail": "surface.jpeg",
},
{
"id": 3,
"title": "Music Stream",
"streamer": "Musician789",
"viewers": 2000,
"thumbnail": "monkey.png",
},
{
"id": 4,
"title": "Just Chatting",
"streamer": "Chatty101",
"viewers": 1200,
"thumbnail": "elf.webp",
},
{
"id": 5,
"title": "Cooking Stream",
"streamer": "Chef202",
"viewers": 1000,
"thumbnail": "art.jpg",
}
]
return streamers
# TODO Add a category field to the stream object
streams = [
{
"id": 1,
"title": "Gaming Stream",
"streamer": "Gamer123",
"viewers": 1500,
"thumbnail": "dance_game.png",
},
{
"id": 2,
"title": "Art Stream",
"streamer": "Artist456",
"viewers": 800,
"thumbnail": "surface.jpeg",
},
{
"id": 3,
"title": "Music Stream",
"streamer": "Musician789",
"viewers": 2000,
"thumbnail": "monkey.png",
},
{
"id": 4,
"title": "Just Chatting",
"streamer": "Chatty101",
"viewers": 1200,
"thumbnail": "chatting_category.jpg",
},
{
"id": 5,
"title": "Cooking Stream",
"streamer": "Chef202",
"viewers": 1000,
"thumbnail": "cooking_category.jpg",
}
]
return streams
@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
@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'])
def get_followed_streamers():
"""
@@ -66,9 +155,10 @@ def get_followed_streamers():
"""
return
@stream_bp.route('/save_stream_thumbnail/<int:streamer_id>', methods=['POST'])
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
"""
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"
}
}