From 8de0ac50d31466f41d8e5f38b1bd4b58f22b2428 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Tue, 14 Apr 2026 09:21:08 -0400 Subject: [PATCH] Add backend/app/main.py --- backend/app/main.py | 127 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 backend/app/main.py diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..b33ecdb --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,127 @@ +"""Main FastAPI application for Deltacast SDI Recorder.""" + +import asyncio +import json +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse +from pathlib import Path + +from .config import Settings +from .recorders.recorder import RecorderManager +from .recorders.scte35 import SCTE35Manager +from .utils.hls import HLSPreviewManager +from .api.routes import router +from .api import routes as routes_module # to set module-level globals +from .api.websocket import ConnectionManager + +logger = logging.getLogger(__name__) +settings = Settings() + +# Global manager instances +recorder_manager: RecorderManager | None = None +scte35_manager: SCTE35Manager | None = None +hls_manager: HLSPreviewManager | None = None +connection_manager = ConnectionManager() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Initialize and cleanup application resources.""" + global recorder_manager, scte35_manager, hls_manager + + # Startup + logger.info("Starting Deltacast SDI Recorder...") + + recorder_manager = RecorderManager(settings) + recorder_manager.initialize() + + scte35_manager = SCTE35Manager() + + hls_manager = HLSPreviewManager(settings) + + # Inject managers into routes module + routes_module.recorder_manager = recorder_manager + routes_module.scte35_manager = scte35_manager + + # Start background task for status broadcasting + status_task = asyncio.create_task(broadcast_port_status()) + + logger.info("Deltacast SDI Recorder started successfully") + + yield # Application runs here + + # Shutdown + logger.info("Shutting down Deltacast SDI Recorder...") + status_task.cancel() + try: + await status_task + except asyncio.CancelledError: + pass + + # Stop all recorders and previews + for port_index in range(settings.deltacast_port_count): + await recorder_manager.stop_recording(port_index) + await hls_manager.stop_preview(port_index) + + logger.info("Shutdown complete") + + +async def broadcast_port_status(): + """Background task: broadcast port status to all WebSocket clients every 1 second.""" + while True: + try: + if recorder_manager is not None: + statuses = recorder_manager.get_all_status() + data = { + "type": "port_status", + "ports": [s.model_dump() for s in statuses] + } + await connection_manager.broadcast(data) + except Exception as e: + logger.error(f"Error broadcasting port status: {e}") + await asyncio.sleep(1) + + +app = FastAPI( + title="Deltacast SDI Recorder", + description="Web GUI for Deltacast SDI capture card recording", + version="1.0.0", + lifespan=lifespan +) + +# CORS - allow all origins for local network use +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include API router +app.include_router(router) + + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """WebSocket endpoint for real-time port status updates.""" + await connection_manager.connect(websocket) + try: + while True: + # Keep connection alive, receive any messages + # (not used but required to detect disconnect) + data = await websocket.receive_text() + except WebSocketDisconnect: + connection_manager.disconnect(websocket) + + +@app.get("/hls/{filename}") +async def serve_hls(filename: str): + """Serve HLS segment and playlist files.""" + hls_path = Path("/tmp/hls") / filename + if not hls_path.exists(): + raise HTTPException(status_code=404, detail="HLS file not found") + return FileResponse(str(hls_path))