diff --git a/API_Device_RemoteMgmt_InterfaceTable.md b/API_Device_RemoteMgmt_InterfaceTable.md index 19e3252..6dfc244 100644 --- a/API_Device_RemoteMgmt_InterfaceTable.md +++ b/API_Device_RemoteMgmt_InterfaceTable.md @@ -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)并选择加载哪个配置文件”。 diff --git a/PRD_06_Hardware_Decoupling_Plan.md b/PRD_06_Hardware_Decoupling_Plan.md new file mode 100644 index 0000000..0264a15 --- /dev/null +++ b/PRD_06_Hardware_Decoupling_Plan.md @@ -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 实现作为默认后端 diff --git a/agent/build/rk3588-agent_linux_arm64 b/agent/build/rk3588-agent_linux_arm64 new file mode 100644 index 0000000..29d0c93 Binary files /dev/null and b/agent/build/rk3588-agent_linux_arm64 differ diff --git a/agent/internal/httpapi/server.go b/agent/internal/httpapi/server.go index cfb58f9..12bf0f0 100644 --- a/agent/internal/httpapi/server.go +++ b/agent/internal/httpapi/server.go @@ -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 { diff --git a/agent/orangepi@10.0.0.81 b/agent/orangepi@10.0.0.81 new file mode 100644 index 0000000..db8b95d --- /dev/null +++ b/agent/orangepi@10.0.0.81 @@ -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] } + } +} diff --git a/configs/sample_cam1_native_rtsp_face.json b/configs/sample_cam1_native_rtsp_face.json index 8166894..fceb1ec 100644 --- a/configs/sample_cam1_native_rtsp_face.json +++ b/configs/sample_cam1_native_rtsp_face.json @@ -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"] diff --git a/include/utils/config_schema.h b/include/utils/config_schema.h index a5d9ab0..1fc24ec 100644 --- a/include/utils/config_schema.h +++ b/include/utils/config_schema.h @@ -1,6 +1,7 @@ #pragma once #include +#include #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 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; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f42537e..27694c9 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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) diff --git a/plugins/alarm/actions/clip_action.cpp b/plugins/alarm/actions/clip_action.cpp index dc5be41..49fa954 100644 --- a/plugins/alarm/actions/clip_action.cpp +++ b/plugins/alarm/actions/clip_action.cpp @@ -13,6 +13,9 @@ #include #if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif #include #elif defined(__unix__) || defined(__APPLE__) #include diff --git a/tests/test_simple_json.cpp b/tests/test_simple_json.cpp index e5b2569..7e6aa60 100644 --- a/tests/test_simple_json.cpp +++ b/tests/test_simple_json.cpp @@ -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"); } diff --git a/tests/test_spsc_queue.cpp b/tests/test_spsc_queue.cpp index 8529fb4..9568160 100644 --- a/tests/test_spsc_queue.cpp +++ b/tests/test_spsc_queue.cpp @@ -237,18 +237,18 @@ TEST(SpscQueueTest, OnDataAvailableCallback) { } TEST(SpscQueueTest, ConcurrentPushPop) { - SpscQueue q(100, QueueDropStrategy::DropOldest); constexpr int kNumItems = 10000; + SpscQueue q(kNumItems, QueueDropStrategy::Block); std::atomic 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; diff --git a/命令.md b/命令.md index 77fb974..8a4367c 100644 --- a/命令.md +++ b/命令.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/ 并按配置调整。 \ No newline at end of file + /opt/rk3588-agent/ 并按配置调整。 + + + ## 后端启动命令 + go run .\cmd\managerd\main.go .\managerd.json \ No newline at end of file