From 9cc5b40b25f1387c2cc5b2b2c2584f6bd32a76d8 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Sat, 22 Feb 2025 12:05:20 +0000 Subject: [PATCH 1/3] UPDATE: Change to how streams are accessed, detected and published to the application --- nginx/nginx.conf | 48 ++++++++-------- web_server/blueprints/streams.py | 94 ++++++++++++++++++++++++++------ web_server/utils/stream_utils.py | 34 +----------- 3 files changed, 105 insertions(+), 71 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 008518e..f5c8bd2 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,4 +1,5 @@ worker_processes 1; +error_log /var/log/nginx/error.log warn; events { worker_connections 1024; @@ -15,8 +16,8 @@ rtmp { deny play all; push rtmp://127.0.0.1:1935/hls-live; - on_publish http://web_server:5000/publish_stream; - on_publish_done http://web_server:5000/end_stream; + on_publish http://web_server:5000/init_stream; # if the stream is detected from OBS + on_publish_done http://web_server:5000/end_stream; # if the stream is ended on OBS } @@ -24,7 +25,7 @@ rtmp { live on; hls on; - hls_path /stream_data/; + hls_path /stream_data/stream/; allow publish 127.0.0.1; deny publish all; @@ -78,7 +79,7 @@ http { # The MPEG-TS video chunks are stored in /tmp/hls location ~ ^/stream/(.+)/(.+\.ts)$ { - alias /stream_data/$1/stream/$2; + alias /stream_data/stream/$1/$2; # Let the MPEG-TS video chunks be cacheable expires max; @@ -86,35 +87,36 @@ http { # The M3U8 playlists location location ~ ^/stream/(.+)/(.+\.m3u8)$ { - alias /stream_data/$1/stream/$2; + alias /stream_data/stream/$1/$2; # The M3U8 playlists should not be cacheable expires -1d; } + #! Unused right now so the following are inaccurate locations # The thumbnails location - location ~ ^/stream/(.+)/thumbnails/(.+\.jpg)$ { - alias /stream_data/$1/thumbnails/$2; + # location ~ ^/stream/(.+)/thumbnails/(.+\.jpg)$ { + # alias /stream_data/$1/thumbnails/$2; - # The thumbnails should not be cacheable - expires -1d; - } + # # The thumbnails should not be cacheable + # expires -1d; + # } - # The vods location - location ~ ^/stream/(.+)/vods/(.+\.mp4)$ { - alias /stream_data/$1/vods/$2; + # # The vods location + # location ~ ^/stream/(.+)/vods/(.+\.mp4)$ { + # alias /stream_data/$1/vods/$2; - # The vods should not be cacheable - expires -1d; - } + # # The vods should not be cacheable + # expires -1d; + # } - location ~ ^/\?token=.*$ { - proxy_pass http://frontend:5173; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - } + # location ~ ^/\?token=.*$ { + # proxy_pass http://frontend:5173; + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + # proxy_set_header Host $host; + # } location / { proxy_pass http://frontend:5173; diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 9e96c25..f49c608 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -72,7 +72,7 @@ def stream_data(streamer_id) -> dict: Returns a streamer's current stream data """ data = get_current_stream_data(streamer_id) - + if session.get('user_id') == streamer_id: with Database() as db: stream_key = db.fetchone( @@ -163,10 +163,39 @@ def vods(username): # RTMP Server Routes + +@stream_bp.route("/init_stream", methods=["POST"]) +def init_stream(): + """ + Called by NGINX when OBS starts streaming. + Creates necessary directories and validates stream key. + """ + stream_key = request.form.get("name") + + print(f"Stream initialization requested in nginx with key: {stream_key}") + + with Database() as db: + # Check if valid stream key and user is allowed to stream + user_info = db.fetchone("""SELECT user_id, username, is_live + FROM users + WHERE stream_key = ?""", (stream_key,)) + + if not user_info: + print("Unauthorized - Invalid stream key", flush=True) + return "Unauthorized - Invalid stream key", 403 + + # Create necessary directories + username = user_info["username"] + create_local_directories(username) + + return "OK", 200 + + @stream_bp.route("/publish_stream", methods=["POST"]) def publish_stream(): """ - Authenticates stream from streamer and publishes it to the site + Called when user clicks Start Stream in dashboard. + Sets up stream in database and starts thumbnail generation. step-by-step: fetch user info from stream key @@ -174,27 +203,26 @@ def publish_stream(): set user as streaming periodically update thumbnail """ - stream_key = request.form.get("key") - print("Stream request received") + data = request.form.get("data") - # Open database connection with Database() as db: - # Get user info from stream key - user_info = db.fetchone("""SELECT user_id, username, current_stream_title, current_selected_category_id, is_live + user_info = db.fetchone("""SELECT user_id, username, current_stream_title, + current_selected_category_id, is_live FROM users - WHERE stream_key = ?""", (stream_key,)) + WHERE stream_key = ?""", (data['stream_key'],)) - # If stream key is invalid, return unauthorized if not user_info or user_info["is_live"]: + print( + "Unauthorized. No user found from Stream key or user is already streaming.", flush=True) return "Unauthorized", 403 # Insert stream into database db.execute("""INSERT INTO streams (user_id, title, start_time, num_viewers, category_id) VALUES (?, ?, ?, ?, ?)""", (user_info["user_id"], - user_info["current_stream_title"], + data["title"], datetime.now(), 0, - 1)) + get_category_id(data['category_name']))) # Set user as streaming db.execute("""UPDATE users SET is_live = 1 WHERE user_id = ?""", @@ -203,16 +231,41 @@ def publish_stream(): username = user_info["username"] user_id = user_info["user_id"] - # Local file creation - create_local_directories(username) - # Update thumbnail periodically update_thumbnail.delay(user_id, path_manager.get_stream_file_path(username), path_manager.get_thumbnail_file_path(username), THUMBNAIL_GENERATION_INTERVAL) - return redirect(f"/{user_info['username']}/stream/") + return "OK", 200 + + +@stream_bp.route("/update_stream", methods=["POST"]) +def update_stream(): + """ + Called by StreamDashboard to update stream info + """ + # TODO: Add thumbnails (paths) to table, allow user to update thumbnail + + stream_key = request.form.get("key") + title = request.form.get("title") + category_name = request.form.get("category_name") + + with Database() as db: + user_info = db.fetchone("""SELECT user_id, username, is_live + FROM users + WHERE stream_key = ?""", (stream_key,)) + + if not user_info or not user_info["is_live"]: + print( + "Unauthorized - No user found from stream key or user is not streaming", flush=True) + return "Unauthorized", 403 + + db.execute("""UPDATE streams + SET title = ?, category_id = ? + WHERE user_id = ?""", (title, get_category_id(category_name), user_info["user_id"])) + + return "Stream updated", 200 @stream_bp.route("/end_stream", methods=["POST"]) @@ -230,6 +283,12 @@ def end_stream(): """ stream_key = request.form.get("key") + if stream_key is None: + stream_key = request.form.get("name") + + if stream_key is None: + print("Unauthorized - No stream key provided", flush=True) + return "Unauthorized", 403 # Open database connection with Database() as db: @@ -244,6 +303,7 @@ def end_stream(): # If stream key is invalid, return unauthorized if not user_info: + print("Unauthorized - No user found from stream key", flush=True) return "Unauthorized", 403 # Remove stream from database @@ -256,9 +316,9 @@ def end_stream(): db.execute("""INSERT INTO vods (user_id, title, datetime, category_id, length, views) VALUES (?, ?, ?, ?, ?, ?)""", (user_info["user_id"], - user_info["current_stream_title"], + stream_info["title"], stream_info["start_time"], - user_info["current_selected_category_id"], + stream_info["category_id"], stream_length, 0)) diff --git a/web_server/utils/stream_utils.py b/web_server/utils/stream_utils.py index 52581de..8e40830 100644 --- a/web_server/utils/stream_utils.py +++ b/web_server/utils/stream_utils.py @@ -142,41 +142,14 @@ def get_vod_tags(vod_id: int): """, (vod_id,)) return tags -def transfer_stream_to_vod(user_id: int): - """ - Deletes stream from stream table and moves it to VoD table - TODO: Add functionaliy to save stream permanently - """ - - with Database() as db: - stream = db.fetchone(""" - SELECT * FROM streams WHERE user_id = ?; - """, (user_id,)) - - if not stream: - return False - - ## TODO: calculate length in seconds, currently using temp value - - db.execute(""" - INSERT INTO vods (user_id, title, datetime, category_id, length, views) - VALUES (?, ?, ?, ?, ?, ?); - """, (stream["user_id"], stream["title"], stream["datetime"], stream["category_id"], 10, stream["num_viewers"])) - - db.execute(""" - DELETE FROM streams WHERE user_id = ?; - """, (user_id,)) - - return True - def create_local_directories(username: str): """ Create directories for user stream data if they do not exist """ - vods_path = f"stream_data/{username}/vods" - stream_path = f"stream_data/{username}/stream" - thumbnail_path = f"stream_data/{username}/thumbnails" + vods_path = f"stream_data/vods/{username}" + stream_path = f"stream_data/stream" + thumbnail_path = f"stream_data/thumbnails/{username}" if not os.path.exists(vods_path): os.makedirs(vods_path) @@ -188,7 +161,6 @@ def create_local_directories(username: str): os.makedirs(thumbnail_path) # Fix permissions - os.chmod(f"stream_data/{username}", 0o777) os.chmod(vods_path, 0o777) os.chmod(stream_path, 0o777) os.chmod(thumbnail_path, 0o777) From af24bfa7d37c7925072167bdb2cffd8c8429d622 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Sat, 22 Feb 2025 12:14:00 +0000 Subject: [PATCH 2/3] REFACTOR: Relocate components to appropriate folders + minor style adjustments --- frontend/index.html | 4 +- frontend/src/components/Auth/LoginForm.tsx | 2 +- frontend/src/components/Layout/ListItem.tsx | 2 +- frontend/src/components/Layout/ListRow.tsx | 4 +- .../{Video => Stream}/ChatPanel.tsx | 0 .../src/components/Stream/VideoPlayer.tsx | 117 ++++++++++++++++++ frontend/src/components/Video/VideoPlayer.tsx | 96 -------------- frontend/src/pages/HomePage.tsx | 5 +- frontend/src/pages/VideoPage.tsx | 4 +- 9 files changed, 127 insertions(+), 107 deletions(-) rename frontend/src/components/{Video => Stream}/ChatPanel.tsx (100%) create mode 100644 frontend/src/components/Stream/VideoPlayer.tsx delete mode 100644 frontend/src/components/Video/VideoPlayer.tsx diff --git a/frontend/index.html b/frontend/index.html index 2f8fa8d..8989d4f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ Team Software Project -
+
diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index 1d9f558..4fb6457 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -148,7 +148,7 @@ const LoginForm: React.FC = ({ onSubmit, onForgotPassword }) => { errors.password ? "border-red-500" : "" }`} > -
+