using OpenCvSharp; namespace SHH.CameraSdk; /// /// [消费者] 专用渲染线程(零延迟设计) /// 核心策略: /// 1. 容量为1的阻塞队列:仅保留最新一帧,杜绝帧堆积 /// 2. 非阻塞入队+主动丢帧:渲染慢时直接丢弃新帧,确保主线程不阻塞 /// 3. 引用计数联动:丢帧时立即释放引用,内存自动回池复用 /// public class FrameConsumer : IDisposable { #region --- 私有资源与状态 (Private Resources & States) --- /// 帧缓冲队列(容量1):仅存储最新一帧,保证零延迟渲染 /// BlockingCollection 封装线程安全操作,GetConsumingEnumerable 支持取消令牌 private readonly BlockingCollection _frameBuffer = new(1); /// 取消令牌源:用于终止渲染循环 private readonly CancellationTokenSource _cts = new(); /// 后台渲染任务 private Task? _renderTask; /// OpenCV 窗口名称 private readonly string _windowName; #endregion #region --- 构造与生命周期 (Constructor & Lifecycle) --- /// /// 初始化帧渲染消费者 /// /// OpenCV 显示窗口名称 public FrameConsumer(string windowName = "Zero Latency Preview") { _windowName = windowName; } /// /// 启动渲染线程 /// public void Start() { // 防止重复启动 if (_renderTask != null) return; // 启动长期运行的渲染任务,提升线程调度优先级 _renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning); Console.WriteLine($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}"); } /// /// 停止渲染线程并清理资源 /// public void Stop() { // 发送取消信号,终止渲染循环 _cts.Cancel(); // 标记队列完成添加,触发 GetConsumingEnumerable 退出遍历 _frameBuffer.CompleteAdding(); // 等待渲染任务结束(最多等待1秒,防止卡死) if (_renderTask != null) { Task.WaitAny(_renderTask, Task.Delay(1000)); _renderTask = null; } // 清理队列残余帧:释放所有未消费帧的引用,防止内存泄漏 while (_frameBuffer.TryTake(out var residualFrame)) { residualFrame.Dispose(); } Console.WriteLine($"[Consumer] 渲染线程已停止,窗口: {_windowName}"); } #endregion #region --- 帧入队与渲染逻辑 (Frame Enqueue & Render Logic) --- /// /// [生产端入口] 接收帧并尝试入队(非阻塞) /// /// 待渲染的智能帧 public void Enqueue(SmartFrame frame) { // 防护:线程已停止,直接释放帧引用 if (_cts.IsCancellationRequested) { frame.Dispose(); return; } // 核心零延迟策略:非阻塞尝试入队 // 队列满 → 上一帧未渲染完成 → 丢弃当前帧,释放引用 if (!_frameBuffer.TryAdd(frame)) { frame.Dispose(); // Debug.WriteLine($"[Drop] 渲染线程[{_windowName}]处理过慢,丢弃一帧"); } // 入队成功 → 帧由队列托管,等待渲染线程消费 } /// /// 后台渲染循环(核心逻辑) /// private void RenderLoop() { bool isWindowCreated = false; try { // 我们不再使用简单的 foreach 阻塞等待数据, // 而是改用非阻塞模式或带有超时的读取,以保证 WaitKey 的活性 while (!_cts.Token.IsCancellationRequested) { // 尝试在 30ms 内获取一帧数据(相当于 33 fps 的响应速度) if (_frameBuffer.TryTake(out var frame, 30, _cts.Token)) { try { if (frame.InternalMat != null && !frame.InternalMat.IsDisposed) { if (!isWindowCreated) { Cv2.NamedWindow(_windowName, WindowFlags.Normal); isWindowCreated = true; } Cv2.ImShow(_windowName, frame.InternalMat); } } finally { frame.Dispose(); } } // 【核心修复】无论有没有取到帧,都要执行 WaitKey // 只有这样,窗口在没视频时才能被拖动、最小化或手动点击 X 关闭 if (isWindowCreated) { // 1ms 的等待足以处理 Windows 窗口消息 int key = Cv2.WaitKey(1); // 如果用户点击了 OpenCV 窗口右上角的 X (部分版本支持) // 或者按下 ESC,可以根据需要在这里处理 if (key == 27) break; // 检查窗口是否还存在(防止用户手动关掉窗口后报错) // 如果窗口被手动关闭,我们标记为未创建,下次有流时重新弹窗 try { if (Cv2.GetWindowProperty(_windowName, WindowPropertyFlags.Visible) < 1) { isWindowCreated = false; } } catch { isWindowCreated = false; } } } } catch (OperationCanceledException) { } catch { } finally { if (isWindowCreated) { Cv2.DestroyWindow(_windowName); } } } #endregion #region --- 资源释放 (Disposal) --- /// /// 释放所有资源 /// public void Dispose() { Stop(); _frameBuffer.Dispose(); _cts.Dispose(); } #endregion }