"""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))