thay doi tinh nang chay trong mang noi bo
This commit is contained in:
@@ -26,3 +26,6 @@ dist-ssr
|
|||||||
api/.venv/
|
api/.venv/
|
||||||
api/app/__pycache__/
|
api/app/__pycache__/
|
||||||
api/data
|
api/data
|
||||||
|
|
||||||
|
# recordings
|
||||||
|
mediamtx/recordings
|
||||||
+72
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user