Files
Ayay/SHH.CameraSdk/Core/Features/FrameConsumer.cs

193 lines
6.4 KiB
C#
Raw 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 OpenCvSharp;
namespace SHH.CameraSdk;
/// <summary>
/// [消费者] 专用渲染线程(零延迟设计)
/// 核心策略:
/// <para>1. 容量为1的阻塞队列仅保留最新一帧杜绝帧堆积</para>
/// <para>2. 非阻塞入队+主动丢帧:渲染慢时直接丢弃新帧,确保主线程不阻塞</para>
/// <para>3. 引用计数联动:丢帧时立即释放引用,内存自动回池复用</para>
/// </summary>
public class FrameConsumer : IDisposable
{
#region --- (Private Resources & States) ---
/// <summary> 帧缓冲队列容量1仅存储最新一帧保证零延迟渲染 </summary>
/// <remarks> BlockingCollection 封装线程安全操作GetConsumingEnumerable 支持取消令牌 </remarks>
private readonly BlockingCollection<SmartFrame> _frameBuffer = new(1);
/// <summary> 取消令牌源:用于终止渲染循环 </summary>
private readonly CancellationTokenSource _cts = new();
/// <summary> 后台渲染任务 </summary>
private Task? _renderTask;
/// <summary> OpenCV 窗口名称 </summary>
private readonly string _windowName;
#endregion
#region --- (Constructor & Lifecycle) ---
/// <summary>
/// 初始化帧渲染消费者
/// </summary>
/// <param name="windowName">OpenCV 显示窗口名称</param>
public FrameConsumer(string windowName = "Zero Latency Preview")
{
_windowName = windowName;
}
/// <summary>
/// 启动渲染线程
/// </summary>
public void Start()
{
// 防止重复启动
if (_renderTask != null) return;
// 启动长期运行的渲染任务,提升线程调度优先级
_renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning);
Console.WriteLine($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}");
}
/// <summary>
/// 停止渲染线程并清理资源
/// </summary>
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) ---
/// <summary>
/// [生产端入口] 接收帧并尝试入队(非阻塞)
/// </summary>
/// <param name="frame">待渲染的智能帧</param>
public void Enqueue(SmartFrame frame)
{
// 防护:线程已停止,直接释放帧引用
if (_cts.IsCancellationRequested)
{
frame.Dispose();
return;
}
// 核心零延迟策略:非阻塞尝试入队
// 队列满 → 上一帧未渲染完成 → 丢弃当前帧,释放引用
if (!_frameBuffer.TryAdd(frame))
{
frame.Dispose();
// Debug.WriteLine($"[Drop] 渲染线程[{_windowName}]处理过慢,丢弃一帧");
}
// 入队成功 → 帧由队列托管,等待渲染线程消费
}
/// <summary>
/// 后台渲染循环(核心逻辑)
/// </summary>
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) ---
/// <summary>
/// 释放所有资源
/// </summary>
public void Dispose()
{
Stop();
_frameBuffer.Dispose();
_cts.Dispose();
}
#endregion
}