2026-01-16 14:30:42 +08:00
|
|
|
|
using Ayay.SerilogLogs;
|
|
|
|
|
|
using OpenCvSharp;
|
|
|
|
|
|
using Serilog;
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
|
|
|
|
|
namespace SHH.CameraSdk;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [消费者] 专用渲染线程(零延迟设计)
|
|
|
|
|
|
/// 核心策略:
|
|
|
|
|
|
/// <para>1. 容量为1的阻塞队列:仅保留最新一帧,杜绝帧堆积</para>
|
|
|
|
|
|
/// <para>2. 非阻塞入队+主动丢帧:渲染慢时直接丢弃新帧,确保主线程不阻塞</para>
|
|
|
|
|
|
/// <para>3. 引用计数联动:丢帧时立即释放引用,内存自动回池复用</para>
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class FrameConsumer : IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
#region --- 私有资源与状态 (Private Resources & States) ---
|
|
|
|
|
|
|
2026-01-16 14:30:42 +08:00
|
|
|
|
private static ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
|
|
|
|
|
|
2025-12-26 03:18:21 +08:00
|
|
|
|
/// <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);
|
2026-01-16 14:30:42 +08:00
|
|
|
|
_sysLog.Information($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}");
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 14:30:42 +08:00
|
|
|
|
_sysLog.Information($"[Consumer] 渲染线程已停止,窗口: {_windowName}");
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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()
|
|
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
bool isWindowCreated = false;
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
// 我们不再使用简单的 foreach 阻塞等待数据,
|
|
|
|
|
|
// 而是改用非阻塞模式或带有超时的读取,以保证 WaitKey 的活性
|
|
|
|
|
|
while (!_cts.Token.IsCancellationRequested)
|
2025-12-26 03:18:21 +08:00
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
// 尝试在 30ms 内获取一帧数据(相当于 33 fps 的响应速度)
|
|
|
|
|
|
if (_frameBuffer.TryTake(out var frame, 30, _cts.Token))
|
2025-12-26 03:18:21 +08:00
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
try
|
2025-12-26 03:18:21 +08:00
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
if (frame.InternalMat != null && !frame.InternalMat.IsDisposed)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!isWindowCreated)
|
|
|
|
|
|
{
|
|
|
|
|
|
Cv2.NamedWindow(_windowName, WindowFlags.Normal);
|
|
|
|
|
|
isWindowCreated = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Cv2.ImShow(_windowName, frame.InternalMat);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
frame.Dispose();
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-26 18:55:04 +08:00
|
|
|
|
|
|
|
|
|
|
// 【核心修复】无论有没有取到帧,都要执行 WaitKey
|
|
|
|
|
|
// 只有这样,窗口在没视频时才能被拖动、最小化或手动点击 X 关闭
|
|
|
|
|
|
if (isWindowCreated)
|
2025-12-26 03:18:21 +08:00
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
// 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; }
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-26 18:55:04 +08:00
|
|
|
|
catch (OperationCanceledException) { }
|
|
|
|
|
|
catch { }
|
2025-12-26 03:18:21 +08:00
|
|
|
|
finally
|
|
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
if (isWindowCreated)
|
|
|
|
|
|
{
|
|
|
|
|
|
Cv2.DestroyWindow(_windowName);
|
|
|
|
|
|
}
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 资源释放 (Disposal) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 释放所有资源
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
|
|
|
|
|
Stop();
|
|
|
|
|
|
_frameBuffer.Dispose();
|
|
|
|
|
|
_cts.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|