From f9027e856e8d3e21e6f016b53fa0176d31efb18a Mon Sep 17 00:00:00 2001
From: twice109 <3518499@qq.com>
Date: Sat, 27 Dec 2025 05:05:06 +0800
Subject: [PATCH] =?UTF-8?q?Cv2.ImShow=20=E6=98=BE=E7=A4=BA=EF=BC=8C?=
=?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BA=BF=E7=A8=8B=E5=AE=89=E5=85=A8=E5=A4=84?=
=?UTF-8?q?=E7=BD=AE=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=E7=82=B9?=
=?UTF-8?q?=E5=87=BB=E7=94=BB=E9=9D=A2=E6=9A=82=E5=81=9C=E3=80=81=E7=BB=A7?=
=?UTF-8?q?=E7=BB=AD=E6=98=BE=E7=A4=BA=EF=BC=8C=E5=85=B3=E9=97=AD=E7=AA=97?=
=?UTF-8?q?=E4=BD=93=E6=B3=A8=E9=94=80=E5=8A=A8=E6=80=81=E6=B5=81=E6=8E=A7?=
=?UTF-8?q?=E7=9A=84=E8=AE=A2=E9=98=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Core/Services/DisplayWindowManager.cs | 427 ++++++++++++++++--
SHH.CameraSdk/Drivers/BaseVideoSource.cs | 1 +
SHH.CameraSdk/Program.cs | 2 +-
3 files changed, 382 insertions(+), 48 deletions(-)
diff --git a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
index 407193e..5142b41 100644
--- a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
+++ b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
@@ -1,63 +1,396 @@
-namespace SHH.CameraSdk;
+using OpenCvSharp;
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
-///
-/// 动态窗口管理器
-/// 职责:根据业务指令动态创建/销毁 OpenCV 播放窗口,并管理流订阅
-///
-public class DisplayWindowManager
+namespace SHH.CameraSdk
{
- // 存储活跃的渲染器实例:Key = AppId (如 "UI_Preview_Main")
- private readonly ConcurrentDictionary _activeWindows = new();
-
///
- /// 开启一个本地播放窗口
+ /// [终极版] 动态窗口管理器
+ /// 核心修复:
+ /// 1. 防止手动关闭窗口后,下一帧自动创建不可缩放的僵尸窗口
+ /// 2. 确保关闭时流控策略 (FPS=0) 正确下发,释放帧调度资源
+ /// 3. 新增鼠标点击暂停/恢复功能,支持单窗口帧流控制
+ /// 4. 【关键修复】在 UILoop 中增加独立轮询机制,**暂停状态下也能检测窗口关闭**
+ /// 核心特性:
+ /// - 单线程UI渲染:规避 OpenCV GUI 多线程操作风险
+ /// - 策略强联动:窗口生命周期与帧控制器订阅状态深度绑定
+ /// - 资源闭环:自动清理订阅、销毁窗口、释放 Mat 内存,无内存泄漏
///
- /// 业务标识 (将作为窗口标题)
- /// 要观看的设备ID
- public void StartDisplay(string appId, int deviceId)
+ public class DisplayWindowManager : IDisposable
{
- // 如果窗口已存在,直接返回(防止重复创建)
- if (_activeWindows.ContainsKey(appId)) return;
-
- Console.WriteLine($"[DisplayManager] 正在创建窗口: {appId} -> Device {deviceId}...");
-
- // 1. 动态创建渲染器
- var renderer = new FrameConsumer(appId);
-
- // 2. 启动渲染循环 (由于我们之前加了懒加载逻辑,此时不会立即弹窗,直到有帧数据过来)
- renderer.Start();
-
- // 3. 存入字典管理
- if (_activeWindows.TryAdd(appId, renderer))
+ #region --- 内部窗口上下文类 ---
+ ///
+ /// 单个窗口的上下文信息载体
+ /// 存储设备关联、运行状态、回调函数等核心数据
+ ///
+ private class WindowContext
{
- // 4. 【关键】建立数据订阅:将设备流导向这个渲染器
- GlobalStreamDispatcher.Subscribe(appId, deviceId, frame =>
+ /// 关联的设备ID
+ public int DeviceId { get; set; }
+ /// 窗口暂停状态(volatile 保证多线程可见性)
+ public volatile bool IsPaused;
+ /// 鼠标事件回调函数
+ public MouseCallback MouseHandler;
+ /// 窗口是否已实际创建(防止重复初始化)
+ public bool IsWindowCreated;
+ }
+ #endregion
+
+ #region --- 私有成员变量 ---
+ /// 活跃窗口注册表(线程安全)
+ /// Key = 窗口标识 AppId, Value = 窗口上下文
+ private readonly ConcurrentDictionary _activeWindows = new();
+
+ /// UI操作指令队列
+ /// 容量限制100,防止渲染阻塞导致内存溢出
+ private readonly BlockingCollection _uiActionQueue = new BlockingCollection(100);
+
+ /// UI渲染专属线程(长驻后台)
+ private readonly Task _uiThread;
+
+ /// 全局取消令牌源:用于优雅终止渲染循环
+ private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+ /// 相机管理器实例:用于联动帧控制器的流控策略
+ private readonly CameraManager _cameraManager;
+ #endregion
+
+ #region --- 构造与析构函数 ---
+ ///
+ /// 初始化动态窗口管理器
+ ///
+ /// 相机管理器实例,用于帧策略联动
+ public DisplayWindowManager(CameraManager cameraManager)
+ {
+ _cameraManager = cameraManager;
+ // 启动长驻UI线程,设置 LongRunning 提升调度优先级
+ _uiThread = Task.Factory.StartNew(UILoop, TaskCreationOptions.LongRunning);
+ Console.WriteLine("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)");
+ }
+
+ ///
+ /// 释放所有资源,优雅终止窗口管理器
+ ///
+ public void Dispose()
+ {
+ // 1. 发送取消信号,终止 UI 循环
+ _cts.Cancel();
+
+ // 2. 等待 UI 线程退出(最多等待1秒,防止卡死)
+ try { _uiThread.Wait(1000); }
+ catch (Exception ex) { Console.WriteLine($"[DisplayManager] 线程退出异常: {ex.Message}"); }
+
+ // 3. 强制清理所有活跃窗口
+ foreach (var appId in _activeWindows.Keys)
{
- // 引用计数 +1,防止在渲染前被回收
- frame.AddRef();
- renderer.Enqueue(frame);
- });
+ StopDisplay(appId);
+ }
+
+ // 4. 清空 UI 指令队列,释放残留资源
+ while (_uiActionQueue.TryTake(out var _)) { }
+
+ // 5. 销毁所有 OpenCV 残留窗口
+ Cv2.DestroyAllWindows();
+
+ // 6. 释放核心组件资源
+ _uiActionQueue.Dispose();
+ _cts.Dispose();
+
+ Console.WriteLine("[DisplayManager] 渲染引擎已安全销毁");
}
- else
+ #endregion
+
+ #region --- 窗口生命周期管理 ---
+ ///
+ /// 启动指定设备的本地显示窗口
+ ///
+ /// 窗口唯一标识
+ /// 关联的设备ID
+ public void StartDisplay(string appId, int deviceId)
{
- renderer.Dispose(); // 并发冲突处理
- }
- }
+ // 防重入:已存在该窗口则直接返回
+ if (_activeWindows.ContainsKey(appId)) return;
- ///
- /// 关闭并销毁窗口
- ///
- public void StopDisplay(string appId)
- {
- if (_activeWindows.TryRemove(appId, out var renderer))
+ Console.WriteLine($"[DisplayManager] 正在启动窗口: {appId} -> Device {deviceId}");
+
+ // 初始化窗口上下文
+ var context = new WindowContext
+ {
+ DeviceId = deviceId,
+ IsPaused = false,
+ IsWindowCreated = false
+ };
+
+ // 注册鼠标左键回调:切换暂停/恢复状态
+ context.MouseHandler = (mouseEvent, x, y, flags, userData) =>
+ {
+ if (mouseEvent == MouseEventTypes.LButtonDown)
+ {
+ context.IsPaused = !context.IsPaused;
+ Console.WriteLine($"[DisplayManager] 窗口 {appId} 状态切换: {(context.IsPaused ? "暂停" : "恢复")}");
+ }
+ };
+
+ // 线程安全地添加窗口上下文
+ if (_activeWindows.TryAdd(appId, context))
+ {
+ // 步骤1:投递窗口初始化指令到 UI 线程
+ _uiActionQueue.TryAdd(() =>
+ {
+ try
+ {
+ // 创建可缩放窗口,并设置初始尺寸 640x360
+ Cv2.NamedWindow(appId, WindowFlags.Normal | WindowFlags.GuiExpanded);
+ Cv2.ResizeWindow(appId, 640, 360);
+ // 绑定鼠标事件回调
+ Cv2.SetMouseCallback(appId, context.MouseHandler);
+ // 标记窗口已实际创建
+ context.IsWindowCreated = true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}");
+ }
+ });
+
+ // 步骤2:订阅设备帧数据流
+ GlobalStreamDispatcher.Subscribe(appId, deviceId, frame =>
+ {
+ // 快速判断:取消/暂停/窗口已移除,直接跳过帧处理
+ if (_cts.IsCancellationRequested || context.IsPaused || !_activeWindows.ContainsKey(appId))
+ {
+ return;
+ }
+
+ Mat frameClone = null;
+ try
+ {
+ // 深拷贝帧数据:原帧属于解码线程,必须克隆后移交 UI 线程
+ frameClone = frame.InternalMat.Clone();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[DisplayManager] 帧克隆失败: {ex.Message}");
+ return;
+ }
+
+ // 投递帧显示指令到 UI 线程
+ bool enqueued = _uiActionQueue.TryAdd(() =>
+ {
+ try
+ {
+ // 双重检查:窗口上下文是否存在 + 窗口是否已创建
+ if (_activeWindows.TryGetValue(appId, out var ctx) && ctx.IsWindowCreated)
+ {
+ // 最后一道防线:检测窗口是否被手动关闭
+ if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0)
+ {
+ Task.Run(() => StopDisplay(appId));
+ return;
+ }
+ // 执行帧显示
+ Cv2.ImShow(appId, frameClone);
+ }
+ }
+ finally
+ {
+ // 强制释放克隆的 Mat 内存,杜绝内存泄漏
+ frameClone?.Dispose();
+ }
+ });
+
+ // 队列满时的兜底处理:手动释放克隆帧
+ if (!enqueued) frameClone?.Dispose();
+ });
+ }
+ }
+
+ ///
+ /// 停止并销毁指定的显示窗口
+ ///
+ /// 窗口唯一标识
+ public void StopDisplay(string appId)
{
- Console.WriteLine($"[DisplayManager] 正在关闭窗口: {appId}");
+ // 从注册表中移除窗口上下文
+ if (_activeWindows.TryRemove(appId, out var context))
+ {
+ Console.WriteLine($"[DisplayManager] 正在清理窗口资源: {appId}");
- // 1. 取消订阅 (停止接收数据)
- GlobalStreamDispatcher.Unsubscribe(appId);
+ // 步骤1:立即取消帧数据流订阅
+ GlobalStreamDispatcher.Unsubscribe(appId);
- // 2. 销毁渲染器 (OpenCV DestroyWindow 会在 FrameConsumer 内部触发)
- renderer.Dispose();
+ // 步骤2:投递窗口销毁指令到 UI 线程
+ _uiActionQueue.TryAdd(() =>
+ {
+ try
+ {
+ Cv2.DestroyWindow(appId);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}");
+ }
+ });
+
+ // 步骤3:关键操作 - 下发流控策略(FPS=0),释放帧控制器资源
+ UpdateDevicePolicy(context.DeviceId, appId, 0);
+ }
}
+ #endregion
+
+ #region --- 窗口状态控制 ---
+ ///
+ /// 暂停指定窗口的帧显示
+ ///
+ /// 窗口唯一标识
+ public void PauseDisplay(string appId)
+ {
+ if (_activeWindows.TryGetValue(appId, out var ctx))
+ {
+ ctx.IsPaused = true;
+ Console.WriteLine($"[DisplayManager] 窗口 {appId} 已暂停");
+ }
+ }
+
+ ///
+ /// 恢复指定窗口的帧显示
+ ///
+ /// 窗口唯一标识
+ public void ResumeDisplay(string appId)
+ {
+ if (_activeWindows.TryGetValue(appId, out var ctx))
+ {
+ ctx.IsPaused = false;
+ Console.WriteLine($"[DisplayManager] 窗口 {appId} 已恢复");
+ }
+ }
+ #endregion
+
+ #region --- 流控策略联动 ---
+ ///
+ /// 更新设备的帧订阅策略,与帧控制器强联动
+ ///
+ /// 设备ID
+ /// 订阅标识(窗口ID)
+ /// 目标帧率(0表示取消订阅)
+ private void UpdateDevicePolicy(int deviceId, string appId, int fps)
+ {
+ try
+ {
+ // 获取设备实例
+ var device = _cameraManager.GetDevice(deviceId);
+ if (device == null)
+ {
+ Console.WriteLine($"[策略联动] 设备 {deviceId} 不存在");
+ return;
+ }
+
+ // 获取设备内置的帧控制器
+ var frameController = device.Controller;
+ if (frameController == null)
+ {
+ Console.WriteLine($"[策略联动] 设备 {deviceId} 未配置帧控制器");
+ return;
+ }
+
+ // 根据帧率更新订阅状态
+ if (fps > 0)
+ {
+ frameController.Register(appId, fps);
+ Console.WriteLine($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS");
+ }
+ else
+ {
+ frameController.Unregister(appId);
+ Console.WriteLine($"[策略联动] 🗑️ 已注销流控: {appId}");
+ }
+
+ // 记录审计日志,用于前端排查问题
+ device.AddAuditLog($"UI播放窗口状态变更: {appId} -> {(fps > 0 ? $"{fps}fps" : "停止")}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[策略联动] ❌ 联动失败: {ex.Message}");
+ }
+ }
+ #endregion
+
+ #region --- 核心渲染循环 ---
+ ///
+ /// 单线程UI渲染循环,所有 OpenCV GUI 操作的唯一入口
+ ///
+ private void UILoop()
+ {
+ // 轮询计数器:控制窗口状态检查频率
+ int checkCounter = 0;
+
+ while (!_cts.IsCancellationRequested)
+ {
+ try
+ {
+ // 阶段1:处理 UI 指令队列
+ int processedCount = 0;
+ // 单次循环最多处理10条指令,避免长时间占用线程
+ while (processedCount < 10 && _uiActionQueue.TryTake(out var action, 5))
+ {
+ action.Invoke();
+ processedCount++;
+ }
+
+ // 阶段2:执行 OpenCV 消息泵,防止窗口无响应
+ Cv2.WaitKey(1);
+
+ // 阶段3:【核心修复】独立轮询检测窗口状态
+ // 每20次循环(约200ms)检查一次,不受帧渲染暂停影响
+ checkCounter++;
+ if (checkCounter >= 20)
+ {
+ checkCounter = 0;
+ CheckClosedWindows();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[UI] 渲染循环异常: {ex.Message}");
+ }
+ }
+
+ // 循环退出时,销毁所有残留窗口
+ Cv2.DestroyAllWindows();
+ }
+
+ ///
+ /// 检查所有活跃窗口的状态,自动清理被手动关闭的窗口
+ /// 【必须在 UI 线程调用】
+ ///
+ private void CheckClosedWindows()
+ {
+ // 获取当前活跃窗口ID的快照,避免遍历过程中集合被修改
+ var activeIds = _activeWindows.Keys.ToArray();
+
+ foreach (var appId in activeIds)
+ {
+ try
+ {
+ // GetWindowProperty 必须在 UI 线程执行
+ // Visible < 1.0 表示窗口已被用户手动关闭
+ if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0)
+ {
+ Console.WriteLine($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理...");
+ // 异步清理:避免 StopDisplay 内部的队列操作阻塞 UI 线程
+ Task.Run(() => StopDisplay(appId));
+ }
+ }
+ catch
+ {
+ // 捕获异常:通常是窗口已不存在,直接触发清理
+ Task.Run(() => StopDisplay(appId));
+ }
+ }
+ }
+ #endregion
}
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs
index 438d7dd..1fa4de3 100644
--- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs
+++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs
@@ -693,4 +693,5 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
_auditLogs.Clear();
AddAuditLog("用户清空了审计日志");
}
+
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs
index 2a51231..1e4428d 100644
--- a/SHH.CameraSdk/Program.cs
+++ b/SHH.CameraSdk/Program.cs
@@ -47,7 +47,7 @@ public class Program
using var cameraManager = new CameraManager(storageService);
// 动态窗口管理器
- var displayManager = new DisplayWindowManager();
+ var displayManager = new DisplayWindowManager(cameraManager);
// ==============================================================================
// 3. 启动 Web 监控与诊断服务 (注入服务与端口)