diff --git a/README.md b/README.md index 653d98f..33f63c3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/frontend/public/images/art.jpg b/frontend/public/images/art_category.jpg similarity index 100% rename from frontend/public/images/art.jpg rename to frontend/public/images/art_category.jpg diff --git a/frontend/public/images/chatting_category.jpg b/frontend/public/images/chatting_category.jpg new file mode 100644 index 0000000..53902e4 Binary files /dev/null and b/frontend/public/images/chatting_category.jpg differ diff --git a/frontend/public/images/cooking_category.jpg b/frontend/public/images/cooking_category.jpg new file mode 100644 index 0000000..fcb7b3a Binary files /dev/null and b/frontend/public/images/cooking_category.jpg differ diff --git a/frontend/public/images/game1.jpg b/frontend/public/images/game1.jpg new file mode 100644 index 0000000..fd06321 Binary files /dev/null and b/frontend/public/images/game1.jpg differ diff --git a/frontend/public/images/game_music1.jpg b/frontend/public/images/game_music1.jpg new file mode 100644 index 0000000..bb79a5d Binary files /dev/null and b/frontend/public/images/game_music1.jpg differ diff --git a/frontend/public/images/gaming_category.jpg b/frontend/public/images/gaming_category.jpg new file mode 100644 index 0000000..a6d7cab Binary files /dev/null and b/frontend/public/images/gaming_category.jpg differ diff --git a/frontend/public/images/music_category.webp b/frontend/public/images/music_category.webp new file mode 100644 index 0000000..1ba2955 Binary files /dev/null and b/frontend/public/images/music_category.webp differ diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/StreamListRow.tsx similarity index 72% rename from frontend/src/components/Layout/ListRow.tsx rename to frontend/src/components/Layout/StreamListRow.tsx index a7d2fbe..1f33860 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/StreamListRow.tsx @@ -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 = ({ stream, onClick }) => { +const StreamListEntry: React.FC = ({ stream, onClick }) => { return (
{stream.thumbnail ? ( {stream.title} @@ -48,24 +48,24 @@ const ListEntry: React.FC = ({ stream, onClick }) => { }; // Row of stream entries -const ListRow: React.FC = ({ +const StreamListRow: React.FC = ({ title, description, streams, onStreamClick, }) => { return ( -
+

{title}

{description}

{streams.map((stream) => ( - onStreamClick?.(stream.streamer)} + onClick={() => onStreamClick?.(stream.id)} /> ))}
@@ -73,4 +73,4 @@ const ListRow: React.FC = ({ ); }; -export default ListRow; +export default StreamListRow; diff --git a/frontend/src/context/StreamsContext.tsx b/frontend/src/context/StreamsContext.tsx index e6957c5..caa5a7b 100644 --- a/frontend/src/context/StreamsContext.tsx +++ b/frontend/src/context/StreamsContext.tsx @@ -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(undefined); export function StreamsProvider({ children }: { children: React.ReactNode }) { const [featuredStreams, setFeaturedStreams] = useState([]); + const [featuredCategories, setFeaturedCategories] = useState( + [] + ); + 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 ( - + {children} ); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index f1d90aa..c124024 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -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 = () => { > - - {}} //TODO />
); }; 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)" }} > - - - {}} //TODO />
); diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index 49c4eff..fc84a81 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -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]); diff --git a/web_server/.flaskenv b/web_server/.flaskenv deleted file mode 100644 index 9a285a8..0000000 --- a/web_server/.flaskenv +++ /dev/null @@ -1,2 +0,0 @@ -FLASK_APP=backend.blueprints.__init__ -FLASK_DEBUG=True \ No newline at end of file diff --git a/web_server/blueprints/authentication.py b/web_server/blueprints/authentication.py index f861520..65b8bcc 100644 --- a/web_server/blueprints/authentication.py +++ b/web_server/blueprints/authentication.py @@ -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 diff --git a/web_server/blueprints/main.py b/web_server/blueprints/main.py index f43cb21..cab308f 100644 --- a/web_server/blueprints/main.py +++ b/web_server/blueprints/main.py @@ -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/') \n def serve_image(filename): ←←← diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 0bce90c..4a3a450 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -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/', methods=['GET']) +def get_streamer(streamer_id): + """ + Returns a streamer's data """ return + +@stream_bp.route('/get_stream_data/', 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/', 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 \ No newline at end of file + return diff --git a/web_server/package-lock.json b/web_server/package-lock.json deleted file mode 100644 index 7bbf260..0000000 --- a/web_server/package-lock.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/web_server/package.json b/web_server/package.json deleted file mode 100644 index aec11bb..0000000 --- a/web_server/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "lucide-react": "^0.473.0" - } -}