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