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 监控与诊断服务 (注入服务与端口)