Files
Ayay/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
2026-01-16 15:17:23 +08:00

407 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Ayay.SerilogLogs;
using OpenCvSharp;
using Serilog;
namespace SHH.CameraSdk
{
/// <summary>
/// [终极版] 动态窗口管理器
/// 核心修复:
/// 1. 防止手动关闭窗口后,下一帧自动创建不可缩放的僵尸窗口
/// 2. 确保关闭时流控策略 (FPS=0) 正确下发,释放帧调度资源
/// 3. 新增鼠标点击暂停/恢复功能,支持单窗口帧流控制
/// 4. 【关键修复】在 UILoop 中增加独立轮询机制,**暂停状态下也能检测窗口关闭**
/// 核心特性:
/// - 单线程UI渲染规避 OpenCV GUI 多线程操作风险
/// - 策略强联动:窗口生命周期与帧控制器订阅状态深度绑定
/// - 资源闭环:自动清理订阅、销毁窗口、释放 Mat 内存,无内存泄漏
/// </summary>
public class DisplayWindowManager : IDisposable
{
#region --- ---
private ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
/// <summary>
/// 单个窗口的上下文信息载体
/// 存储设备关联、运行状态、回调函数等核心数据
/// </summary>
private class WindowContext
{
/// <summary> 关联的设备ID </summary>
public int DeviceId { get; set; }
/// <summary> 窗口暂停状态volatile 保证多线程可见性) </summary>
public volatile bool IsPaused;
/// <summary> 鼠标事件回调函数 </summary>
public MouseCallback MouseHandler;
/// <summary> 窗口是否已实际创建(防止重复初始化) </summary>
public bool IsWindowCreated;
}
#endregion
#region --- ---
/// <summary> 活跃窗口注册表(线程安全) </summary>
/// <remarks> Key = 窗口标识 AppId, Value = 窗口上下文 </remarks>
private readonly ConcurrentDictionary<string, WindowContext> _activeWindows = new();
/// <summary> UI操作指令队列 </summary>
/// <remarks> 容量限制100防止渲染阻塞导致内存溢出 </remarks>
private readonly BlockingCollection<Action> _uiActionQueue = new BlockingCollection<Action>(100);
/// <summary> UI渲染专属线程长驻后台 </summary>
private readonly Task _uiThread;
/// <summary> 全局取消令牌源:用于优雅终止渲染循环 </summary>
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
/// <summary> 相机管理器实例:用于联动帧控制器的流控策略 </summary>
private readonly CameraManager _cameraManager;
#endregion
#region --- ---
/// <summary>
/// 初始化动态窗口管理器
/// </summary>
/// <param name="cameraManager">相机管理器实例,用于帧策略联动</param>
public DisplayWindowManager(CameraManager cameraManager)
{
_cameraManager = cameraManager;
// 启动长驻UI线程设置 LongRunning 提升调度优先级
_uiThread = Task.Factory.StartNew(UILoop, TaskCreationOptions.LongRunning);
_sysLog.Information("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)");
}
/// <summary>
/// 释放所有资源,优雅终止窗口管理器
/// </summary>
public void Dispose()
{
// 1. 发送取消信号,终止 UI 循环
_cts.Cancel();
// 2. 等待 UI 线程退出最多等待1秒防止卡死
try { _uiThread.Wait(1000); }
catch (Exception ex) { _sysLog.Error($"[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();
_sysLog.Information("[DisplayManager] 渲染引擎已安全销毁");
}
#endregion
#region --- ---
/// <summary>
/// 启动指定设备的本地显示窗口
/// </summary>
/// <param name="appId">窗口唯一标识</param>
/// <param name="deviceId">关联的设备ID</param>
public void StartDisplay(string appId, int deviceId)
{
// 防重入:已存在该窗口则直接返回
if (_activeWindows.ContainsKey(appId)) return;
_sysLog.Information($"[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;
_sysLog.Information($"[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)
{
_sysLog.Error($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}");
}
});
// 步骤2订阅设备帧数据流
GlobalStreamDispatcher.Subscribe(appId, deviceId, frame =>
{
// 快速判断:取消/暂停/窗口已移除,直接跳过帧处理
if (_cts.IsCancellationRequested || context.IsPaused || !_activeWindows.ContainsKey(appId))
{
return;
}
// 1. 先检查队列容量 (虽然 BlockingCollection 没有完美的无锁 IsFull但可以通过 Count 判断)
// 这是一个不需要 100% 精确的优化,只要能拦截掉大部分无用功即可
if (_uiActionQueue.Count >= 30)
{
return; // 直接丢弃,不进行克隆,节省 CPU
}
Mat frameClone = null;
try
{
// 深拷贝帧数据:原帧属于解码线程,必须克隆后移交 UI 线程
if (frame.TargetMat != null)
frameClone = frame.TargetMat.Clone();
else
frameClone = frame.InternalMat.Clone();
}
catch (Exception ex)
{
_sysLog.Error($"[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();
});
}
}
/// <summary>
/// 停止并销毁指定的显示窗口
/// </summary>
/// <param name="appId">窗口唯一标识</param>
public void StopDisplay(string appId)
{
// 从注册表中移除窗口上下文
if (_activeWindows.TryRemove(appId, out var context))
{
_sysLog.Information($"[DisplayManager] 正在清理窗口资源: {appId}");
// 步骤1立即取消帧数据流订阅
GlobalStreamDispatcher.Unsubscribe(appId);
// 步骤2投递窗口销毁指令到 UI 线程
_uiActionQueue.TryAdd(() =>
{
try
{
Cv2.DestroyWindow(appId);
}
catch (Exception ex)
{
_sysLog.Error($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}");
}
});
// 步骤3关键操作 - 下发流控策略FPS=0释放帧控制器资源
UpdateDevicePolicy(context.DeviceId, appId, 0);
}
}
#endregion
#region --- ---
/// <summary>
/// 暂停指定窗口的帧显示
/// </summary>
/// <param name="appId">窗口唯一标识</param>
public void PauseDisplay(string appId)
{
if (_activeWindows.TryGetValue(appId, out var ctx))
{
ctx.IsPaused = true;
_sysLog.Information($"[DisplayManager] 窗口 {appId} 已暂停");
}
}
/// <summary>
/// 恢复指定窗口的帧显示
/// </summary>
/// <param name="appId">窗口唯一标识</param>
public void ResumeDisplay(string appId)
{
if (_activeWindows.TryGetValue(appId, out var ctx))
{
ctx.IsPaused = false;
_sysLog.Information($"[DisplayManager] 窗口 {appId} 已恢复");
}
}
#endregion
#region --- ---
/// <summary>
/// 更新设备的帧订阅策略,与帧控制器强联动
/// </summary>
/// <param name="deviceId">设备ID</param>
/// <param name="appId">订阅标识窗口ID</param>
/// <param name="fps">目标帧率0表示取消订阅</param>
private void UpdateDevicePolicy(int deviceId, string appId, int fps)
{
try
{
// 获取设备实例
var device = _cameraManager.GetDevice(deviceId);
if (device == null)
{
_sysLog.Information($"[策略联动] 设备 {deviceId} 不存在");
return;
}
// 获取设备内置的帧控制器
var frameController = device.Controller;
if (frameController == null)
{
_sysLog.Information($"[策略联动] 设备 {deviceId} 未配置帧控制器");
return;
}
// 根据帧率更新订阅状态
if (fps > 0)
{
frameController.Register(appId, fps);
_sysLog.Information($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS");
}
else
{
frameController.Unregister(appId);
_sysLog.Information($"[策略联动] 🗑️ 已注销流控: {appId}");
}
// 记录审计日志,用于前端排查问题
device.AddAuditLog($"UI播放窗口状态变更: {appId} -> {(fps > 0 ? $"{fps}fps" : "")}");
}
catch (Exception ex)
{
_sysLog.Error($"[策略联动] ❌ 联动失败: {ex.Message}");
}
}
#endregion
#region --- ---
/// <summary>
/// 单线程UI渲染循环所有 OpenCV GUI 操作的唯一入口
/// </summary>
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)
{
_sysLog.Error($"[UI] 渲染循环异常: {ex.Message}");
}
}
// 循环退出时,销毁所有残留窗口
Cv2.DestroyAllWindows();
}
/// <summary>
/// 检查所有活跃窗口的状态,自动清理被手动关闭的窗口
/// 【必须在 UI 线程调用】
/// </summary>
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)
{
_sysLog.Information($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理...");
// 异步清理:避免 StopDisplay 内部的队列操作阻塞 UI 线程
Task.Run(() => StopDisplay(appId));
}
}
catch
{
// 捕获异常:通常是窗口已不存在,直接触发清理
Task.Run(() => StopDisplay(appId));
}
}
}
#endregion
}
}