diff --git a/INSTALL.md b/INSTALL.md index 4b2e9e9..f26577d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -49,6 +49,29 @@ webrtcAdditionalHosts: - 192.168.88.10 ``` +### 2.1) Nếu MediaMTX API bị 401 Unauthorized + +401 nghĩa là request thiếu thông tin xác thực hợp lệ. Khi bật auth trong MediaMTX, bạn cần tạo user có quyền `api` và cấu hình backend dashboard gửi Basic Auth. + +Ví dụ trong `mediamtx.yml`: + +```yaml +authMethod: internal +authInternalUsers: + - user: dashboard + pass: dashboard_password + ips: ['127.0.0.1', '::1', '192.168.88.0/24'] + permissions: + - action: api +``` + +Sau đó trên máy chạy dashboard backend: + +```bash +export MEDIAMTX_API_USER="dashboard" +export MEDIAMTX_API_PASS="dashboard_password" +``` + Trong `paths`, bạn có thể để dashboard tự thêm path bằng API (Settings → Add Camera). Tạo thư mục recordings: @@ -144,7 +167,7 @@ export MEDIAMTX_API_PASS="..." ```bash source api/.venv/bin/activate -uvicorn api.app.main:app --host 0.0.0.0 --port 8000 +uvicorn api.app.main:app --host 0.0.0.0 --port 8008 ``` ## 4) Frontend (build static) @@ -163,7 +186,7 @@ Output nằm ở `dist/`. Có 2 cách phổ biến: -1) Nginx serve `dist/` và reverse proxy `/api` + `/videos` về backend `:8000` +1) Nginx serve `dist/` và reverse proxy `/api` + `/videos` về backend `:8008` 2) Dùng Caddy tương tự Ví dụ Nginx server block tối thiểu: @@ -181,11 +204,11 @@ server { } location /api/ { - proxy_pass http://127.0.0.1:8000; + proxy_pass http://127.0.0.1:8008; } location /videos/ { - proxy_pass http://127.0.0.1:8000; + proxy_pass http://127.0.0.1:8008; } } ``` @@ -207,7 +230,7 @@ After=network.target WorkingDirectory=/opt/ipcam-dashboard Environment=MEDIAMTX_API_USER= Environment=MEDIAMTX_API_PASS= -ExecStart=/opt/ipcam-dashboard/api/.venv/bin/uvicorn api.app.main:app --host 0.0.0.0 --port 8000 +ExecStart=/opt/ipcam-dashboard/api/.venv/bin/uvicorn api.app.main:app --host 0.0.0.0 --port 8008 Restart=always RestartSec=2 @@ -225,7 +248,7 @@ sudo systemctl status ipcam-dashboard ## 6) Kiểm tra nhanh -- Backend: `curl http://localhost:8000/api/health` +- Backend: `curl http://localhost:8008/api/health` - MediaMTX API: `curl http://localhost:9997/v3/paths/list` - Frontend: mở `http:///` diff --git a/IPCam_Backend_FastAPI.md b/IPCam_Backend_FastAPI.md index 6698f82..9cc7ce5 100644 --- a/IPCam_Backend_FastAPI.md +++ b/IPCam_Backend_FastAPI.md @@ -156,7 +156,7 @@ async def toggle_recording(data: RecordingToggle): ## 8. Run Server ``` -uvicorn main:app --host 0.0.0.0 --port 8000 +uvicorn main:app --host 0.0.0.0 --port 8008 ``` --- diff --git a/IPCam_Recordings_API.md b/IPCam_Recordings_API.md index c923e76..073ae1b 100644 --- a/IPCam_Recordings_API.md +++ b/IPCam_Recordings_API.md @@ -154,7 +154,7 @@ Use standard HTML: ```html ``` diff --git a/README.md b/README.md index ed8ca64..46cadd9 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Dashboard giám sát camera IP gọn nhẹ chạy trên Orange Pi, dựa trên M python3 -m venv api/.venv source api/.venv/bin/activate pip install -r api/requirements.txt -uvicorn api.app.main:app --host 0.0.0.0 --port 8000 +uvicorn api.app.main:app --host 0.0.0.0 --port 8008 ``` ### 2) Chạy frontend @@ -41,7 +41,7 @@ npm install npm run dev ``` -Frontend dev server đã được cấu hình proxy `/api` và `/videos` sang `http://localhost:8000`. +Frontend dev server đã được cấu hình proxy `/api` và `/videos` sang `http://localhost:8008`. ## Cấu hình diff --git a/api/app/main.py b/api/app/main.py index 41a1f19..43781f4 100644 --- a/api/app/main.py +++ b/api/app/main.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio import os +import logging from pathlib import Path from typing import Optional +import httpx from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles @@ -26,6 +28,8 @@ def _cors_origins() -> list[str]: raw = os.getenv("CORS_ORIGINS", "http://localhost:5173") return [x.strip() for x in raw.split(",") if x.strip()] +logger = logging.getLogger("ipcam_dashboard") + app = FastAPI(title="IPCam Dashboard API") app.add_middleware( @@ -61,9 +65,21 @@ async def _scheduler_loop() -> None: try: cfg = await store.load() await scheduler.tick(cfg.schedule) + except Exception: + logger.exception("scheduler_tick_failed") finally: await asyncio.sleep(60) +def _raise_mediamtx_http_error(err: httpx.HTTPError) -> None: + if isinstance(err, httpx.HTTPStatusError): + code = err.response.status_code + if code == 401: + raise HTTPException(status_code=502, detail="mediamtx_unauthorized") + if code == 403: + raise HTTPException(status_code=502, detail="mediamtx_forbidden") + raise HTTPException(status_code=502, detail=f"mediamtx_http_{code}") + raise HTTPException(status_code=502, detail="mediamtx_unreachable") + @app.on_event("startup") async def _startup() -> None: @@ -98,7 +114,10 @@ async def add_camera(camera: Camera) -> AppConfig: username=os.getenv("MEDIAMTX_API_USER"), password=os.getenv("MEDIAMTX_API_PASS"), ) - await client.upsert_paths_sources_bulk({camera.name: camera.rtsp_url}) + try: + await client.upsert_paths_sources_bulk({camera.name: camera.rtsp_url}) + except httpx.HTTPError as e: + _raise_mediamtx_http_error(e) return cfg @@ -116,7 +135,10 @@ async def delete_camera(name: str) -> AppConfig: username=os.getenv("MEDIAMTX_API_USER"), password=os.getenv("MEDIAMTX_API_PASS"), ) - await client.delete_path(name) + try: + await client.delete_path(name) + except httpx.HTTPError as e: + _raise_mediamtx_http_error(e) return cfg @@ -128,12 +150,18 @@ async def list_paths() -> dict: username=os.getenv("MEDIAMTX_API_USER"), password=os.getenv("MEDIAMTX_API_PASS"), ) - return await client.list_paths_status() + try: + return await client.list_paths_status() + except httpx.HTTPError as e: + _raise_mediamtx_http_error(e) @app.post("/api/recording") async def toggle_recording(data: RecordingToggle) -> dict: - await _apply_recording(data.enabled) + try: + await _apply_recording(data.enabled) + except httpx.HTTPError as e: + _raise_mediamtx_http_error(e) return {"enabled": data.enabled} diff --git a/vite.config.ts b/vite.config.ts index 5a5ed75..069bc6e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,7 @@ import { traeBadgePlugin } from 'vite-plugin-trae-solo-badge'; // https://vite.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); - const backend = env.VITE_DEV_BACKEND_URL || "http://localhost:8000"; + const backend = env.VITE_DEV_BACKEND_URL || "http://localhost:8008"; return { server: {