解耦前的最后一个准备,可恢复版本
This commit is contained in:
parent
19a37a3de3
commit
0a5a879418
@ -236,6 +236,38 @@ Response 200:`{"ok":true}`
|
||||
|
||||
失败:500 + `{"error":"..."}`
|
||||
|
||||
### 5.3 `PUT /v1/media-server/configs/{name}`
|
||||
用途:上传 media-server 配置文件到 `agent.media_server_process.configs_dir`。
|
||||
|
||||
**Auth**:必须(401)
|
||||
|
||||
Headers:
|
||||
- `Content-Type: application/json`
|
||||
- `X-RK-Token: ...`
|
||||
|
||||
Path params:
|
||||
- `name`: string(仅允许 `[A-Za-z0-9._-]`,禁止 `/`、`\\`、`..`;若无 `.json` 后缀则自动追加)
|
||||
|
||||
Body:media-server 配置 JSON
|
||||
|
||||
Response 200:
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"name": "cam1.json",
|
||||
"path": "/opt/rk3588sys/configs/cam1.json",
|
||||
"size": 1234,
|
||||
"mtime_ms": 1730000000000
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
- 400:name 非法 / Content-Type 非 application/json / JSON 无效 / 空 body
|
||||
- 401:unauthorized
|
||||
- 413:超过 `max_upload_mb`
|
||||
- 501:`configs_dir` 未配置
|
||||
- 500:写盘失败
|
||||
|
||||
## 6. 主程序进程控制(agent 对外)
|
||||
|
||||
> 说明:该能力用于“启动/重启/关闭主程序(media-server)并选择加载哪个配置文件”。
|
||||
|
||||
145
PRD_06_Hardware_Decoupling_Plan.md
Normal file
145
PRD_06_Hardware_Decoupling_Plan.md
Normal file
@ -0,0 +1,145 @@
|
||||
# PRD_06 多硬件解耦计划(RK3588/Atlas/Jetson)
|
||||
|
||||
## 1. 背景与目标
|
||||
当前工程深度绑定 RK3588(RKNN/RGA/MPP/DMA-BUF),限制了 Atlas、Jetson 等平台的接入。目标是在**不破坏现有功能**的前提下,通过接口解耦与默认实现保留现有行为,实现多硬件可插拔支持。
|
||||
|
||||
### 目标
|
||||
- 以接口层抽象 **推理、图像处理、编解码、缓冲区** 四个核心模块。
|
||||
- 默认实现保持 RK3588 行为与性能路径(零拷贝/DMA-BUF)。
|
||||
- 逐步迁移现有节点,避免一次性大改。
|
||||
|
||||
### 非目标
|
||||
- 不改变业务逻辑(检测/识别流程、RTSP/HLS 业务)。
|
||||
- 不在本阶段引入新 UI/协议或跨平台发布流程。
|
||||
|
||||
## 2. 现状摘要(关键路径)
|
||||
- 推理:`include/ai_scheduler.h`, `src/ai_scheduler.cpp`(RKNN + DMA-BUF 输入)
|
||||
- 图像处理:`plugins/preprocess/preprocess_node.cpp`(RGA 或 swscale)
|
||||
- 编解码:`plugins/input_rtsp/*`, `plugins/input_file/*`, `plugins/publish/*`(MPP + FFmpeg 混用)
|
||||
- 缓冲:`include/frame/frame.h`(dma_fd/planes/data_owner)
|
||||
|
||||
## 3. 总体方案
|
||||
引入四类接口与默认实现:
|
||||
- **IInferBackend**:推理后端抽象(默认 RKNN)
|
||||
- **IImageProcessor**:图像预处理抽象(默认 RGA + swscale 兜底)
|
||||
- **IDecoder / IEncoder**:编解码抽象(默认 MPP,必要时 FFmpeg)
|
||||
- **FrameBuffer**:统一缓冲区语义与同步
|
||||
|
||||
所有接口通过工厂/配置注入,保持现有 JSON 配置兼容。
|
||||
|
||||
## 4. 实施步骤、里程碑与单元测试
|
||||
|
||||
### Step 1:建立基础抽象与工厂
|
||||
**实施内容**
|
||||
- 新建 `include/hw/` 下接口定义:`i_infer_backend.h`, `i_image_processor.h`, `i_decoder.h`, `i_encoder.h`, `frame_buffer.h`
|
||||
- 定义最小能力集:
|
||||
- IInferBackend: `LoadModel`, `Infer`, `InferBorrowed`
|
||||
- IImageProcessor: `Resize`, `CvtColor`, `Normalize`
|
||||
- IDecoder/IEncoder: `Open`, `Send`, `Receive`, `Close`
|
||||
- FrameBuffer: `Planes()`, `DmaFd()`, `SyncStart/End()`
|
||||
- 新建工厂:`hw_factory.h/cpp`,根据配置返回默认实现
|
||||
|
||||
**关键里程碑**
|
||||
- 接口头文件编译通过;工程无行为变化
|
||||
- 工厂默认返回 RK3588 实现(空实现也可先用占位)
|
||||
|
||||
**单元测试(GTest)**
|
||||
- `HwFactory_Defaults_ReturnsRk3588Impls`
|
||||
- `FrameBuffer_Metadata_Preserved`(dma_fd/planes 赋值一致性)
|
||||
|
||||
---
|
||||
|
||||
### Step 2:推理模块解耦(RKNN → IInferBackend)
|
||||
**实施内容**
|
||||
- 将 `AiScheduler` 包装为 `RknnInferBackend` 实现
|
||||
- `ai_*` 节点依赖 `IInferBackend` 接口注入(保留默认行为)
|
||||
- 保留 `InferBorrowed` 以支持零拷贝输入
|
||||
|
||||
**关键里程碑**
|
||||
- 现有模型推理链路无回归,性能基准一致(同配置)
|
||||
- RKNN 仍可多上下文并发
|
||||
|
||||
**单元测试(GTest)**
|
||||
- `InferBackend_LoadModel_Smoke`(加载模型返回成功)
|
||||
- `InferBackend_BorrowedInput_UsesDmaFd`(检查传入 dma_fd 路径被调用)
|
||||
|
||||
---
|
||||
|
||||
### Step 3:图像预处理解耦(RGA/CPU → IImageProcessor)
|
||||
**实施内容**
|
||||
- 抽取 RGA 路径为 `RgaImageProcessor`
|
||||
- 抽取 swscale 路径为 `SwscaleImageProcessor`
|
||||
- `preprocess_node` 仅面向接口调用
|
||||
|
||||
**关键里程碑**
|
||||
- `use_rga=true/false` 行为完全一致
|
||||
- RGA 限流逻辑(RgaGate)保留
|
||||
|
||||
**单元测试(GTest)**
|
||||
- `ImageProcessor_RgaVsSwscale_OutputShape`(输出尺寸一致)
|
||||
- `ImageProcessor_ColorConversion_Nv12ToRgb`(像素格式转换)
|
||||
|
||||
---
|
||||
|
||||
### Step 4:编解码解耦(MPP/FFmpeg → IDecoder/IEncoder)
|
||||
**实施内容**
|
||||
- `MppDecoder`, `FfmpegDecoder` 实现 `IDecoder`
|
||||
- `MppEncoder`, `FfmpegEncoder` 实现 `IEncoder`
|
||||
- `input_rtsp/input_file/publish/storage` 节点仅面向接口
|
||||
|
||||
**关键里程碑**
|
||||
- RTSP 输入与 HLS 输出链路不变
|
||||
- MPP 仍为默认路径,FFmpeg 作为兜底/平台适配
|
||||
|
||||
**单元测试(GTest)**
|
||||
- `Decoder_Open_Close_Smoke`
|
||||
- `Encoder_Open_Close_Smoke`
|
||||
- `Codec_Pipeline_EncodeDecode_OneFrame`(小尺寸样例帧)
|
||||
|
||||
---
|
||||
|
||||
### Step 5:缓冲区抽象(Frame → FrameBuffer)
|
||||
**实施内容**
|
||||
- 新增 `FrameBuffer`,替代直接使用 `Frame` 的 dma_fd/data_owner
|
||||
- `Frame` 保留为业务结构,内部持有 `FrameBuffer`
|
||||
- 统一 DMA 同步接口以便多硬件适配
|
||||
|
||||
**关键里程碑**
|
||||
- DMA-BUF 与内存缓冲区均可通过统一接口访问
|
||||
- `Frame` 兼容旧字段,最小侵入替换完成
|
||||
|
||||
**单元测试(GTest)**
|
||||
- `FrameBuffer_Sync_NoCrash`
|
||||
- `FrameBuffer_PlaneAccess_Consistent`
|
||||
|
||||
---
|
||||
|
||||
### Step 6:多硬件适配接入(Atlas/Jetson)
|
||||
**实施内容**
|
||||
- 新增 `AtlasInferBackend/AtlasImageProcessor/AtlasCodec` 实现(占位/实验性)
|
||||
- 新增 `JetsonInferBackend/JetsonImageProcessor/JetsonCodec` 实现
|
||||
- 通过配置切换平台实现
|
||||
|
||||
**关键里程碑**
|
||||
- 不影响 RK3588 默认路径
|
||||
- 新平台可在单机完成 smoke 测试
|
||||
|
||||
**单元测试(GTest)**
|
||||
- `HwFactory_SelectsBackend_ByConfig`
|
||||
- `PlatformImpls_Smoke_Construct`(构造/释放)
|
||||
|
||||
## 5. 风险与缓解
|
||||
- **接口过宽导致迁移成本增加** → 控制最小接口集,逐步扩展
|
||||
- **性能回退** → 保留 RK3588 默认实现与 DMA-BUF 快路径
|
||||
- **迁移破坏现有节点** → 节点逐个替换,保持旧路径可回退
|
||||
|
||||
## 6. 验证与回滚
|
||||
### 验证命令
|
||||
```
|
||||
scripts/build_host.sh
|
||||
ctest --test-dir build/host --output-on-failure
|
||||
```
|
||||
|
||||
### 回滚策略
|
||||
- 以编译开关/配置切换回旧路径
|
||||
- 保留 RK3588 实现作为默认后端
|
||||
BIN
agent/build/rk3588-agent_linux_arm64
Normal file
BIN
agent/build/rk3588-agent_linux_arm64
Normal file
Binary file not shown.
@ -10,6 +10,7 @@ import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -28,6 +29,7 @@ type Server struct {
|
||||
ms *mediaserver.Client
|
||||
store *modelstore.Store
|
||||
proc *procctl.Controller
|
||||
baseDir string
|
||||
deviceID string
|
||||
hostname string
|
||||
agentPort int
|
||||
@ -60,6 +62,7 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st
|
||||
ms: ms,
|
||||
store: store,
|
||||
proc: pc,
|
||||
baseDir: baseDir,
|
||||
deviceID: deviceID,
|
||||
hostname: sysinfo.Hostname(),
|
||||
agentPort: agentPort,
|
||||
@ -85,6 +88,7 @@ func New(agentCfg config.AgentConfig, baseDir string, ms *mediaserver.Client, st
|
||||
mux.HandleFunc("/v1/media-server/restart", s.handleMediaRestart)
|
||||
mux.HandleFunc("/v1/media-server/stop", s.handleMediaStop)
|
||||
mux.HandleFunc("/v1/media-server/status", s.handleMediaStatus)
|
||||
mux.HandleFunc("/v1/media-server/configs/", s.handleMediaConfigUpload)
|
||||
mux.HandleFunc("/v1/graphs", s.handleGraphs)
|
||||
mux.HandleFunc("/v1/graphs/", s.handleGraphDetail)
|
||||
mux.HandleFunc("/v1/logs/recent", s.handleLogsRecent)
|
||||
@ -205,6 +209,7 @@ func (s *Server) applyRootConfigBytes(ctx context.Context, body []byte) error {
|
||||
}
|
||||
|
||||
var modelNameRE = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||
var configNameRE = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||
|
||||
func (s *Server) handleModelUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
@ -285,6 +290,74 @@ func (s *Server) handleModelsList(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, m)
|
||||
}
|
||||
|
||||
func (s *Server) handleMediaConfigUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
if !s.authorize(r, true) {
|
||||
errorJSON(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
if mt, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")); err != nil || mt != "application/json" {
|
||||
errorJSON(w, http.StatusBadRequest, "validation failed: Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(r.URL.Path, "/v1/media-server/configs/")
|
||||
name = strings.TrimSpace(name)
|
||||
finalName, err := normalizeConfigName(name)
|
||||
if err != nil {
|
||||
errorJSON(w, http.StatusBadRequest, "validation failed: invalid name")
|
||||
return
|
||||
}
|
||||
|
||||
configsDir, err := s.resolveConfigsDir()
|
||||
if err != nil {
|
||||
errorJSON(w, http.StatusNotImplemented, "not supported")
|
||||
return
|
||||
}
|
||||
|
||||
maxBytes := int64(s.agentCfg.MaxUploadMB) * 1024 * 1024
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "request body too large") {
|
||||
errorJSON(w, http.StatusRequestEntityTooLarge, "payload too large")
|
||||
return
|
||||
}
|
||||
errorJSON(w, http.StatusBadRequest, "invalid json: "+err.Error())
|
||||
return
|
||||
}
|
||||
if len(body) == 0 {
|
||||
errorJSON(w, http.StatusBadRequest, "validation failed: empty body")
|
||||
return
|
||||
}
|
||||
var tmp any
|
||||
if err := json.Unmarshal(body, &tmp); err != nil {
|
||||
errorJSON(w, http.StatusBadRequest, "invalid json: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dst := filepath.Join(configsDir, finalName)
|
||||
if err := files.WriteFileAtomic(dst, append(body, '\n'), 0o644); err != nil {
|
||||
errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error())
|
||||
return
|
||||
}
|
||||
st, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
errorJSON(w, http.StatusInternalServerError, "internal error: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"ok": true,
|
||||
"name": finalName,
|
||||
"path": filepath.ToSlash(dst),
|
||||
"size": st.Size(),
|
||||
"mtime_ms": st.ModTime().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMediaReload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
errorJSON(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
@ -447,6 +520,33 @@ func (s *Server) handleMediaStatus(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "running": st.Running, "pid": st.Pid, "config_path": st.ConfigPath})
|
||||
}
|
||||
|
||||
func normalizeConfigName(name string) (string, error) {
|
||||
if name == "" || strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
|
||||
return "", errors.New("invalid name")
|
||||
}
|
||||
if !configNameRE.MatchString(name) {
|
||||
return "", errors.New("invalid name")
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(name), ".json") {
|
||||
name += ".json"
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (s *Server) resolveConfigsDir() (string, error) {
|
||||
base := strings.TrimSpace(s.agentCfg.MediaServerProcess.ConfigsDir)
|
||||
if base == "" {
|
||||
return "", errors.New("configs_dir is empty")
|
||||
}
|
||||
if filepath.IsAbs(base) {
|
||||
return base, nil
|
||||
}
|
||||
if s.baseDir == "" {
|
||||
return filepath.Clean(base), nil
|
||||
}
|
||||
return filepath.Join(s.baseDir, base), nil
|
||||
}
|
||||
|
||||
func readOptionalJSON[T any](w http.ResponseWriter, r *http.Request, maxBytes int64) (T, error) {
|
||||
var zero T
|
||||
if r.Body == nil {
|
||||
|
||||
29
agent/orangepi@10.0.0.81
Normal file
29
agent/orangepi@10.0.0.81
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"agent": {
|
||||
"listen": "0.0.0.0:9100",
|
||||
"token": "CHANGE_ME",
|
||||
"require_token_for_read": false,
|
||||
|
||||
"discovery_enable": true,
|
||||
"discovery_port": 35688,
|
||||
|
||||
"device_name": "cam1_strict_minio_alarm",
|
||||
"device_id_path": "./device_id",
|
||||
|
||||
"models_dir": "./models",
|
||||
"max_upload_mb": 200,
|
||||
|
||||
"config_path": "./configs/test_cam1_strict_minio_alarm_rtsp_server.json",
|
||||
"media_server_process": {
|
||||
"enable": true,
|
||||
"exec_path": "../build/media-server",
|
||||
"work_dir": "..",
|
||||
"configs_dir": "../configs",
|
||||
"pid_file": "./rk3588sys-media-server.pid",
|
||||
"graceful_timeout_ms": 5000
|
||||
},
|
||||
"media_server_base_url": "http://127.0.0.1:9000",
|
||||
"media_server_timeout_ms": 3000,
|
||||
"media_server_retry": { "max_attempts": 3, "backoff_ms": [200, 500] }
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
"queue": { "size": 8, "strategy": "drop_oldest" },
|
||||
"graphs": [
|
||||
{
|
||||
"name": "cam1_native_face_dual_output",
|
||||
"name": "cam1_native_main",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "in_cam1",
|
||||
@ -30,7 +30,7 @@
|
||||
"dst_format": "rgb",
|
||||
"dst_packed": true,
|
||||
"keep_ratio": false,
|
||||
"rga_gate": "cam1_native_face_dual_output",
|
||||
"rga_gate": "cam1_native_main",
|
||||
"use_rga": true
|
||||
},
|
||||
{
|
||||
@ -53,7 +53,7 @@
|
||||
"enable": true,
|
||||
"mode": "bytetrack_lite",
|
||||
"per_class": true,
|
||||
"state_key": "cam1_native_face_dual_output",
|
||||
"state_key": "cam1_native_main",
|
||||
"track_classes": [0],
|
||||
"ignore_classes": [],
|
||||
"allowed_models": ["yolov5", "yolov8"],
|
||||
@ -86,7 +86,7 @@
|
||||
"dst_h": 720,
|
||||
"dst_format": "nv12",
|
||||
"keep_ratio": false,
|
||||
"rga_gate": "cam1_native_face_dual_output",
|
||||
"rga_gate": "cam1_native_main",
|
||||
"use_rga": true
|
||||
},
|
||||
{
|
||||
@ -109,7 +109,7 @@
|
||||
"id": "alarm_cam1",
|
||||
"type": "alarm",
|
||||
"role": "sink",
|
||||
"enable": true,
|
||||
"enable": false,
|
||||
"eval_fps": 10,
|
||||
"labels": [],
|
||||
"rules": [
|
||||
@ -125,7 +125,7 @@
|
||||
"actions": {
|
||||
"log": { "enable": true, "level": "info" },
|
||||
"snapshot": {
|
||||
"enable": true,
|
||||
"enable": false,
|
||||
"format": "jpg",
|
||||
"quality": 85,
|
||||
"upload": {
|
||||
@ -138,7 +138,7 @@
|
||||
}
|
||||
},
|
||||
"clip": {
|
||||
"enable": true,
|
||||
"enable": false,
|
||||
"pre_sec": 5,
|
||||
"post_sec": 10,
|
||||
"format": "mp4",
|
||||
@ -160,19 +160,47 @@
|
||||
"method": "POST"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
["in_cam1", "pre_cam1"],
|
||||
["pre_cam1", "yolo_cam1"],
|
||||
["yolo_cam1", "trk_cam1"],
|
||||
["trk_cam1", "osd_cam1"],
|
||||
["osd_cam1", "post_cam1"],
|
||||
["post_cam1", "pub_cam1"],
|
||||
["pub_cam1", "alarm_cam1"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cam1_face_recog_rtsp",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "pre_face_cam1",
|
||||
"id": "in_cam1",
|
||||
"type": "input_rtsp",
|
||||
"role": "source",
|
||||
"enable": true,
|
||||
"url": "rtsp://10.0.0.5:8554/cam",
|
||||
"fps": 30,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"use_mpp": true,
|
||||
"use_ffmpeg": false,
|
||||
"force_tcp": true,
|
||||
"reconnect_sec": 5,
|
||||
"reconnect_backoff_max_sec": 30
|
||||
},
|
||||
{
|
||||
"id": "pre_cam1",
|
||||
"type": "preprocess",
|
||||
"role": "filter",
|
||||
"enable": true,
|
||||
"dst_w": 0,
|
||||
"dst_h": 0,
|
||||
"dst_w": 1280,
|
||||
"dst_h": 720,
|
||||
"dst_format": "rgb",
|
||||
"dst_packed": true,
|
||||
"keep_ratio": false,
|
||||
"rga_gate": "cam1_native_face_dual_output",
|
||||
"rga_gate": "cam1_face_recog_rtsp",
|
||||
"use_rga": true
|
||||
},
|
||||
{
|
||||
@ -225,11 +253,11 @@
|
||||
"type": "preprocess",
|
||||
"role": "filter",
|
||||
"enable": true,
|
||||
"dst_w": 0,
|
||||
"dst_h": 0,
|
||||
"dst_w": 1280,
|
||||
"dst_h": 720,
|
||||
"dst_format": "nv12",
|
||||
"keep_ratio": false,
|
||||
"rga_gate": "cam1_native_face_dual_output",
|
||||
"rga_gate": "cam1_face_recog_rtsp",
|
||||
"use_rga": true
|
||||
},
|
||||
{
|
||||
@ -251,16 +279,8 @@
|
||||
],
|
||||
"edges": [
|
||||
["in_cam1", "pre_cam1"],
|
||||
["pre_cam1", "yolo_cam1"],
|
||||
["yolo_cam1", "trk_cam1"],
|
||||
["trk_cam1", "osd_cam1"],
|
||||
["osd_cam1", "post_cam1"],
|
||||
["post_cam1", "pub_cam1"],
|
||||
["pub_cam1", "alarm_cam1"],
|
||||
|
||||
["in_cam1", "pre_face_cam1", {"queue": {"size": 1, "strategy": "drop_oldest"}}],
|
||||
["pre_face_cam1", "face_det_cam1", {"queue": {"size": 1, "strategy": "drop_oldest"}}],
|
||||
["face_det_cam1", "face_recog_cam1", {"queue": {"size": 1, "strategy": "drop_oldest"}}],
|
||||
["pre_cam1", "face_det_cam1"],
|
||||
["face_det_cam1", "face_recog_cam1"],
|
||||
["face_recog_cam1", "osd_face_cam1"],
|
||||
["osd_face_cam1", "post_face_cam1"],
|
||||
["post_face_cam1", "pub_face_cam1"]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "utils/simple_json.h"
|
||||
|
||||
@ -185,12 +186,19 @@ inline bool ValidateExpandedRootConfig(const SimpleJson& root, std::string& err)
|
||||
err = "graph.edges must be array";
|
||||
return false;
|
||||
}
|
||||
std::unordered_set<std::string> node_ids;
|
||||
for (const auto& nv : nodes->AsArray()) {
|
||||
std::string nerr;
|
||||
if (!ValidateNodeCfg(nv, nerr)) {
|
||||
err = "graph.nodes invalid: " + nerr;
|
||||
return false;
|
||||
}
|
||||
const SimpleJson* id = nv.Find("id");
|
||||
const std::string id_value = id ? id->AsString("") : "";
|
||||
if (!id_value.empty() && !node_ids.insert(id_value).second) {
|
||||
err = "graph.nodes duplicate id: " + id_value;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& ev : edges->AsArray()) {
|
||||
std::string eerr;
|
||||
@ -198,6 +206,30 @@ inline bool ValidateExpandedRootConfig(const SimpleJson& root, std::string& err)
|
||||
err = "graph.edges invalid: " + eerr;
|
||||
return false;
|
||||
}
|
||||
std::string from;
|
||||
std::string to;
|
||||
if (ev.IsArray()) {
|
||||
const auto& a = ev.AsArray();
|
||||
if (a.size() >= 2) {
|
||||
from = a[0].AsString("");
|
||||
to = a[1].AsString("");
|
||||
}
|
||||
} else if (ev.IsObject()) {
|
||||
if (const SimpleJson* v = ev.Find("from")) {
|
||||
from = v->AsString("");
|
||||
}
|
||||
if (const SimpleJson* v = ev.Find("to")) {
|
||||
to = v->AsString("");
|
||||
}
|
||||
}
|
||||
if (!from.empty() && node_ids.find(from) == node_ids.end()) {
|
||||
err = "graph.edges unknown node: " + from;
|
||||
return false;
|
||||
}
|
||||
if (!to.empty() && node_ids.find(to) == node_ids.end()) {
|
||||
err = "graph.edges unknown node: " + to;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -100,9 +100,13 @@ if(RK3588_ENABLE_RKNN AND RK_RKNN_LIB)
|
||||
endif()
|
||||
set_target_properties(ai_scheduler PROPERTIES
|
||||
OUTPUT_NAME "ai_scheduler"
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
|
||||
LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
|
||||
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
|
||||
)
|
||||
if(WIN32)
|
||||
set_target_properties(ai_scheduler PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
endif()
|
||||
|
||||
add_library(input_rtsp SHARED input_rtsp/input_rtsp_node.cpp)
|
||||
target_include_directories(input_rtsp PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party)
|
||||
|
||||
@ -13,6 +13,9 @@
|
||||
#include <thread>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#elif defined(__unix__) || defined(__APPLE__)
|
||||
#include <unistd.h>
|
||||
|
||||
@ -74,7 +74,7 @@ TEST(SimpleJsonTest, ParseString) {
|
||||
TEST(SimpleJsonTest, ParseStringEscapes) {
|
||||
SimpleJson v;
|
||||
std::string err;
|
||||
EXPECT_TRUE(ParseSimpleJson(R"("line1\nline2\ttab\\slash\"quote")", v, err));
|
||||
EXPECT_TRUE(ParseSimpleJson("\"line1\\nline2\\ttab\\\\slash\\\"quote\"", v, err));
|
||||
EXPECT_TRUE(v.IsString());
|
||||
EXPECT_EQ(v.AsString(), "line1\nline2\ttab\\slash\"quote");
|
||||
}
|
||||
|
||||
@ -237,18 +237,18 @@ TEST(SpscQueueTest, OnDataAvailableCallback) {
|
||||
}
|
||||
|
||||
TEST(SpscQueueTest, ConcurrentPushPop) {
|
||||
SpscQueue<int> q(100, QueueDropStrategy::DropOldest);
|
||||
constexpr int kNumItems = 10000;
|
||||
SpscQueue<int> q(kNumItems, QueueDropStrategy::Block);
|
||||
|
||||
std::atomic<int> sum{0};
|
||||
|
||||
std::thread producer([&q]() {
|
||||
std::thread producer([&q, kNumItems]() {
|
||||
for (int i = 1; i <= kNumItems; ++i) {
|
||||
q.Push(i);
|
||||
}
|
||||
});
|
||||
|
||||
std::thread consumer([&q, &sum]() {
|
||||
std::thread consumer([&q, &sum, kNumItems]() {
|
||||
int received = 0;
|
||||
while (received < kNumItems) {
|
||||
int v;
|
||||
|
||||
26
命令.md
26
命令.md
@ -6,20 +6,22 @@
|
||||
ffmpeg -f dshow -video_size 1280x720 -vcodec mjpeg -i video="1080P USB Camera" -c:v libx264 -preset ultrafast -pix_fmt yuv420p -f rtsp rtsp://localhost:8554/cam
|
||||
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DRK3588_ENABLE_FFMPEG=ON \
|
||||
-DRK3588_ENABLE_MPP=ON \
|
||||
-DRK3588_ENABLE_RGA=ON \
|
||||
-DRK3588_ENABLE_ZLMEDIAKIT=ON \
|
||||
-DRK3588_ENABLE_RKNN=ON \
|
||||
-DRK_ZLMK_API_LIB_PATH=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/aarch64/libmk_api.so \
|
||||
-DRK_ZLMEDIAKIT_INCLUDE_DIR=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/include
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_TESTS=OFF \
|
||||
-DBUILD_SAMPLES=ON \
|
||||
-DRK3588_ENABLE_FFMPEG=ON \
|
||||
-DRK3588_ENABLE_MPP=ON \
|
||||
-DRK3588_ENABLE_RGA=ON \
|
||||
-DRK3588_ENABLE_ZLMEDIAKIT=ON \
|
||||
-DRK3588_ENABLE_RKNN=ON \
|
||||
-DRK_ZLMK_API_LIB_PATH=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/aarch64/libmk_api.so \
|
||||
-DRK_ZLMEDIAKIT_INCLUDE_DIR=$PWD/third_party/rknpu2/examples/3rdparty/zlmediakit/include
|
||||
|
||||
cmake --build build -j$(nproc)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//退出
|
||||
pidof media-server
|
||||
|
||||
@ -112,4 +114,8 @@ ls -l ./rk3588-agent_linux_arm64
|
||||
sudo systemctl restart rk3588-agent
|
||||
|
||||
这样运行后,SSH 断开不会影响进程(由 systemd 托管)。如果你的 agent.config.json 里有相对路径(如 models 目录),记得写成绝对路径,或放到
|
||||
/opt/rk3588-agent/ 并按配置调整。
|
||||
/opt/rk3588-agent/ 并按配置调整。
|
||||
|
||||
|
||||
## 后端启动命令
|
||||
go run .\cmd\managerd\main.go .\managerd.json
|
||||
Loading…
Reference in New Issue
Block a user