using OpenCvSharp; using System; using System.Collections.Concurrent; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace SHH.CameraSdk { /// /// [终极版] 动态窗口管理器 /// 核心修复: /// 1. 防止手动关闭窗口后,下一帧自动创建不可缩放的僵尸窗口 /// 2. 确保关闭时流控策略 (FPS=0) 正确下发,释放帧调度资源 /// 3. 新增鼠标点击暂停/恢复功能,支持单窗口帧流控制 /// 4. 【关键修复】在 UILoop 中增加独立轮询机制,**暂停状态下也能检测窗口关闭** /// 核心特性: /// - 单线程UI渲染:规避 OpenCV GUI 多线程操作风险 /// - 策略强联动:窗口生命周期与帧控制器订阅状态深度绑定 /// - 资源闭环:自动清理订阅、销毁窗口、释放 Mat 内存,无内存泄漏 /// public class DisplayWindowManager : IDisposable { #region --- 内部窗口上下文类 --- /// /// 单个窗口的上下文信息载体 /// 存储设备关联、运行状态、回调函数等核心数据 /// private class WindowContext { /// 关联的设备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) { StopDisplay(appId); } // 4. 清空 UI 指令队列,释放残留资源 while (_uiActionQueue.TryTake(out var _)) { } // 5. 销毁所有 OpenCV 残留窗口 Cv2.DestroyAllWindows(); // 6. 释放核心组件资源 _uiActionQueue.Dispose(); _cts.Dispose(); Console.WriteLine("[DisplayManager] 渲染引擎已安全销毁"); } #endregion #region --- 窗口生命周期管理 --- /// /// 启动指定设备的本地显示窗口 /// /// 窗口唯一标识 /// 关联的设备ID public void StartDisplay(string appId, int deviceId) { // 防重入:已存在该窗口则直接返回 if (_activeWindows.ContainsKey(appId)) return; 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) { // 从注册表中移除窗口上下文 if (_activeWindows.TryRemove(appId, out var context)) { Console.WriteLine($"[DisplayManager] 正在清理窗口资源: {appId}"); // 步骤1:立即取消帧数据流订阅 GlobalStreamDispatcher.Unsubscribe(appId); // 步骤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 } }