diff --git a/docker-compose.yml b/docker-compose.yml index b163bce..d602e05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: - "1935:1935" # RTMP - "8080:8080" depends_on: + - frontend - web_server networks: - app_network @@ -35,6 +36,8 @@ services: - "5173" networks: - app_network + environment: + - VITE_API_URL=/api depends_on: - web_server diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 0a5f3ab..764f755 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -13,4 +13,4 @@ COPY . . EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file +CMD ["npm", "run", "docker-dev", "--", "--host", "--strictPort"] \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index cea08d6..9c9165f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,8 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --config vite.config.dev.ts", + "docker-dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" @@ -35,6 +36,6 @@ "tailwindcss": "^3.4.17", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", - "vite": "^6.0.5" + "vite": "latest" } } diff --git a/frontend/src/components/Video/ChatPanel.tsx b/frontend/src/components/Video/ChatPanel.tsx index 91a7c21..9a28b6e 100644 --- a/frontend/src/components/Video/ChatPanel.tsx +++ b/frontend/src/components/Video/ChatPanel.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import Input from "../Layout/Input"; import { useAuth } from "../../context/AuthContext"; import { useSocket } from "../../context/SocketContext"; -import Button from "../Layout/Button"; +import Button, { ToggleButton } from "../Layout/Button"; import AuthModal from "../Auth/AuthModal"; interface ChatMessage { @@ -21,6 +21,7 @@ const ChatPanel: React.FC = ({ streamId }) => { const [inputMessage, setInputMessage] = useState(""); const chatContainerRef = useRef(null); const { isLoggedIn, username } = useAuth(); + const [isChatVisible, setIsChatVisible] = useState(false); // Join chat room when component mounts useEffect(() => { @@ -92,6 +93,10 @@ const ChatPanel: React.FC = ({ streamId }) => { setInputMessage(""); }; + const toggleChat = () => { + setIsChatVisible((prev) => !prev); + }; + const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); @@ -115,72 +120,83 @@ const ChatPanel: React.FC = ({ streamId }) => { return ( <> -
-

Stream Chat

+ + {isChatVisible ? "Hide Chat" : "Show Chat"} + + {isChatVisible && ( +
+

Stream Chat

-
- {messages.map((msg, index) => ( -
- - {new Date(msg.time_sent).toLocaleTimeString()} - - + {messages.map((msg, index) => ( +
- {" "} - {msg.chatter_username}:{" "} - - {msg.message} -
- ))} -
- -
- {isLoggedIn && ( - <> - setInputMessage(e.target.value)} - onKeyDown={handleKeyPress} - placeholder={isLoggedIn ? "Type a message..." : "Login to chat"} - disabled={!isLoggedIn} - extraClasses="flex-grow" - onClick={() => !isLoggedIn && setShowAuthModal(true)} - /> - - - )} - - {!isLoggedIn && ( - - )} -
- {showAuthModal && ( -
- setShowAuthModal(false)} /> + + {new Date(msg.time_sent).toLocaleTimeString()} + + + {" "} + {msg.chatter_username}:{" "} + + {msg.message} +
+ ))}
- )} -
+ +
+ {isLoggedIn && ( + <> + setInputMessage(e.target.value)} + onKeyDown={handleKeyPress} + placeholder={ + isLoggedIn ? "Type a message..." : "Login to chat" + } + disabled={!isLoggedIn} + extraClasses="flex-grow" + onClick={() => !isLoggedIn && setShowAuthModal(true)} + /> + + + )} + + {!isLoggedIn && ( + + )} +
+ {showAuthModal && ( +
+ setShowAuthModal(false)} /> +
+ )} +
+ )} ); }; diff --git a/frontend/src/context/SocketContext.tsx b/frontend/src/context/SocketContext.tsx index 33b57e0..6f3d85f 100644 --- a/frontend/src/context/SocketContext.tsx +++ b/frontend/src/context/SocketContext.tsx @@ -1,4 +1,10 @@ -import React, { createContext, useContext, useEffect, useRef, useState } from "react"; +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { io, Socket } from "socket.io-client"; interface SocketContextType { @@ -26,19 +32,21 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ } console.log("Creating new socket connection"); - const newSocket = io('http://localhost:8080', { - path: '/socket.io/', - transports: ['websocket'], + const newSocket = io("http://localhost:8080", { + path: "/socket.io/", + transports: ["websocket"], withCredentials: true, reconnectionDelay: 1000, reconnectionDelayMax: 5000, reconnectionAttempts: 5, - timeout: 5000 + timeout: 5000, + autoConnect: true, + forceNew: true, }); - + socketRef.current = newSocket; setSocket(newSocket); - + newSocket.on("connect", () => { console.log("Socket connected!"); setIsConnected(true); @@ -65,15 +73,11 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ console.error("Socket connection error:", error); setIsLoading(false); if (newSocket) newSocket.disconnect(); - newSocket.connect(); }); newSocket.on("disconnect", (reason) => { - console.log( - "Socket disconnected! Reason: " + reason + " - Attempting reconnect..." - ); + console.log("Socket disconnected! Reason: " + reason); setIsConnected(false); - newSocket.connect(); }); return () => { diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index 8accfbf..afb5561 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import Navbar from "../components/Layout/Navbar"; -import Button, { ToggleButton } from "../components/Layout/Button"; +import Button from "../components/Layout/Button"; import ChatPanel from "../components/Video/ChatPanel"; // import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm"; import { useNavigate, useParams } from "react-router-dom"; @@ -19,7 +19,7 @@ interface StreamDataProps { streamerId: number; startTime: string; viewerCount: number; - categoryId: number; + categoryName: string; } const VideoPage: React.FC = ({ streamId }) => { @@ -29,11 +29,6 @@ const VideoPage: React.FC = ({ streamId }) => { // const [showCheckout, setShowCheckout] = useState(false); // const showReturn = window.location.search.includes("session_id"); // const navigate = useNavigate(); - const [isChatVisible, setIsChatVisible] = useState(false); - - const toggleChat = () => { - setIsChatVisible((prev) => !prev); - }; // useEffect(() => { // // Prevent scrolling when checkout is open @@ -57,19 +52,24 @@ const VideoPage: React.FC = ({ streamId }) => { if (!res.ok) { console.error("Failed to load stream data:", res.statusText); } - res.json().then((data) => { - // if (!data.validStream) navigate(`/`); - console.log(`Loading stream data for ${streamerName}`); - setStreamData({ - streamId: data.streamId, - streamTitle: data.streamTitle, - streamerName: data.streamerName, - streamerId: data.streamerId, - startTime: data.startTime, - viewerCount: data.viewerCount, - categoryId: data.categoryId, + res + .json() + .then((data) => { + // Transform snake_case to camelCase + const transformedData: StreamDataProps = { + streamId: streamId, + streamerName: data.username, + streamerId: data.user_id, + streamTitle: data.title, + startTime: data.start_time, + viewerCount: data.num_viewers, + categoryName: data.category_name, + }; + setStreamData(transformedData); + }) + .catch((error) => { + console.error("Error fetching stream data:", error); }); - }); }); }, [streamId, streamerName]); @@ -79,35 +79,48 @@ const VideoPage: React.FC = ({ streamId }) => {
- - {isChatVisible ? "Hide Chat" : "Show Chat"} - - {isChatVisible && ( -
- -
- )} + +
-

{streamData?.streamTitle}

-

{streamData?.streamerName}

+

+ {streamData ? streamData.streamTitle : "Loading..."} +

+
+
+ Streamer: + + {streamData ? streamData.streamerName : "Loading..."} + +
+
+ Viewer Count: + {streamData ? streamData.viewerCount : "0"} +
+
+ Started At: + + {streamData + ? new Date(streamData.startTime).toLocaleString() + : "Loading..."} + +
+
+ Category ID: + + {streamData ? streamData.categoryName : "Loading..."} + +
+
{isLoggedIn && ( - + )}
diff --git a/frontend/vite.config.dev.ts b/frontend/vite.config.dev.ts new file mode 100644 index 0000000..bfaec4c --- /dev/null +++ b/frontend/vite.config.dev.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react({ + // Add development-specific React plugin options + jsxRuntime: 'automatic' + })], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false + }, + '/socket.io': { + target: 'http://localhost:8080', + changeOrigin: true, + ws: true + } + } + }, + build: { + sourcemap: true, + outDir: 'dist' + }, + optimizeDeps: { + exclude: ['@vite/client', '@vite/env'] + }, + esbuild: { + logOverride: { 'this-is-undefined-in-esm': 'silent' } + } +}) \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 118feb0..eb42cdc 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,6 +12,12 @@ export default defineConfig({ target: 'http://web_server:5000', changeOrigin: true, } + }, + hmr: { + protocol: 'ws', + host: '127.0.0.1', + clientPort: 8080, + port: 5173 } - }, + } }) \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 07b6507..2e1b5ba 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -46,6 +46,10 @@ http { location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://web_server:5000; # flask-app is the name of the Flask container in docker-compose + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /socket.io/ { @@ -57,6 +61,9 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_cache_bypass $http_upgrade; + proxy_buffers 8 32k; + proxy_buffer_size 64k; + proxy_read_timeout 86400; } location /hmr/ { @@ -91,8 +98,20 @@ http { 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 / { - proxy_pass http://frontend:5173; # frontend is the name of the React container in docker-compose + proxy_pass http://frontend:5173; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; } } } diff --git a/web_server/blueprints/socket.py b/web_server/blueprints/socket.py index 41a9794..9932947 100644 --- a/web_server/blueprints/socket.py +++ b/web_server/blueprints/socket.py @@ -1,3 +1,10 @@ from flask_socketio import SocketIO -socketio = SocketIO(cors_allowed_origins="*", async_mode='gevent', logger=True, engineio_logger=True) +socketio = SocketIO( + cors_allowed_origins="*", + async_mode='gevent', + logger=False, # Reduce logging + engineio_logger=False, # Reduce logging + ping_timeout=5000, + ping_interval=25000 +) \ No newline at end of file diff --git a/web_server/utils/stream_utils.py b/web_server/utils/stream_utils.py index f7822ba..d7ed394 100644 --- a/web_server/utils/stream_utils.py +++ b/web_server/utils/stream_utils.py @@ -74,9 +74,12 @@ def user_stream(user_id: int, stream_id: int) -> dict: """ with Database() as db: stream = db.fetchone(""" - SELECT * FROM streams - WHERE user_id = ? - AND stream_id = ? + SELECT u.username, s.user_id, s.title, s.start_time, s.num_viewers, c.category_name + FROM streams AS s + JOIN categories AS c ON s.category_id = c.category_id + JOIN users AS u ON s.user_id = u.user_id + WHERE u.user_id = ? + AND s.stream_id = ? """, (user_id, stream_id)) return stream