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
```
### 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://<orange-pi-ip>/`
+1 -1
View File
@@ -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
```
---
+1 -1
View File
@@ -154,7 +154,7 @@ Use standard HTML:
```html
<video controls width="600">
<source src="http://<server>:8000/videos/cam1/file.fmp4">
<source src="http://<server>:8008/videos/cam1/file.fmp4">
</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
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``/videos` sang `http://localhost:8000`.
Frontend dev server đã được cấu hình proxy `/api``/videos` sang `http://localhost:8008`.
## Cấu hình
+32 -4
View File
@@ -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}
+1 -1
View File
@@ -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: {