diff --git a/config/nginx_config_example.conf b/config/nginx_config_example.conf deleted file mode 100644 index cbce633..0000000 --- a/config/nginx_config_example.conf +++ /dev/null @@ -1,88 +0,0 @@ -server { - listen 80; - server_name yourdomain.com; - - location / { - # Redirect all HTTP requests to HTTPS for secure communication - return 301 https://$server_name$request_uri; - } -} - -server { - listen 443 ssl; - server_name yourdomain.com; - - # SSL certificate paths for secure connection - ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; - - # Configuration for serving static files - location /upload/ { - # Define the local directory containing uploaded static files - alias /home/youruser/Documents/Construction-Hazard-Detection/static/uploads/; - - # Enable directory indexing to allow viewing of all files within the directory - autoindex on; - - # Ensure unrestricted access to this location for all users - allow all; - } - - # WebSocket proxy configuration for FastAPI application - location /ws/ { - # Proxy requests to the FastAPI application running on localhost, port 8000 - proxy_pass http://127.0.0.1:8000; - - # Specify HTTP version 1.1 to ensure compatibility with WebSocket protocol - proxy_http_version 1.1; - - # Set headers for WebSocket upgrade to switch protocols as needed - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Pass the original host header to the proxied server - proxy_set_header Host $host; - - # Capture the client’s real IP address and forward it to the proxied server - proxy_set_header X-Real-IP $remote_addr; - - # Forward additional client information, including the originating IP addresses - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # Indicate the original protocol (HTTP or HTTPS) used by the client - proxy_set_header X-Forwarded-Proto $scheme; - - # Disable buffering to immediately forward data to the client, improving real-time updates - proxy_buffering off; - } - - # General HTTP proxy configuration for FastAPI - location / { - # Proxy all other HTTP requests to the FastAPI application on localhost, port 8000 - proxy_pass http://127.0.0.1:8000; - - # Use HTTP version 1.1 for improved connection handling - proxy_http_version 1.1; - - # Forward the original host information to maintain request integrity - proxy_set_header Host $host; - - # Pass on the real IP address of the client - proxy_set_header X-Real-IP $remote_addr; - - # Add the client's forwarded IP addresses to the header - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - # Include information about the original protocol (HTTP or HTTPS) - proxy_set_header X-Forwarded-Proto $scheme; - - # Specify SSL usage for the forwarded request - proxy_set_header X-Forwarded-SSL on; - - # Include the original port number in the forwarded headers - proxy_set_header X-Forwarded-Port $server_port; - - # Disable buffering to ensure immediate data forwarding - proxy_buffering off; - } -} diff --git a/config/streaming_web_frontend.conf b/config/streaming_web_frontend.conf new file mode 100644 index 0000000..86a8a25 --- /dev/null +++ b/config/streaming_web_frontend.conf @@ -0,0 +1,51 @@ +server { + # Listen on port 80 (HTTP) + listen 80; + server_name changdar-server.mooo.com; + + # Set the root directory for the website, pointing to the dist folder of the frontend build + root /var/www/html; + + # Specify the default homepage file + index index.html; + + # Handle static file requests + location / { + # Check if the requested file exists, otherwise fallback to index.html (for SPA) + try_files $uri /index.html; + } + + # Proxy /api requests to the backend service + location /api/ { + proxy_pass http://127.0.0.1:8800; # Backend service address + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Necessary configuration to support WebSocket upgrades + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Configure caching for static assets + location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|mp4)$ { + expires 6M; + access_log off; + add_header Cache-Control "public"; + } + + # Configure the 404 error page + error_page 404 /index.html; + + # Handle WebSocket live streaming on a specific route (if required) + location /ws/ { + proxy_pass http://127.0.0.1:8800; # Backend WebSocket service address + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/examples/streaming_web/READMD-zh-tw.md b/examples/streaming_web/backend/READMD-zh-tw.md similarity index 96% rename from examples/streaming_web/READMD-zh-tw.md rename to examples/streaming_web/backend/READMD-zh-tw.md index 74e44b3..a3f333a 100644 --- a/examples/streaming_web/READMD-zh-tw.md +++ b/examples/streaming_web/backend/READMD-zh-tw.md @@ -15,7 +15,7 @@ 或是 ```sh - uvicorn examples.streaming_web.app:sio_app --host 127.0.0.1 --port 8000 + uvicorn examples.streaming_web.backend.app:sio_app --host 127.0.0.1 --port 8000 ``` 2. **打開您的網頁瀏覽器並導航至:** diff --git a/examples/streaming_web/README.md b/examples/streaming_web/backend/README.md similarity index 96% rename from examples/streaming_web/README.md rename to examples/streaming_web/backend/README.md index c1720d3..00fbbbb 100644 --- a/examples/streaming_web/README.md +++ b/examples/streaming_web/backend/README.md @@ -15,7 +15,7 @@ This section provides an example implementation of a Streaming Web application, or ```sh - uvicorn examples.streaming_web.app:sio_app --host 127.0.0.1 --port 8000 + uvicorn examples.streaming_web.backend.app:sio_app --host 127.0.0.1 --port 8000 ``` 2. **Open your web browser and navigate to:** diff --git a/examples/streaming_web/__init__.py b/examples/streaming_web/backend/__init__.py similarity index 100% rename from examples/streaming_web/__init__.py rename to examples/streaming_web/backend/__init__.py diff --git a/examples/streaming_web/app.py b/examples/streaming_web/backend/app.py similarity index 96% rename from examples/streaming_web/app.py rename to examples/streaming_web/backend/app.py index 93a0e74..d96fc17 100644 --- a/examples/streaming_web/app.py +++ b/examples/streaming_web/backend/app.py @@ -52,7 +52,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]: # Mount the static files directory to serve static assets app.mount( '/static', - StaticFiles(directory='examples/streaming_web/static'), + StaticFiles(directory='examples/streaming_web/backend/static'), name='static', ) diff --git a/examples/streaming_web/routes.py b/examples/streaming_web/backend/routes.py similarity index 61% rename from examples/streaming_web/routes.py rename to examples/streaming_web/backend/routes.py index 787b044..7bc5d02 100644 --- a/examples/streaming_web/routes.py +++ b/examples/streaming_web/backend/routes.py @@ -11,10 +11,7 @@ from fastapi import WebSocket from fastapi import WebSocketDisconnect from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles from fastapi_limiter.depends import RateLimiter -from starlette.responses import Response -from starlette.templating import Jinja2Templates from .utils import RedisManager from .utils import Utils @@ -23,9 +20,6 @@ # Create an API router for defining routes router = APIRouter() -templates = Jinja2Templates( - directory='examples/streaming_web/templates', -) def register_routes(app: Any) -> None: @@ -37,96 +31,62 @@ def register_routes(app: Any) -> None: """ app.include_router(router) - # Mount the static files directory to serve static assets - app.mount( - '/static', - StaticFiles(directory='examples/streaming_web/static'), - name='static', - ) - +# Create rate limiters for the API routes rate_limiter_index = RateLimiter(times=60, seconds=60) rate_limiter_label = RateLimiter(times=6000, seconds=6000) -@router.get('/', dependencies=[Depends(rate_limiter_index)]) -async def index(request: Request) -> Response: - """ - Renders the index page with available labels from Redis. - - Args: - request (Request): The HTTP request object. - - Returns: - Response: The rendered HTML template for the index page, - containing available labels. - """ - try: - # Retrieve available labels from Redis - labels = await redis_manager.get_labels() - except Exception as e: - # Raise HTTP 500 error if labels cannot be fetched - raise HTTPException( - status_code=500, detail=f"Failed to fetch labels: {str(e)}", - ) - # Render and return the index template with the fetched labels - return templates.TemplateResponse( - request, - 'index.html', - { - 'labels': labels, - }, - ) - - -@router.get('/label/{label}', dependencies=[Depends(rate_limiter_label)]) -async def label_page( - request: Request, - label: str, -) -> Response: +@router.get('/api/labels', dependencies=[Depends(rate_limiter_index)]) +async def get_labels() -> JSONResponse: """ Renders the page for a specific label with available labels from Redis. Args: - request (Request): The HTTP request object. - label (str): The label identifier to display on the page. + None Returns: - Response: The rendered HTML template for the label page - with available labels. + JSONResponse: A JSON response containing the labels. """ try: # Retrieve available labels from Redis labels = await redis_manager.get_labels() - # Check if the requested label is present in the available labels - if label not in labels: - # Raise HTTP 404 error if the label is not found - raise HTTPException( - status_code=404, detail=f"Label '{label}' not found", - ) - except HTTPException as e: - # Re-raise HTTP exceptions - raise e + except ValueError as ve: + print(f"ValueError while fetching labels: {str(ve)}") + raise HTTPException( + status_code=400, + detail=f"Invalid data encountered: {str(ve)}", + ) + except KeyError as ke: + print(f"KeyError while fetching labels: {str(ke)}") + raise HTTPException( + status_code=404, + detail=f"Missing key encountered: {str(ke)}", + ) + except ConnectionError as ce: + print(f"ConnectionError while fetching labels: {str(ce)}") + raise HTTPException( + status_code=503, + detail=f"Failed to connect to the database: {str(ce)}", + ) + except TimeoutError as te: + print(f"TimeoutError while fetching labels: {str(te)}") + raise HTTPException( + status_code=504, + detail=f"Request timed out: {str(te)}", + ) except Exception as e: - # Log unexpected errors and raise HTTP 500 error print(f"Unexpected error while fetching labels: {str(e)}") raise HTTPException( - status_code=500, detail=f"Failed to fetch labels: {str(e)}", + status_code=500, + detail=f"Failed to fetch labels: {str(e)}", ) - # Render and return the label template with the fetched labels - return templates.TemplateResponse( - request, - 'label.html', - { - 'label': label, - 'labels': labels, - }, - ) + return JSONResponse(content={'labels': labels}) -@router.websocket('/ws/label/{label}') +@router.websocket('/api/ws/labels/{label}') async def websocket_label_stream(websocket: WebSocket, label: str) -> None: """ Establishes a WebSocket connection to stream updated frames @@ -168,7 +128,7 @@ async def websocket_label_stream(websocket: WebSocket, label: str) -> None: print('WebSocket connection closed') -@router.post('/webhook') +@router.post('/api/webhook') async def webhook(request: Request) -> JSONResponse: """ Processes incoming webhook requests by logging the request body. @@ -188,7 +148,7 @@ async def webhook(request: Request) -> JSONResponse: # Uncomment and use the following endpoint for file uploads if needed -@router.post('/upload') +@router.post('/api/upload') async def upload_file(file: UploadFile) -> JSONResponse: """ Saves an uploaded file to the designated upload folder @@ -200,10 +160,10 @@ async def upload_file(file: UploadFile) -> JSONResponse: Returns: JSONResponse: A JSON response containing the URL of the uploaded file. """ - UPLOAD_FOLDER = Path('static/uploads') + UPLOAD_FOLDER = Path('uploads') UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True) - # 检查 filename 是否为 None + # Check if the file has a filename if not file.filename: raise HTTPException(status_code=400, detail='Filename is missing') @@ -216,5 +176,5 @@ async def upload_file(file: UploadFile) -> JSONResponse: status_code=500, detail=f"Failed to save file: {str(e)}", ) - url = f"https://yihong-server.mooo.com/static/uploads/{file.filename}" + url = f"/uploads/{file.filename}" return JSONResponse(content={'url': url}) diff --git a/examples/streaming_web/sockets.py b/examples/streaming_web/backend/sockets.py similarity index 100% rename from examples/streaming_web/sockets.py rename to examples/streaming_web/backend/sockets.py diff --git a/examples/streaming_web/utils.py b/examples/streaming_web/backend/utils.py similarity index 94% rename from examples/streaming_web/utils.py rename to examples/streaming_web/backend/utils.py index 1c20e0f..43a6958 100644 --- a/examples/streaming_web/utils.py +++ b/examples/streaming_web/backend/utils.py @@ -33,7 +33,7 @@ def __init__( redis_password (str): The Redis password for authentication. """ self.redis_host: str = os.getenv('REDIS_HOST') or redis_host - self.redis_port: int = int(os.getenv('REDIS_PORT')) or redis_port + self.redis_port: int = int(os.getenv('REDIS_PORT') or redis_port) self.redis_password: str = os.getenv( 'REDIS_PASSWORD', ) or redis_password @@ -74,11 +74,16 @@ async def get_labels(self) -> list[str]: match = re.match( r'stream_frame:([\w\x80-\xFF]+)_([\w\x80-\xFF]+)', key, ) - if match: - label, stream_name = match.groups() + if not match: + continue + + label, stream_name = match.groups() + + if 'test' in label: + continue + + labels.add(label) - if label and 'test' not in label: - labels.add(f"{label}") if cursor == 0: # Exit loop if scan cursor has reached the end break diff --git a/examples/streaming_web/frontend/package.json b/examples/streaming_web/frontend/package.json new file mode 100644 index 0000000..ddfb195 --- /dev/null +++ b/examples/streaming_web/frontend/package.json @@ -0,0 +1,20 @@ +{ + "name": "streaming-web-frontend", + "version": "1.0.0", + "description": "Frontend for the streaming web application with FastAPI backend.", + "main": "index.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "keywords": [], + "author": "yihong1120", + "license": "AGPL-3.0-only", + "dependencies": { + "axios": "^1.5.0" + }, + "devDependencies": { + "vite": "^5.4.11" + } +} diff --git a/examples/streaming_web/static/css/styles.css b/examples/streaming_web/frontend/public/css/styles.css similarity index 100% rename from examples/streaming_web/static/css/styles.css rename to examples/streaming_web/frontend/public/css/styles.css diff --git a/examples/streaming_web/static/favicon.ico b/examples/streaming_web/frontend/public/favicon.ico similarity index 100% rename from examples/streaming_web/static/favicon.ico rename to examples/streaming_web/frontend/public/favicon.ico diff --git a/examples/streaming_web/frontend/public/index.html b/examples/streaming_web/frontend/public/index.html new file mode 100644 index 0000000..5283823 --- /dev/null +++ b/examples/streaming_web/frontend/public/index.html @@ -0,0 +1,33 @@ + + + + + + Camera Streams + + + + + + + + + + + + + + +

Camera Labels

+ + +
+ + diff --git a/examples/streaming_web/frontend/public/js/index.js b/examples/streaming_web/frontend/public/js/index.js new file mode 100644 index 0000000..2641014 --- /dev/null +++ b/examples/streaming_web/frontend/public/js/index.js @@ -0,0 +1,37 @@ +const API_URL = '/api'; // Backend API base path + +// Execute when the document's DOM is fully loaded +document.addEventListener('DOMContentLoaded', async () => { + const cameraGrid = document.getElementById('camera-grid'); // Reference to the camera grid container + + try { + // Fetch the list of labels from the backend + const response = await fetch(`${API_URL}/labels`); + if (!response.ok) throw new Error('Failed to fetch labels'); // Throw an error if the response is not OK + const data = await response.json(); // Parse the JSON response + + // Render the fetched labels onto the page + const labels = data.labels || []; // Default to an empty array if no labels are returned + labels.forEach(label => { + const cameraDiv = document.createElement('div'); // Create a container for each label + cameraDiv.className = 'camera'; // Assign a class for styling + + const link = document.createElement('a'); // Create a clickable link + link.href = `/label.html?label=${encodeURIComponent(label)}`; // Encode the label to ensure URL safety + + const title = document.createElement('h2'); // Create a title for the label + title.textContent = label; // Set the label text + + const description = document.createElement('p'); // Create a description under the title + description.textContent = `View ${label}`; // Set the descriptive text + + link.appendChild(title); // Add the title to the link + link.appendChild(description); // Add the description to the link + cameraDiv.appendChild(link); // Add the link to the camera container + cameraGrid.appendChild(cameraDiv); // Add the camera container to the grid + }); + } catch (error) { + // Log an error message if fetching or processing labels fails + console.error('Error fetching labels:', error); + } +}); diff --git a/examples/streaming_web/frontend/public/js/label.js b/examples/streaming_web/frontend/public/js/label.js new file mode 100644 index 0000000..df1012f --- /dev/null +++ b/examples/streaming_web/frontend/public/js/label.js @@ -0,0 +1,117 @@ +let socket; // Define the WebSocket globally to manage the connection throughout the script + +// Execute when the document's DOM is fully loaded +document.addEventListener('DOMContentLoaded', () => { + const labelTitle = document.getElementById('label-title'); // Reference to the label title element + const cameraGrid = document.getElementById('camera-grid'); // Reference to the camera grid container + const urlParams = new URLSearchParams(window.location.search); // Extract query parameters from the URL + const label = urlParams.get('label'); // Retrieve the 'label' parameter + + // If the label is missing from the URL, log an error and terminate further execution + if (!label) { + console.error('Label parameter is missing in the URL'); + return; + } + + // Set the page's title to the label name + labelTitle.textContent = label; + + // Initialise the WebSocket connection for the given label + initializeWebSocket(label); + + // Ensure the WebSocket connection is closed when the page is unloaded + window.addEventListener('beforeunload', () => { + if (socket) { + socket.close(); // Close the WebSocket connection gracefully + } + }); +}); + +/** + * Initialise the WebSocket connection for live updates. + * + * @param {string} label - The label used to establish the WebSocket connection. + */ +function initializeWebSocket(label) { + // Determine the appropriate WebSocket protocol based on the page's protocol + const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + socket = new WebSocket(`${protocol}${window.location.host}/api/ws/labels/${encodeURIComponent(label)}`); + + // Handle WebSocket connection establishment + socket.onopen = () => { + console.log('WebSocket connected!'); + + // Set up a heartbeat mechanism to keep the connection alive + setInterval(() => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify({ type: 'ping' })); // Send a ping message every 30 seconds + } + }, 30000); + }; + + // Handle incoming messages from the WebSocket server + socket.onmessage = (event) => { + const data = JSON.parse(event.data); // Parse the received JSON data + handleUpdate(data, label); // Process the update based on the current label + }; + + // Handle WebSocket errors + socket.onerror = (error) => console.error('WebSocket error:', error); + + // Handle WebSocket closure + socket.onclose = () => console.log('WebSocket closed'); +} + +/** + * Handle updates received from the WebSocket server. + * + * @param {Object} data - The data received from the WebSocket server. + * @param {string} currentLabel - The label currently being displayed. + */ +function handleUpdate(data, currentLabel) { + if (data.label === currentLabel) { + console.log('Received update for current label:', data.label); + updateCameraGrid(data.images); // Update the camera grid with new images + } else { + console.log('Received update for different label:', data.label); + } +} + +/** + * Update the camera grid with new images. + * + * @param {Array} images - An array of image data, each containing a key and base64-encoded image. + */ +function updateCameraGrid(images) { + const cameraGrid = document.getElementById('camera-grid'); // Reference to the camera grid container + images.forEach(({ key, image }) => { + // Check if a camera div for the given key already exists + const existingCameraDiv = document.querySelector(`.camera[data-key="${key}"]`); + if (existingCameraDiv) { + // Update the existing image source + const img = existingCameraDiv.querySelector('img'); + img.src = `data:image/png;base64,${image}`; + } else { + // Create a new camera div if it doesn't exist + const cameraDiv = document.createElement('div'); + cameraDiv.className = 'camera'; // Add a class for styling + cameraDiv.dataset.key = key; // Set a custom data attribute with the key + + // Create a title for the camera + const title = document.createElement('h2'); + title.textContent = key.split('_').pop(); // Extract and display a simplified key + + // Create an image element for the camera + const img = document.createElement('img'); + img.src = `data:image/png;base64,${image}`; // Set the base64-encoded image as the source + img.alt = `${key} image`; // Add an alternative text for accessibility + + // Append the title and image to the camera div + cameraDiv.appendChild(title); + cameraDiv.appendChild(img); + + // Append the camera div to the grid + cameraGrid.appendChild(cameraDiv); + } + }); +} diff --git a/examples/streaming_web/frontend/public/label.html b/examples/streaming_web/frontend/public/label.html new file mode 100644 index 0000000..67cd243 --- /dev/null +++ b/examples/streaming_web/frontend/public/label.html @@ -0,0 +1,29 @@ + + + + + + + + + + + Label Details + + + + + + + + + + + + +

+ + +
+ + diff --git a/examples/streaming_web/frontend/vite.config.js b/examples/streaming_web/frontend/vite.config.js new file mode 100644 index 0000000..23b6108 --- /dev/null +++ b/examples/streaming_web/frontend/vite.config.js @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite'; + +// Export the Vite configuration object +export default defineConfig({ + // Set the root directory for the project to 'public' + root: 'public', + + server: { + // Define the port for the development server + port: 8888, + + proxy: { + // Configure proxy settings for the '/api' prefix + '/api': { + // The backend server to forward API requests to + target: 'http://127.0.0.1:8000', + + // Enable changing the origin of the host header to the target URL + changeOrigin: true, + + // Enable WebSocket proxying + ws: true, + }, + }, + }, + + build: { + // Specify the output directory for the build process + outDir: '../dist', + }, +}); diff --git a/examples/streaming_web/static/js/camera.js b/examples/streaming_web/static/js/camera.js deleted file mode 100644 index c4cdd87..0000000 --- a/examples/streaming_web/static/js/camera.js +++ /dev/null @@ -1,7 +0,0 @@ -$(document).ready(function(){ - function updateImage() { - var src = $("#camera-image").attr("src").split('?')[0]; // Remove any existing query string - $("#camera-image").attr("src", src + '?' + new Date().getTime()); - } - setInterval(updateImage, 5000); // Update every 5 seconds -}); diff --git a/examples/streaming_web/static/js/index.js b/examples/streaming_web/static/js/index.js deleted file mode 100644 index 8c8a360..0000000 --- a/examples/streaming_web/static/js/index.js +++ /dev/null @@ -1,8 +0,0 @@ -$(document).ready(function(){ - setInterval(function(){ - $('img').each(function(){ - var src = $(this).attr('src').split('?')[0]; // Remove any existing query string - $(this).attr('src', src + '?' + new Date().getTime()); - }); - }, 5000); // Update every 5 seconds -}); diff --git a/examples/streaming_web/static/js/label.js b/examples/streaming_web/static/js/label.js deleted file mode 100644 index 44761d6..0000000 --- a/examples/streaming_web/static/js/label.js +++ /dev/null @@ -1,87 +0,0 @@ -$(document).ready(() => { - initializeWebSocket(); -}); - -/** - * Initialize the WebSocket connection and set up event handlers. - */ -function initializeWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; - const currentPageLabel = getCurrentPageLabel(); - - const socket = new WebSocket(`${protocol}${window.location.host}/ws/label/${currentPageLabel}`); - - socket.onopen = () => { - console.log('WebSocket connected!'); - }; - - socket.onmessage = (event) => { - const data = JSON.parse(event.data); - handleUpdate(data, currentPageLabel); - }; - - socket.onerror = (error) => { - console.error('WebSocket error:', error); - }; - - socket.onclose = () => { - console.log('WebSocket closed'); - }; -} - -/** - * Get the label of the current page. - * @returns {string} The label of the current page. - */ -function getCurrentPageLabel() { - return $('h1').text(); -} - -/** - * Handle WebSocket updates for multiple images. - * @param {Object} data - The received data - * @param {string} currentPageLabel - The label of the current page - */ -function handleUpdate(data, currentPageLabel) { - if (data.label === currentPageLabel) { - console.log('Received update for current label:', data.label); - updateCameraGrid(data.images); // 更新為處理多鏡頭影像 - } else { - console.log('Received update for different label:', data.label); - } -} - -/** - * Update the camera grid with new images for multiple cameras. - * @param {Array} images - The array of images with key and base64 data. - */ -function updateCameraGrid(images) { - images.forEach((cameraData) => { - // 檢查是否已經存在相同的 image_name - const existingCameraDiv = $(`.camera h2:contains(${cameraData.key.split('_').pop()})`).closest('.camera'); - - if (existingCameraDiv.length > 0) { - // 更新現有的圖像 - existingCameraDiv.find('img').attr('src', `data:image/png;base64,${cameraData.image}`); - } else { - // 如果沒有相同的 image_name,則創建新的圖區 - const cameraDiv = createCameraDiv(cameraData); - $('.camera-grid').append(cameraDiv); - } - }); -} - -/** - * Create a camera div element. - * @param {Object} cameraData - The data for creating the camera div - * @returns {HTMLElement} - The div element containing the image and title - */ -function createCameraDiv(cameraData) { - const cameraDiv = $('
').addClass('camera'); - // 只保留 _ 之後的部分作為標題 - const titleText = cameraData.key.split('_').pop(); - const title = $('

').text(titleText); - const img = $('').attr('src', `data:image/png;base64,${cameraData.image}`).attr('alt', `${cameraData.key} image`); - cameraDiv.append(title).append(img); - return cameraDiv[0]; -} diff --git a/examples/streaming_web/templates/camera.html b/examples/streaming_web/templates/camera.html deleted file mode 100644 index b3b67a4..0000000 --- a/examples/streaming_web/templates/camera.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - -{{ camera_id }} - - - - - - -

{{ camera_id }}

- -{{ camera_id }} - - diff --git a/examples/streaming_web/templates/index.html b/examples/streaming_web/templates/index.html deleted file mode 100644 index 07e30a5..0000000 --- a/examples/streaming_web/templates/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Camera Streams - - - - - -

Camera Labels

-
- {% for label in labels %} - - {% endfor %} -
- - diff --git a/examples/streaming_web/templates/label.html b/examples/streaming_web/templates/label.html deleted file mode 100644 index 152bc36..0000000 --- a/examples/streaming_web/templates/label.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - {{ label | e }} - - - - - -

{{ label | e }}

-
- {% for image, image_name in image_data %} -
-

{{ image_name | e }}

- {{ label | e }} image -
- {% endfor %} -
- - diff --git a/examples/streaming_web/templates/test_model.html b/examples/streaming_web/templates/test_model.html deleted file mode 100644 index 625b2fc..0000000 --- a/examples/streaming_web/templates/test_model.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - Test Model - - - - -

Test Model

-
-
-
-
-
-
-
-
-
- -
-

Real-Time Detection

-
- - - diff --git a/tests/examples/streaming_web/backend/__init__.py b/tests/examples/streaming_web/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/examples/streaming_web/app_test.py b/tests/examples/streaming_web/backend/app_test.py similarity index 87% rename from tests/examples/streaming_web/app_test.py rename to tests/examples/streaming_web/backend/app_test.py index b9028ac..7ef4a6b 100644 --- a/tests/examples/streaming_web/app_test.py +++ b/tests/examples/streaming_web/backend/app_test.py @@ -9,7 +9,7 @@ from fastapi.testclient import TestClient from fastapi_limiter import FastAPILimiter -import examples.streaming_web.app as app_module +import examples.streaming_web.backend.app as app_module class TestStreamingWebApp(unittest.IsolatedAsyncioTestCase): @@ -25,7 +25,7 @@ def setUp(self) -> None: self.client = TestClient(self.app) @patch( - 'examples.streaming_web.app.redis_manager.client', + 'examples.streaming_web.backend.app.redis_manager.client', new_callable=AsyncMock, ) async def test_redis_connection( @@ -53,7 +53,7 @@ async def test_redis_connection( password='virtualpass', decode_responses=True, ) - @patch('examples.streaming_web.app.CORSMiddleware') + @patch('examples.streaming_web.backend.app.CORSMiddleware') def test_cors_initialization(self, mock_cors: MagicMock) -> None: """ Test that CORS is properly initialized for the FastAPI app. @@ -69,11 +69,11 @@ def test_cors_initialization(self, mock_cors: MagicMock) -> None: ) @patch( - 'examples.streaming_web.app.FastAPILimiter.init', + 'examples.streaming_web.backend.app.FastAPILimiter.init', new_callable=AsyncMock, ) @patch( - 'examples.streaming_web.app.redis_manager.client', + 'examples.streaming_web.backend.app.redis_manager.client', new_callable=AsyncMock, ) async def test_rate_limiter_initialization( @@ -96,11 +96,11 @@ def test_app_running_configuration( Test that the application runs with the expected configurations. """ uvicorn.run( - 'examples.streaming_web.app:sio_app', + 'examples.streaming_web.backend.app:sio_app', host='127.0.0.1', port=8000, log_level='info', ) mock_uvicorn_run.assert_called_once_with( - 'examples.streaming_web.app:sio_app', + 'examples.streaming_web.backend.app:sio_app', host='127.0.0.1', port=8000, log_level='info', ) diff --git a/tests/examples/streaming_web/routes_test.py b/tests/examples/streaming_web/backend/routes_test.py similarity index 50% rename from tests/examples/streaming_web/routes_test.py rename to tests/examples/streaming_web/backend/routes_test.py index c28879a..8c1dd91 100644 --- a/tests/examples/streaming_web/routes_test.py +++ b/tests/examples/streaming_web/backend/routes_test.py @@ -2,17 +2,14 @@ import asyncio import unittest -from unittest.mock import AsyncMock -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from fastapi import FastAPI from fastapi.testclient import TestClient from fastapi_limiter import FastAPILimiter -from examples.streaming_web.routes import rate_limiter_index -from examples.streaming_web.routes import rate_limiter_label -from examples.streaming_web.routes import register_routes -from examples.streaming_web.utils import RedisManager +from examples.streaming_web.backend.routes import rate_limiter_index, rate_limiter_label, register_routes +from examples.streaming_web.backend.utils import RedisManager class TestRoutes(unittest.IsolatedAsyncioTestCase): @@ -49,71 +46,31 @@ def tearDown(self) -> None: self.app.dependency_overrides.clear() patch.stopall() - @patch( - 'examples.streaming_web.utils.RedisManager.get_labels', - new_callable=AsyncMock, - ) + @patch('examples.streaming_web.backend.utils.RedisManager.get_labels', new_callable=AsyncMock) def test_index(self, mock_get_labels: AsyncMock): """ - Test the index route to ensure it renders the correct template - and context. + Test the index route to ensure it renders the correct response. """ - # Mock the get_labels function to return a list of labels mock_get_labels.return_value = ['label1', 'label2'] - - # Make a GET request to the index route - response = self.client.get('/') + response = self.client.get('/api/labels') self.assertEqual(response.status_code, 200) - self.assertIn('label1', response.text) + self.assertEqual(response.json(), {'labels': ['label1', 'label2']}) - @patch( - 'examples.streaming_web.utils.RedisManager.get_labels', - new_callable=AsyncMock, - ) - def test_label_page_found(self, mock_get_labels: AsyncMock): + @patch('examples.streaming_web.backend.utils.RedisManager.get_keys_for_label', new_callable=AsyncMock) + def test_label_page_found(self, mock_get_keys: AsyncMock): """ - Test the label route to ensure it renders the correct template - and context. - - Args: - mock_get_labels (AsyncMock): Mocked get_labels function. + Test the WebSocket route for an existing label. """ - # Mock the get_labels function to return a list of labels - mock_get_labels.return_value = ['test_label'] - - # Make a GET request to the label route - response = self.client.get('/label/test_label') + mock_get_keys.return_value = ['stream_frame:label1_Cam0'] + response = self.client.get('/api/labels') self.assertEqual(response.status_code, 200) - mock_get_labels.assert_called_once_with() - - @patch( - 'examples.streaming_web.utils.RedisManager.get_labels', - new_callable=AsyncMock, - ) - def test_label_page_not_found(self, mock_get_labels: AsyncMock): - """ - Test the label route to ensure it returns a 404 error when the label - is not found. - - Args: - mock_get_labels (AsyncMock): Mocked get_labels function. - """ - # Mock the get_labels function to return a different label - mock_get_labels.return_value = ['another_label'] - - # Make a GET request to the label route - response = self.client.get('/label/test_label') - self.assertEqual(response.status_code, 404) def test_webhook(self): """ Test the webhook route to ensure it returns a successful response. """ - # Define the request body body = {'event': 'test_event'} - - # Make a POST request to the webhook route - response = self.client.post('/webhook', json=body) + response = self.client.post('/api/webhook', json=body) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {'status': 'ok'}) @@ -121,12 +78,9 @@ def test_upload_file_successful(self): """ Test the upload route to ensure it returns a successful response. """ - # Define the file content file_content = b'fake image data' - - # Create a file object with the content files = {'file': ('test_image.png', file_content, 'image/png')} - response = self.client.post('/upload', files=files) + response = self.client.post('/api/upload', files=files) self.assertEqual(response.status_code, 200) self.assertIn('url', response.json()) @@ -135,8 +89,9 @@ def test_upload_file_missing_filename(self): Test the upload route to ensure it returns a 422 error when the filename is missing. """ + # FastAPI automatically raises a 422 for missing file validation files = {'file': ('', b'data', 'image/png')} - response = self.client.post('/upload', files=files) + response = self.client.post('/api/upload', files=files) self.assertEqual(response.status_code, 422) diff --git a/tests/examples/streaming_web/sockets_test.py b/tests/examples/streaming_web/backend/sockets_test.py similarity index 97% rename from tests/examples/streaming_web/sockets_test.py rename to tests/examples/streaming_web/backend/sockets_test.py index 874c220..4aed449 100644 --- a/tests/examples/streaming_web/sockets_test.py +++ b/tests/examples/streaming_web/backend/sockets_test.py @@ -8,8 +8,8 @@ import socketio -from examples.streaming_web.sockets import register_sockets -from examples.streaming_web.sockets import update_images +from examples.streaming_web.backend.sockets import register_sockets +from examples.streaming_web.backend.sockets import update_images class TestSockets(unittest.IsolatedAsyncioTestCase): diff --git a/tests/examples/streaming_web/utils_test.py b/tests/examples/streaming_web/backend/utils_test.py similarity index 97% rename from tests/examples/streaming_web/utils_test.py rename to tests/examples/streaming_web/backend/utils_test.py index cf27cdc..d72c1a1 100644 --- a/tests/examples/streaming_web/utils_test.py +++ b/tests/examples/streaming_web/backend/utils_test.py @@ -8,8 +8,8 @@ import redis from fastapi import WebSocket -from examples.streaming_web.utils import RedisManager -from examples.streaming_web.utils import Utils +from examples.streaming_web.backend.utils import RedisManager +from examples.streaming_web.backend.utils import Utils class TestUtils(unittest.IsolatedAsyncioTestCase):