thay dỏi cong

This commit is contained in:
2026-04-27 22:13:43 +07:00
parent 0de7d67511
commit f6c5dce452
6 changed files with 66 additions and 15 deletions
+29 -6
View File
@@ -49,6 +49,29 @@ webrtcAdditionalHosts:
- 192.168.88.10 - 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). Trong `paths`, bạn có thể để dashboard tự thêm path bằng API (Settings → Add Camera).
Tạo thư mục recordings: Tạo thư mục recordings:
@@ -144,7 +167,7 @@ export MEDIAMTX_API_PASS="..."
```bash ```bash
source api/.venv/bin/activate 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) ## 4) Frontend (build static)
@@ -163,7 +186,7 @@ Output nằm ở `dist/`.
Có 2 cách phổ biến: 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ự 2) Dùng Caddy tương tự
Ví dụ Nginx server block tối thiểu: Ví dụ Nginx server block tối thiểu:
@@ -181,11 +204,11 @@ server {
} }
location /api/ { location /api/ {
proxy_pass http://127.0.0.1:8000; proxy_pass http://127.0.0.1:8008;
} }
location /videos/ { 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 WorkingDirectory=/opt/ipcam-dashboard
Environment=MEDIAMTX_API_USER= Environment=MEDIAMTX_API_USER=
Environment=MEDIAMTX_API_PASS= 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 Restart=always
RestartSec=2 RestartSec=2
@@ -225,7 +248,7 @@ sudo systemctl status ipcam-dashboard
## 6) Kiểm tra nhanh ## 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` - MediaMTX API: `curl http://localhost:9997/v3/paths/list`
- Frontend: mở `http://<orange-pi-ip>/` - Frontend: mở `http://<orange-pi-ip>/`
+1 -1
View File
@@ -156,7 +156,7 @@ async def toggle_recording(data: RecordingToggle):
## 8. Run Server ## 8. Run Server
``` ```
uvicorn main:app --host 0.0.0.0 --port 8000 uvicorn main:app --host 0.0.0.0 --port 8008
``` ```
--- ---
+1 -1
View File
@@ -154,7 +154,7 @@ Use standard HTML:
```html ```html
<video controls width="600"> <video controls width="600">
<source src="http://<server>:8000/videos/cam1/file.fmp4"> <source src="http://<server>:8008/videos/cam1/file.fmp4">
</video> </video>
``` ```
+2 -2
View File
@@ -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 python3 -m venv api/.venv
source api/.venv/bin/activate source api/.venv/bin/activate
pip install -r api/requirements.txt 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 ### 2) Chạy frontend
@@ -41,7 +41,7 @@ npm install
npm run dev npm run dev
``` ```
Frontend dev server đã được cấu hình proxy `/api``/videos` sang `http://localhost:8000`. Frontend dev server đã được cấu hình proxy `/api``/videos` sang `http://localhost:8008`.
## Cấu hình ## Cấu hình
+32 -4
View File
@@ -2,9 +2,11 @@ from __future__ import annotations
import asyncio import asyncio
import os import os
import logging
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import httpx
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -26,6 +28,8 @@ def _cors_origins() -> list[str]:
raw = os.getenv("CORS_ORIGINS", "http://localhost:5173") raw = os.getenv("CORS_ORIGINS", "http://localhost:5173")
return [x.strip() for x in raw.split(",") if x.strip()] return [x.strip() for x in raw.split(",") if x.strip()]
logger = logging.getLogger("ipcam_dashboard")
app = FastAPI(title="IPCam Dashboard API") app = FastAPI(title="IPCam Dashboard API")
app.add_middleware( app.add_middleware(
@@ -61,9 +65,21 @@ async def _scheduler_loop() -> None:
try: try:
cfg = await store.load() cfg = await store.load()
await scheduler.tick(cfg.schedule) await scheduler.tick(cfg.schedule)
except Exception:
logger.exception("scheduler_tick_failed")
finally: finally:
await asyncio.sleep(60) 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") @app.on_event("startup")
async def _startup() -> None: async def _startup() -> None:
@@ -98,7 +114,10 @@ async def add_camera(camera: Camera) -> AppConfig:
username=os.getenv("MEDIAMTX_API_USER"), username=os.getenv("MEDIAMTX_API_USER"),
password=os.getenv("MEDIAMTX_API_PASS"), 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 return cfg
@@ -116,7 +135,10 @@ async def delete_camera(name: str) -> AppConfig:
username=os.getenv("MEDIAMTX_API_USER"), username=os.getenv("MEDIAMTX_API_USER"),
password=os.getenv("MEDIAMTX_API_PASS"), 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 return cfg
@@ -128,12 +150,18 @@ async def list_paths() -> dict:
username=os.getenv("MEDIAMTX_API_USER"), username=os.getenv("MEDIAMTX_API_USER"),
password=os.getenv("MEDIAMTX_API_PASS"), 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") @app.post("/api/recording")
async def toggle_recording(data: RecordingToggle) -> dict: 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} return {"enabled": data.enabled}
+1 -1
View File
@@ -6,7 +6,7 @@ import { traeBadgePlugin } from 'vite-plugin-trae-solo-badge';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), ""); 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 { return {
server: { server: {