import os from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from starlette.responses import RedirectResponse from starlette.background import BackgroundTask import httpx import asyncio import time import psutil from database import init_db from services.transcoder import transcode_mgr from routes import streams_router, nodes_router, auth_router from routes.settings_route import router as settings_router from config import settings from services.load_balancer import lb from services.node_store import load_nodes async def monitor_origin(): previous = psutil.net_io_counters() previous_time = time.monotonic() while True: await asyncio.sleep(10) current = psutil.net_io_counters() current_time = time.monotonic() elapsed = current_time - previous_time network_in = ((current.bytes_recv - previous.bytes_recv) * 8 / elapsed / 1e6) if elapsed > 0 else None network_out = ((current.bytes_sent - previous.bytes_sent) * 8 / elapsed / 1e6) if elapsed > 0 else None media_available = False viewer_count = None try: async with httpx.AsyncClient(timeout=3) as client: response = await client.get(f"http://127.0.0.1:{settings.media_origin_api_port}/v3/paths/list") response.raise_for_status() payload = response.json() viewer_count = sum(item.get("readers", 0) for item in payload.get("items", [])) media_available = True except Exception: pass lb.report_health("default", { "cpu_percent": psutil.cpu_percent(), "memory_percent": psutil.virtual_memory().percent, "bandwidth_mbps": network_in + network_out if network_in is not None and network_out is not None else None, "network_in_mbps": network_in, "network_out_mbps": network_out, "active_connections": len(psutil.net_connections(kind="tcp")), "hls_readers": viewer_count, "viewer_count": viewer_count, "frame_drop_indicators": None, "packet_loss_indicators": None, "media_metrics_available": False, }) previous = current previous_time = current_time @asynccontextmanager async def lifespan(app: FastAPI): load_nodes() await init_db() origin_monitor = asyncio.create_task(monitor_origin()) yield origin_monitor.cancel() transcode_mgr.stop_all() app = FastAPI(title="ProgramCore", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(streams_router) app.include_router(nodes_router) app.include_router(auth_router) app.include_router(settings_router) @app.get("/api/health") async def health(): from services.multicast_detector import multicast_detector return { "status": "ok", "multicast_available": multicast_detector.is_available(), } @app.get("/hls/{path:path}") async def serve_hls(path: str): hls_root = "/tmp/hls" fs_path = os.path.join(hls_root, path) if os.path.isfile(fs_path): return FileResponse(fs_path) target = f"http://127.0.0.1:{settings.media_origin_hls_port}/{path}" try: async with httpx.AsyncClient(timeout=httpx.Timeout(10)) as client: req = client.build_request("GET", target) resp = await client.send(req, stream=True) from starlette.responses import StreamingResponse return StreamingResponse( resp.aiter_bytes(), status_code=resp.status_code, headers=dict(resp.headers), media_type=resp.headers.get("content-type"), ) except Exception: raise HTTPException(status_code=404)