Quellcode durchsuchen

修复网格布局跳动 + MJPEG推流开关

- DisplayOutput: 空帧不再erase通道(保留最后一帧)
  支持fixed_cells固定布局单元数, 避免启动/断帧时布局从1路跳到N路
- 网格布局固定为rtsp通道数, 钢包位固定2格
- WebServer: 新增MJPEG推流开关(临时关闭省CPU)
  /api/mjpeg/status, /api/mjpeg/toggle, config.html加下拉开关
- 关闭时新连接返回stream disabled, 已有连接自动断开
dev vor 1 Woche
Ursprung
Commit
b784369226
6 geänderte Dateien mit 81 neuen und 9 gelöschten Zeilen
  1. 5 2
      src/display_output.cpp
  2. 3 1
      src/display_output.h
  3. 31 2
      src/main.cpp
  4. 28 1
      src/web_server.cpp
  5. 7 3
      src/web_server.h
  6. 7 0
      web/config.html

+ 5 - 2
src/display_output.cpp

@@ -8,12 +8,13 @@ DisplayOutput::~DisplayOutput() {
 }
 
 bool DisplayOutput::start(bool enable, int grid_cols, const string& title,
-                          bool fullscreen, int refresh_ms) {
+                          bool fullscreen, int refresh_ms, int fixed_cells) {
     enable_ = enable;
     grid_cols_ = grid_cols;
     title_ = title;
     fullscreen_ = fullscreen;
     refresh_ms_ = refresh_ms > 0 ? refresh_ms : 100;
+    fixed_cells_ = fixed_cells;
     if (!enable_) return false;
     if (running_.load()) return true;
 
@@ -37,7 +38,7 @@ void DisplayOutput::setFrame(int channel, const cv::Mat& frame,
     if (!enable_.load()) return;
     lock_guard<mutex> lk(mtx_);
     if (frame.empty()) {
-        frames_.erase(channel);
+        // 空帧保留最后一帧, 避免网格布局来回跳动
         return;
     }
     frames_[channel] = {frame.clone(), label};
@@ -80,6 +81,8 @@ void DisplayOutput::loop() {
             cv::imshow(title_, black);
         } else {
             int n = (int)snap.size();
+            // 固定布局单元数(避免启动/断帧时布局跳动)
+            if (fixed_cells_ > 0 && fixed_cells_ >= n) n = fixed_cells_;
             int cols = grid_cols_;
             if (cols <= 0) {
                 // 自动布局: 1->1, 2->2, 3/4->2, 5-6->3, 7-9->3, 10-16->4

+ 3 - 1
src/display_output.h

@@ -16,8 +16,9 @@ public:
 
     // 初始化: enable=启用输出, grid_cols=网格列数(0=自动), fullscreen=全屏显示
     // refresh_ms=画布刷新间隔(毫秒, 默认100=10FPS, 0=最快)
+    // fixed_cells=固定布局单元数(0=动态按已收帧数), 设固定值可避免启动/断帧时布局跳动
     bool start(bool enable, int grid_cols = 0, const std::string& title = "ZoneAlarm",
-               bool fullscreen = true, int refresh_ms = 100);
+               bool fullscreen = true, int refresh_ms = 100, int fixed_cells = 0);
     void stop();
 
     void setFrame(int channel, const cv::Mat& frame, const std::string& label);
@@ -34,6 +35,7 @@ private:
     std::string title_ = "ZoneAlarm";
     int grid_cols_ = 0;
     int refresh_ms_ = 100;
+    int fixed_cells_ = 0;
 
     std::mutex mtx_;
     std::map<int, std::pair<cv::Mat, std::string>> frames_;

+ 31 - 2
src/main.cpp

@@ -1503,9 +1503,38 @@ int main(int argc, char* argv[]) {
         }
     });
 
+    // ========== MJPEG推流开关(临时关闭省CPU) ==========
+    web.setGetHandler("/api/mjpeg/status", [&]() -> string {
+        json r;
+        r["enabled"] = web.mjpegEnabled();
+        return r.dump();
+    });
+
+    web.setPostHandler("/api/mjpeg/toggle", [&](const string& body) -> string {
+        try {
+            json j = json::parse(body);
+            if (!j.contains("enabled"))
+                return R"({"ok":false,"error":"enabled required"})";
+            bool en = j["enabled"].get<bool>();
+            web.setMjpegEnabled(en);
+            cout << "[Web] MJPEG推流: " << (en ? "启用" : "临时关闭")
+                 << " (省CPU)" << endl;
+            json r;
+            r["ok"] = true;
+            r["enabled"] = en;
+            return r.dump();
+        } catch (exception& e) {
+            return string(R"({"ok":false,"error":")") + e.what() + R"("})";
+        }
+    });
+
     web.start();
-    displayGrid.start(grid_output, hdmi_grid_cols, app_name + " - 通道网格", hdmi_fullscreen);
-    displayBucket.start(steel_output, 2, app_name + " - 钢包位", hdmi_fullscreen);
+    // 网格布局固定为已配置的rtsp通道数, 避免启动/断帧时布局跳动
+    int grid_fixed = (int)channels.size();
+    displayGrid.start(grid_output, hdmi_grid_cols, app_name + " - 通道网格",
+                      hdmi_fullscreen, 100, grid_fixed);
+    displayBucket.start(steel_output, 2, app_name + " - 钢包位",
+                        hdmi_fullscreen, 100, 2);
     cout << "[Main] Ctrl+C 退出" << endl;
 
     // 自动激活第一个工位(如果有配置)

+ 28 - 1
src/web_server.cpp

@@ -18,6 +18,7 @@ public:
     string host;
     int port;
     std::atomic<bool> stopping{false};   // 退出标记: 让MJPEG推流循环及时结束
+    std::atomic<bool> mjpeg_enabled{true}; // MJPEG推流开关
     unordered_map<string, function<string()>> get_handlers;
     unordered_map<string, function<string(const map<string,string>&)>> getq_handlers;
     unordered_map<string, function<string(const string&)>> post_handlers;
@@ -227,6 +228,10 @@ public:
 
         svr.Get("/mjpeg/:ch", [this](const httplib::Request& req, httplib::Response& res) {
             if (!authed(req)) { denyJson(res); return; }
+            if (!mjpeg_enabled.load()) {
+                res.set_content("stream disabled", "text/plain");
+                return;
+            }
             int ch = 0;
             try { ch = stoi(req.path_params.at("ch")); } catch (...) {}
             auto fp = frame_providers.find(ch);
@@ -241,7 +246,7 @@ public:
                 "multipart/x-mixed-replace; boundary=mjpegboundary",
                 [this, fp, seqfn](size_t, httplib::DataSink& sink) -> bool {
                     uint64_t last_seq = 0;
-                    while (!stopping.load() && sink.is_writable()) {
+                    while (!stopping.load() && mjpeg_enabled.load() && sink.is_writable()) {
                         uint64_t seq = seqfn ? seqfn() : 0;
                         if (seq != 0 && seq == last_seq) {
                             // 帧未更新, 跳过重复JPEG编码
@@ -509,6 +514,20 @@ public:
             if (it != post_handlers.end())
                 res.set_content(it->second(req.body), "application/json");
         });
+
+        svr.Get("/api/mjpeg/status", [this](const httplib::Request& req, httplib::Response& res) {
+            if (!authed(req)) { denyJson(res); return; }
+            auto it = get_handlers.find("/api/mjpeg/status");
+            if (it != get_handlers.end())
+                res.set_content(it->second(), "application/json");
+        });
+
+        svr.Post("/api/mjpeg/toggle", [this](const httplib::Request& req, httplib::Response& res) {
+            if (!authed(req)) { denyJson(res); return; }
+            auto it = post_handlers.find("/api/mjpeg/toggle");
+            if (it != post_handlers.end())
+                res.set_content(it->second(req.body), "application/json");
+        });
     }
 };
 
@@ -536,6 +555,14 @@ void WebServer::stop() {
     running_ = false;
 }
 
+void WebServer::setMjpegEnabled(bool en) {
+    if (impl_) impl_->mjpeg_enabled.store(en);
+}
+
+bool WebServer::mjpegEnabled() const {
+    return impl_ ? impl_->mjpeg_enabled.load() : true;
+}
+
 void WebServer::setGetHandler(const string& path, function<string()> handler) {
     impl_->get_handlers[path] = handler;
 }

+ 7 - 3
src/web_server.h

@@ -39,12 +39,16 @@ public:
     void setFrameSeqProvider(int channel, std::function<uint64_t()> provider);
     void removeFrameProvider(int channel);
 
+    // MJPEG 推流开关(临时关闭省CPU): true=允许推流, false=拒绝并断开
+    void setMjpegEnabled(bool en);
+    bool mjpegEnabled() const;
+
+    class Impl;
+    Impl* impl_ = nullptr;
+
 private:
     int port_ = 8080;
     std::string host_ = "0.0.0.0";
     std::atomic<bool> running_{false};
     std::thread server_thread_;
-
-    class Impl;
-    Impl* impl_ = nullptr;
 };

+ 7 - 0
web/config.html

@@ -157,6 +157,13 @@ body.rail .logo .t,body.rail .nav-label,body.rail .side-foot .nav-label{display:
           </select>
         </div>
       </div>
+      <div class="form-group">
+        <label>Web视频推流(MJPEG) <span style="color:#e94560" id="mjpegState"></span></label>
+        <select id="mjpegEnabled" onchange="toggleMjpeg()">
+          <option value="true">启用</option>
+          <option value="false">临时关闭(省CPU)</option>
+        </select>
+      </div>
       <div class="btn-row">
         <button class="btn btn-primary" onclick="saveSystemConfig()">保存系统配置</button>
       </div>