thay doi tinh nang chay trong mang noi bo

This commit is contained in:
2026-04-27 22:05:18 +07:00
parent a89e145497
commit 0de7d67511
8 changed files with 146 additions and 41 deletions
+3
View File
@@ -26,3 +26,6 @@ dist-ssr
api/.venv/ api/.venv/
api/app/__pycache__/ api/app/__pycache__/
api/data api/data
# recordings
mediamtx/recordings
+72
View File
@@ -2,6 +2,21 @@
Tài liệu này hướng dẫn triển khai trên Orange Pi (Linux). Mục tiêu: MediaMTX chạy như media server, FastAPI chạy như dashboard backend, React build ra static. Tài liệu này hướng dẫn triển khai trên Orange Pi (Linux). Mục tiêu: MediaMTX chạy như media server, FastAPI chạy như dashboard backend, React build ra static.
## 0) Mô hình triển khai
### A) 1 thiết bị (đơn giản nhất)
- MediaMTX + Dashboard backend + Dashboard frontend chạy cùng máy
### B) 2 thiết bị (cùng LAN / cùng lớp mạng)
- Máy A: chạy MediaMTX + lưu recordings
- Máy B: chạy Dashboard backend + frontend
- Máy B sẽ gọi:
- MediaMTX API của máy A (port 9997) để add/remove camera + bật/tắt recording
- WebRTC/WHEP của máy A (port 8889 + UDP 8189) để xem live
- Playback: máy B cần đọc được recordings của máy A (khuyến nghị mount NFS/SMB)
## 1) Yêu cầu ## 1) Yêu cầu
- Orange Pi chạy Debian/Ubuntu - Orange Pi chạy Debian/Ubuntu
@@ -19,6 +34,7 @@ apiAddress: :9997
webrtc: yes webrtc: yes
webrtcAddress: :8889 webrtcAddress: :8889
webrtcAllowOrigin: '*'
record: yes record: yes
recordFormat: fmp4 recordFormat: fmp4
@@ -26,6 +42,13 @@ recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S-%f
recordPartDuration: 5m recordPartDuration: 5m
``` ```
Nếu chạy mô hình 2 thiết bị, đảm bảo MediaMTX trả về đúng IP LAN để client kết nối ICE:
```yaml
webrtcAdditionalHosts:
- 192.168.88.10
```
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:
@@ -42,6 +65,33 @@ Mở firewall/cổng (tuỳ hệ thống):
- `9997/tcp` MediaMTX Control API - `9997/tcp` MediaMTX Control API
- `8189/udp` ICE - `8189/udp` ICE
## 2.1) Playback khi tách 2 thiết bị (mount recordings)
Vì MediaMTX ghi file recordings trên máy A, nên Dashboard (máy B) cần truy cập được folder này để:
- list file `/api/recordings`
- serve file `/videos/...`
Khuyến nghị dùng NFS (Linux-Linux):
Máy A:
```bash
sudo apt-get update
sudo apt-get install -y nfs-kernel-server
echo "/recordings 192.168.88.0/24(rw,sync,no_subtree_check)" | sudo tee -a /etc/exports
sudo exportfs -ra
```
Máy B:
```bash
sudo apt-get update
sudo apt-get install -y nfs-common
sudo mkdir -p /recordings
sudo mount -t nfs 192.168.88.10:/recordings /recordings
```
## 3) Backend FastAPI ## 3) Backend FastAPI
### 3.1 Cài dependencies ### 3.1 Cài dependencies
@@ -63,6 +113,26 @@ Bạn có thể chỉnh:
- `mediamtx_webrtc_url` (mặc định `http://127.0.0.1:8889`) - `mediamtx_webrtc_url` (mặc định `http://127.0.0.1:8889`)
- `recordings_dir` (mặc định `/recordings`) - `recordings_dir` (mặc định `/recordings`)
Nếu chạy mô hình 2 thiết bị, set theo IP máy A (MediaMTX), ví dụ:
```json
{
"mediamtx_api_url": "http://192.168.88.10:9997",
"mediamtx_webrtc_url": "http://192.168.88.10:8889",
"recordings_dir": "/recordings",
"cameras": [],
"schedule": { "enabled": true, "weekdays_from": "18:00", "weekdays_to": "08:00", "weekend_all_day": true }
}
```
Hoặc set ENV trước lần chạy đầu tiên để tạo config mặc định:
```bash
export MEDIAMTX_API_URL="http://192.168.88.10:9997"
export MEDIAMTX_WEBRTC_URL="http://192.168.88.10:8889"
export RECORDINGS_DIR="/recordings"
```
Nếu MediaMTX API có auth, export biến môi trường: Nếu MediaMTX API có auth, export biến môi trường:
```bash ```bash
@@ -120,6 +190,8 @@ server {
} }
``` ```
Nếu chạy mô hình 2 thiết bị, phần Nginx ở máy B không cần proxy tới MediaMTX. Frontend sẽ gọi trực tiếp `mediamtx_webrtc_url` (máy A).
## 5) Systemd service (khuyến nghị) ## 5) Systemd service (khuyến nghị)
### 5.1 Backend service ### 5.1 Backend service
+14
View File
@@ -55,6 +55,20 @@ Backend tự tạo file cấu hình tại `api/data/config.json` khi chạy lầ
Có thể override WebRTC base URL ở frontend bằng biến môi trường `VITE_MEDIAMTX_WEBRTC_URL`. Có thể override WebRTC base URL ở frontend bằng biến môi trường `VITE_MEDIAMTX_WEBRTC_URL`.
Có thể set giá trị mặc định khi chạy lần đầu bằng ENV (backend):
- `MEDIAMTX_API_URL` (ví dụ `http://192.168.88.10:9997`)
- `MEDIAMTX_WEBRTC_URL` (ví dụ `http://192.168.88.10:8889`)
- `RECORDINGS_DIR` (ví dụ `/recordings` hoặc đường dẫn mount NFS/SMB)
## Chạy tách 2 thiết bị (cùng LAN)
- Máy A (MediaMTX + ổ lưu recordings): chạy MediaMTX, mở cổng `9997/tcp`, `8889/tcp`, `8189/udp`, `8554/tcp`
- Máy B (Dashboard backend + frontend): chạy FastAPI + serve web UI
- Playback: máy B cần đọc được thư mục recordings của máy A (khuyến nghị mount NFS/SMB về `RECORDINGS_DIR`)
Chi tiết xem `INSTALL.md`.
## Triển khai ## Triển khai
Xem hướng dẫn chi tiết trong `INSTALL.md`. Xem hướng dẫn chi tiết trong `INSTALL.md`.
+2 -2
View File
@@ -69,8 +69,8 @@ async def _scheduler_loop() -> None:
async def _startup() -> None: async def _startup() -> None:
cfg = await store.load() cfg = await store.load()
recordings_dir = cfg.recordings_dir recordings_dir = cfg.recordings_dir
if Path(recordings_dir).exists(): Path(recordings_dir).mkdir(parents=True, exist_ok=True)
app.mount("/videos", StaticFiles(directory=recordings_dir), name="videos") app.mount("/videos", StaticFiles(directory=recordings_dir), name="videos")
asyncio.create_task(_scheduler_loop()) asyncio.create_task(_scheduler_loop())
+9 -3
View File
@@ -1,3 +1,5 @@
import os
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -14,9 +16,13 @@ class Schedule(BaseModel):
class AppConfig(BaseModel): class AppConfig(BaseModel):
mediamtx_api_url: str = "http://127.0.0.1:9997" mediamtx_api_url: str = Field(
mediamtx_webrtc_url: str = "http://127.0.0.1:8889" default_factory=lambda: os.getenv("MEDIAMTX_API_URL", "http://127.0.0.1:9997")
recordings_dir: str = "/recordings" )
mediamtx_webrtc_url: str = Field(
default_factory=lambda: os.getenv("MEDIAMTX_WEBRTC_URL", "http://127.0.0.1:8889")
)
recordings_dir: str = Field(default_factory=lambda: os.getenv("RECORDINGS_DIR", "/recordings"))
cameras: list[Camera] = Field(default_factory=list) cameras: list[Camera] = Field(default_factory=list)
schedule: Schedule = Field(default_factory=Schedule) schedule: Schedule = Field(default_factory=Schedule)
+1
View File
@@ -30,6 +30,7 @@ hlsPartDuration: 200ms
############################################ ############################################
webrtc: true webrtc: true
webrtcAddress: :8889 webrtcAddress: :8889
webrtcAllowOrigin: '*'
webrtcLocalUDPAddress: :8189 webrtcLocalUDPAddress: :8189
webrtcAdditionalHosts: webrtcAdditionalHosts:
- 192.168.88.3 - 192.168.88.3
+7 -1
View File
@@ -147,7 +147,13 @@ export function useWhepPlayer({
const answerSdp = await res.text(); const answerSdp = await res.text();
const location = res.headers.get("location") || res.headers.get("Location"); const location = res.headers.get("location") || res.headers.get("Location");
if (location) sessionUrlRef.current = location; if (location) {
try {
sessionUrlRef.current = new URL(location, whepUrl).toString();
} catch {
sessionUrlRef.current = location;
}
}
await pc.setRemoteDescription({ type: "answer", sdp: answerSdp }); await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
if (!disposed) setStatus("playing"); if (!disposed) setStatus("playing");
+37 -34
View File
@@ -1,40 +1,43 @@
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { traeBadgePlugin } from 'vite-plugin-trae-solo-badge'; import { traeBadgePlugin } from 'vite-plugin-trae-solo-badge';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
server: { const env = loadEnv(mode, process.cwd(), "");
proxy: { const backend = env.VITE_DEV_BACKEND_URL || "http://localhost:8000";
"/api": "http://localhost:8000",
"/videos": "http://localhost:8000", return {
}, server: {
}, proxy: {
build: { "/api": backend,
sourcemap: 'hidden', "/videos": backend,
},
plugins: [
react({
babel: {
plugins: [
'react-dev-locator',
],
}, },
}), },
traeBadgePlugin({ build: {
variant: 'dark', sourcemap: "hidden",
position: 'bottom-right', },
prodOnly: true, plugins: [
clickable: true, react({
clickUrl: 'https://www.trae.ai/solo?showJoin=1', babel: {
autoTheme: true, plugins: ["react-dev-locator"],
autoThemeTarget: '#root' },
}), }),
tsconfigPaths() traeBadgePlugin({
], variant: "dark",
test: { position: "bottom-right",
environment: "jsdom", prodOnly: true,
setupFiles: ["./src/test/setup.ts"], clickable: true,
}, clickUrl: "https://www.trae.ai/solo?showJoin=1",
}) autoTheme: true,
autoThemeTarget: "#root",
}),
tsconfigPaths(),
],
test: {
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
},
};
});