This commit is contained in:
2026-05-30 00:08:59 +05:00
commit d3afd698b6
52 changed files with 8150 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
"""Точка входа NVR: FastAPI + супервизор записи + индексатор + retention."""
from __future__ import annotations
import asyncio
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from .config import load_config
from .db import Database
from .recorder.indexer import Indexer
from .recorder.supervisor import Supervisor
from .services.retention import Retention
from .runtime import runtime
from . import auth, prefs
from .api import cameras, recordings, system, ws
from .api.ws import ConnectionManager
from .services import go2rtc
logging.basicConfig(
level=os.environ.get("NVR_LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger("nvr")
FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend")
@asynccontextmanager
async def lifespan(app: FastAPI):
runtime.config = load_config()
runtime.db = Database()
await runtime.db.connect()
runtime.ws = ConnectionManager()
runtime.ws.bind_loop(asyncio.get_running_loop())
def on_status(status) -> None:
runtime.ws.broadcast_threadsafe({"type": "camera", "camera": status.to_dict()})
runtime.supervisor = Supervisor(runtime.config, on_status=on_status)
runtime.indexer = Indexer(runtime.config, runtime.db)
runtime.retention = Retention(runtime.config, runtime.db)
os.makedirs(runtime.config.storage.path, exist_ok=True)
await go2rtc.sync_all(runtime.config) # потоки (вкл. <cam>_main) в go2rtc для рестрима
runtime.supervisor.start_all()
runtime.indexer.start()
runtime.retention.start()
log.info("NVR запущен: камер=%d, хранилище=%s",
len(runtime.config.cameras), runtime.config.storage.path)
try:
yield
finally:
log.info("остановка NVR…")
await runtime.indexer.stop()
await runtime.retention.stop()
await runtime.supervisor.stop_all()
await runtime.db.close()
app = FastAPI(title="CoRE.Vision", version="1.0", lifespan=lifespan)
# публичные пути (без авторизации)
PUBLIC_PATHS = {"/login", "/api/login", "/api/health"}
@app.middleware("http")
async def cross_origin_isolation(request: Request, call_next):
# COOP/COEP включают crossOriginIsolated → доступен SharedArrayBuffer,
# который нужен многопоточному WASM-декодеру H.265 (свой плеер jessibuca).
response = await call_next(request)
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
return response
@app.middleware("http")
async def require_auth(request: Request, call_next):
path = request.url.path
if path in PUBLIC_PATHS or path.startswith("/static/"):
return await call_next(request)
if auth.auth_manager.validate(request.cookies.get(auth.COOKIE_NAME)):
return await call_next(request)
if path.startswith("/api/"):
return JSONResponse({"detail": "unauthorized"}, status_code=401)
return RedirectResponse("/login")
app.include_router(auth.router)
app.include_router(prefs.router)
app.include_router(cameras.router)
app.include_router(recordings.router)
app.include_router(system.router)
app.include_router(ws.router)
# ── фронтенд ────────────────────────────────────────────────────
@app.get("/")
async def index() -> FileResponse:
return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))
@app.get("/archive")
async def archive() -> FileResponse:
return FileResponse(os.path.join(FRONTEND_DIR, "archive.html"))
@app.get("/settings")
async def settings() -> FileResponse:
return FileResponse(os.path.join(FRONTEND_DIR, "settings.html"))
@app.get("/cams")
async def cams() -> FileResponse:
return FileResponse(os.path.join(FRONTEND_DIR, "cams.html"))
@app.get("/layout")
async def layout() -> RedirectResponse:
# раскладка переехала во вкладку «Настройки → Раскладка»
return RedirectResponse("/settings")
@app.get("/login")
async def login_page() -> FileResponse:
return FileResponse(os.path.join(FRONTEND_DIR, "login.html"))
if os.path.isdir(os.path.join(FRONTEND_DIR, "static")):
app.mount("/static", StaticFiles(directory=os.path.join(FRONTEND_DIR, "static")), name="static")