thay dỏi cong
This commit is contained in:
+29
-6
@@ -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>/`
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+32
-4
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user