海康摄像头取流示例初始签入

This commit is contained in:
2025-12-26 03:18:21 +08:00
parent 86db2cf6b4
commit 6281f4248e
44 changed files with 5588 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
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()
{
// 创建 OpenCV 显示窗口
Cv2.NamedWindow(_windowName, WindowFlags.Normal);
try
{
// 阻塞遍历队列:队列空时等待,收到取消信号时退出
foreach (var frame in _frameBuffer.GetConsumingEnumerable(_cts.Token))
{
try
{
// 渲染有效性校验Mat 未释放且不为空
if (frame.InternalMat != null && !frame.InternalMat.IsDisposed)
{
// 零拷贝渲染:直接引用 InternalMat无内存复制开销
Cv2.ImShow(_windowName, frame.InternalMat);
// 1ms 等待 UI 事件响应(必须调用,否则窗口无响应)
Cv2.WaitKey(1);
}
}
catch (Exception ex)
{
Debug.WriteLine($"[RenderError] 窗口[{_windowName}]渲染失败: {ex.Message}");
}
finally
{
// 至关重要:渲染完成后释放帧引用
// 引用计数归零 → 帧自动回池复用,避免内存泄漏
frame.Dispose();
}
}
}
catch (OperationCanceledException)
{
// 正常取消,无需处理
Debug.WriteLine($"[RenderInfo] 窗口[{_windowName}]渲染循环已取消");
}
finally
{
// 销毁 OpenCV 窗口,释放 UI 资源
Cv2.DestroyWindow(_windowName);
}
}
#endregion
#region --- (Disposal) ---
/// <summary>
/// 释放所有资源
/// </summary>
public void Dispose()
{
Stop();
_frameBuffer.Dispose();
_cts.Dispose();
}
#endregion
}

View File

@@ -0,0 +1,92 @@
namespace SHH.CameraSdk;
/// <summary>
/// 截图协调器(单例模式)
/// 功能:桥接 API 线程的截图请求与 SDK 线程的帧数据推送,实现异步截图功能
/// 核心机制:基于 TaskCompletionSource 实现跨线程通信,支持超时控制与自动清理
/// 线程安全:全量使用并发容器,无锁设计,支持多设备同时请求截图
/// </summary>
public class SnapshotCoordinator
{
#region --- 1. (Singleton Implementation) ---
/// <summary> 全局唯一实例 </summary>
public static SnapshotCoordinator Instance { get; } = new();
/// <summary> 私有构造函数:禁止外部实例化,确保单例特性 </summary>
private SnapshotCoordinator() { }
#endregion
#region --- 2. (Private Task Pool) ---
/// <summary> 待处理截图请求任务池(线程安全) </summary>
/// <remarks> Key = 设备IDValue = 任务完成源,用于传递截图结果或超时信号 </remarks>
private readonly ConcurrentDictionary<long, TaskCompletionSource<byte[]>> _pendingRequests = new();
#endregion
#region --- 3. API 线 (API Thread Interface) ---
/// <summary>
/// API 线程调用:申请指定设备的截图并异步等待结果
/// </summary>
/// <param name="deviceId">目标设备ID</param>
/// <param name="timeoutMs">超时时间(默认 3000ms</param>
/// <returns>截图字节数组JPEG/PNG 格式)/ 超时返回 null</returns>
public async Task<byte[]?> RequestSnapshotAsync(long deviceId, int timeoutMs = 3000)
{
// 1. 创建任务完成源,用于接收 SDK 线程的截图数据
var tcs = new TaskCompletionSource<byte[]>();
// 2. 注册待处理请求到任务池
_pendingRequests[deviceId] = tcs;
try
{
// 3. 配置超时控制:超时后自动取消任务
using var cts = new CancellationTokenSource(timeoutMs);
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
// 4. 等待 SDK 线程推送截图数据
return await tcs.Task.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// 超时异常:返回 null 表示截图失败
return null;
}
finally
{
// 5. 无论成功/超时,最终从任务池移除请求,防止内存泄漏
_pendingRequests.TryRemove(deviceId, out _);
}
}
#endregion
#region --- 4. SDK 线 (SDK Thread Interface) ---
/// <summary>
/// SDK 线程调用:检查指定设备是否存在待处理的截图请求
/// </summary>
/// <param name="deviceId">目标设备ID</param>
/// <returns>存在待处理请求返回 true否则返回 false</returns>
public bool HasRequest(long deviceId) => _pendingRequests.ContainsKey(deviceId);
/// <summary>
/// SDK 线程调用:提交指定设备的截图数据,完成待处理请求
/// </summary>
/// <param name="deviceId">目标设备ID</param>
/// <param name="data">截图字节数组JPEG/PNG 格式)</param>
public void ProvideSnapshot(long deviceId, byte[] data)
{
// 检查是否存在待处理请求,存在则推送数据并完成任务
if (_pendingRequests.TryGetValue(deviceId, out var tcs))
{
tcs.TrySetResult(data);
}
}
#endregion
}

View File

@@ -0,0 +1,233 @@
namespace SHH.CameraSdk;
/// <summary>
/// [管理层] 视频源总控管理器 (V3.3.1 修复版)
/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈
/// 核心修复:
/// <para>1. [Bug γ] 二次伤害:强化销毁流程,防止 Dispose 阶段因 GC 乱序导致的非托管内存非法访问</para>
/// <para>2. [Bug A/L] 继承之前的动态感知与末日销毁协同修复,保障多线程环境下的状态一致性</para>
/// </summary>
public class CameraManager : IDisposable, IAsyncDisposable
{
#region --- 1. (Fields & States) ---
/// <summary> 全局设备实例池线程安全Key = 设备唯一标识 </summary>
private readonly ConcurrentDictionary<long, BaseVideoSource> _cameraPool = new();
/// <summary> 后台协调器实例:负责心跳检测、断线重连、僵尸流恢复 </summary>
private readonly CameraCoordinator _coordinator = new();
/// <summary> 全局取消令牌源:用于销毁时瞬间关停所有异步扫描任务 </summary>
private readonly CancellationTokenSource _globalCts = new();
/// <summary> 销毁状态标记:防止重复销毁或销毁过程中执行操作 </summary>
private volatile bool _isDisposed;
/// <summary>
/// [Fix Bug A: 动态失效] 协调器引擎运行状态标记
/// 使用 volatile 关键字确保多线程环境下的内存可见性,避免指令重排导致的状态不一致
/// </summary>
private volatile bool _isEngineStarted = false;
#endregion
#region --- 2. (Device Management) ---
/// <summary>
/// 向管理池添加新相机设备
/// </summary>
/// <param name="config">相机设备配置信息</param>
public void AddDevice(VideoSourceConfig config)
{
// [安全防护] 销毁过程中禁止添加新设备
if (_isDisposed) return;
// 防止重复添加同一设备
if (_cameraPool.ContainsKey(config.Id)) return;
// 1. 根据设备品牌实例化对应的驱动实现类
BaseVideoSource source = config.Brand switch
{
DeviceBrand.HikVision => new HikVideoSource(config),
_ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}")
};
// 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
if (_isEngineStarted)
{
source.IsRunning = true;
}
// 3. 将设备注册到内存池与协调器,纳入统一管理
if (_cameraPool.TryAdd(config.Id, source))
{
_coordinator.Register(source);
}
}
/// <summary>
/// 根据设备ID获取指定的视频源实例
/// </summary>
/// <param name="id">设备唯一标识</param>
/// <returns>视频源实例 / 不存在则返回 null</returns>
public BaseVideoSource? GetDevice(long id)
=> _cameraPool.TryGetValue(id, out var source) ? source : null;
#endregion
#region --- 3. (Engine Lifecycle) ---
/// <summary>
/// 启动视频管理引擎初始化SDK并启动协调器自愈循环
/// </summary>
public async Task StartAsync()
{
// 防护:已销毁则抛出异常
if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager));
// 防护:避免重复启动
if (_isEngineStarted) return;
// 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境
HikSdkManager.Initialize();
// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程
foreach (var source in _cameraPool.Values)
{
source.IsRunning = true;
}
// 标记引擎启动状态,后续新增设备自动激活
_isEngineStarted = true;
// 3. 启动协调器后台自愈循环(标记为 LongRunning 提升调度优先级)
_ = Task.Factory.StartNew(
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
_globalCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
await Task.CompletedTask;
}
/// <summary>
/// 获取当前所有相机的全局状态简报
/// </summary>
/// <returns>包含设备ID、IP、运行状态的元组集合</returns>
public IEnumerable<(long Id, string Ip, VideoSourceStatus Status)> GetGlobalStatus()
{
return _cameraPool.Values.Select(v => (v.Id, v.Config.IpAddress, v.Status));
}
#endregion
#region --- 4. (Telemetry Collection) ---
/// <summary>
/// 获取所有相机的健康度报告
/// </summary>
/// <returns>相机健康度报告集合</returns>
public IEnumerable<CameraHealthReport> GetDetailedTelemetry()
{
return _cameraPool.Values.Select(cam => new CameraHealthReport
{
DeviceId = cam.Id,
Ip = cam.Config.IpAddress,
Status = cam.Status.ToString(),
LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常"
// 扩展:可补充 RealFps/DropFrames/ReconnectCount 等指标
});
}
/// <summary>
/// [新增] 获取全量相机实时遥测数据快照
/// 用于 WebAPI 实时监控大屏展示
/// </summary>
/// <returns>相机遥测数据快照集合</returns>
public IEnumerable<CameraTelemetryInfo> GetTelemetrySnapshot()
{
// 立即物化列表,防止枚举过程中集合被修改导致异常
return _cameraPool.Values.Select(cam =>
{
// 健康度评分算法(示例):基于设备状态与实时帧率综合判定
int healthScore = 100;
if (cam.Status == VideoSourceStatus.Faulted)
healthScore = 0;
else if (cam.Status == VideoSourceStatus.Reconnecting)
healthScore = 60;
else if (cam.RealFps < 1.0 && cam.Status == VideoSourceStatus.Playing)
healthScore = 40; // 有连接状态但无有效流
return new CameraTelemetryInfo
{
DeviceId = cam.Id,
Name = cam.Config.Name,
IpAddress = cam.Config.IpAddress,
Status = cam.Status.ToString(),
IsOnline = cam.IsOnline,
Fps = cam.RealFps,
TotalFrames = cam.TotalFrames,
HealthScore = healthScore,
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
Timestamp = DateTime.Now
};
}).ToList();
}
#endregion
#region --- 5. (Disposal) ---
/// <summary>
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成
/// </summary>
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
/// <summary>
/// [修复 Bug L & Bug γ] 异步执行全局资源清理
/// 严格遵循销毁顺序:停止任务 → 销毁设备 → 卸载SDK防止非托管内存泄漏
/// </summary>
public async ValueTask DisposeAsync()
{
// 防护:避免重复销毁
if (_isDisposed) return;
// 标记为已销毁,禁止后续操作
_isDisposed = true;
_isEngineStarted = false;
try
{
// 1. 发送全局取消信号,立即停止协调器所有后台扫描任务
_globalCts.Cancel();
// 2. [Fix Bug L] 锁定设备池快照并清空,防止并发修改导致异常
var devices = _cameraPool.Values.ToArray();
_cameraPool.Clear();
// 3. 并行销毁所有相机设备,释放设备持有的非托管资源
var disposeTasks = devices.Select(async device =>
{
try { await device.DisposeAsync(); }
catch { /* 隔离单个设备销毁异常,不影响其他设备 */ }
});
await Task.WhenAll(disposeTasks);
// 4. [Fix Bug γ: 二次伤害] 彻底卸载全局 SDK 环境
// 加 try-catch 防护极端场景(如进程强制终止时 SDK 已被系统回收)
try
{
HikSdkManager.Uninitialize();
}
catch
{
// 忽略卸载异常,保证销毁流程正常结束
}
}
finally
{
// 释放取消令牌源资源
_globalCts.Dispose();
}
}
#endregion
}

View File

@@ -0,0 +1,140 @@
using OpenCvSharp;
namespace SHH.CameraSdk;
/// <summary>
/// [零延迟核心] 智能帧对象池
/// 功能:预分配并复用 SmartFrame 实例,杜绝频繁 new Mat() 与 GC 回收,消除内存分配停顿
/// 核心策略:
/// <para>1. 预热分配:启动时创建初始数量帧,避免运行时内存申请</para>
/// <para>2. 上限控制:最大池大小限制内存占用,防止内存溢出</para>
/// <para>3. 背压丢帧:池空时返回 null强制丢帧保证实时性不阻塞生产端</para>
/// </summary>
public class FramePool : IDisposable
{
#region --- (Private Resources & Configurations) ---
/// <summary> 可用帧队列(线程安全):存储待借出的空闲智能帧 </summary>
private readonly ConcurrentQueue<SmartFrame> _availableFrames = new();
/// <summary> 所有已分配帧列表:用于统一销毁释放内存 </summary>
private readonly List<SmartFrame> _allAllocatedFrames = new();
/// <summary> 创建新帧锁:确保多线程下创建新帧的线程安全 </summary>
private readonly object _lock = new();
/// <summary> 帧宽度(与相机输出分辨率一致) </summary>
private readonly int _width;
/// <summary> 帧高度(与相机输出分辨率一致) </summary>
private readonly int _height;
/// <summary> 帧数据类型(如 CV_8UC3 对应 RGB 彩色图像) </summary>
private readonly MatType _type;
/// <summary> 池最大容量:限制最大分配帧数,防止内存占用过高 </summary>
private readonly int _maxPoolSize;
#endregion
#region --- (Constructor & Warm-Up) ---
/// <summary>
/// 初始化智能帧对象池
/// </summary>
/// <param name="width">帧宽度</param>
/// <param name="height">帧高度</param>
/// <param name="type">帧数据类型</param>
/// <param name="initialSize">初始预热帧数默认5</param>
/// <param name="maxSize">池最大容量默认10</param>
public FramePool(int width, int height, MatType type, int initialSize = 5, int maxSize = 10)
{
_width = width;
_height = height;
_type = type;
_maxPoolSize = maxSize;
// 预热:启动时预分配初始数量帧,避免运行时动态申请内存
for (int i = 0; i < initialSize; i++)
{
CreateNewFrame();
}
}
/// <summary>
/// 创建新智能帧并加入池(内部调用,加锁保护)
/// </summary>
private void CreateNewFrame()
{
var frame = new SmartFrame(this, _width, _height, _type);
_allAllocatedFrames.Add(frame);
_availableFrames.Enqueue(frame);
}
#endregion
#region --- (Frame Borrow & Return) ---
/// <summary>
/// 从池借出一个智能帧O(1) 时间复杂度)
/// </summary>
/// <returns>可用智能帧 / 池空且达上限时返回 null触发背压丢帧</returns>
public SmartFrame Get()
{
// 1. 优先从可用队列取帧,无锁快速路径
if (_availableFrames.TryDequeue(out var frame))
{
frame.Activate();
return frame;
}
// 2. 可用队列为空,检查是否达最大容量
if (_allAllocatedFrames.Count < _maxPoolSize)
{
// 加锁创建新帧,避免多线程重复创建
lock (_lock)
{
// 双重检查:防止等待锁期间其他线程已创建新帧
if (_allAllocatedFrames.Count < _maxPoolSize)
{
CreateNewFrame();
}
}
// 递归重试取帧
return Get();
}
// 3. 背压策略:池空且达上限,返回 null 强制丢帧,保证生产端不阻塞
// 适用场景:消费端处理过慢导致帧堆积,丢帧保实时性
return null;
}
/// <summary>
/// [系统内部调用] 将帧归还至池(由 SmartFrame.Dispose 自动触发)
/// </summary>
/// <param name="frame">待归还的智能帧</param>
public void Return(SmartFrame frame)
{
_availableFrames.Enqueue(frame);
}
#endregion
#region --- (Resource Disposal) ---
/// <summary>
/// 释放帧池所有资源,销毁所有 Mat 内存
/// </summary>
public void Dispose()
{
// 遍历所有已分配帧,释放 OpenCV Mat 底层内存
foreach (var frame in _allAllocatedFrames)
{
frame.InternalMat.Dispose();
}
_allAllocatedFrames.Clear();
_availableFrames.Clear();
}
#endregion
}

View File

@@ -0,0 +1,95 @@
using OpenCvSharp;
namespace SHH.CameraSdk;
/// <summary>
/// [零延迟核心] 智能帧(内存复用+引用计数)
/// 功能:封装 OpenCV Mat 实现物理内存复用,通过引用计数管理生命周期,避免 GC 频繁回收导致的性能抖动
/// 特性:引用归零自动回池,全程无内存分配/释放开销,支撑高帧率实时流处理
/// </summary>
public class SmartFrame : IDisposable
{
#region --- (Private Resources & States) ---
/// <summary> 所属帧池:用于引用归零后自动回收复用 </summary>
private readonly FramePool _pool;
/// <summary> 引用计数器:线程安全,控制帧的生命周期 </summary>
/// <remarks> 初始值 0激活后设为 1引用归零则自动回池 </remarks>
private int _refCount = 0;
#endregion
#region --- (Public Properties) ---
/// <summary> 帧数据物理内存载体OpenCV Mat 对象) </summary>
/// <remarks> 内存由帧池预分配,全程复用,不触发 GC </remarks>
public Mat InternalMat { get; private set; }
/// <summary> 帧激活时间戳(记录帧被取出池的时刻) </summary>
public DateTime Timestamp { get; private set; }
#endregion
#region --- (Constructor & Activation) ---
/// <summary>
/// 初始化智能帧(由帧池调用,外部禁止直接实例化)
/// </summary>
/// <param name="pool">所属帧池实例</param>
/// <param name="width">帧宽度</param>
/// <param name="height">帧高度</param>
/// <param name="type">帧数据类型(如 MatType.CV_8UC3</param>
internal SmartFrame(FramePool pool, int width, int height, MatType type)
{
_pool = pool;
// 预分配物理内存:内存块在帧池生命周期内复用,避免频繁申请/释放
InternalMat = new Mat(height, width, type);
}
/// <summary>
/// [生产者调用] 从帧池取出时激活帧
/// 功能:初始化引用计数,标记激活时间戳
/// </summary>
public void Activate()
{
// 激活后引用计数设为 1代表生产者驱动/管道)持有该帧
_refCount = 1;
// 记录帧被取出池的时间,用于后续延迟计算
Timestamp = DateTime.Now;
}
#endregion
#region --- (Reference Count Management) ---
/// <summary>
/// [消费者调用] 增加引用计数
/// 适用场景:帧需要被多模块同时持有(如同时分发到 UI 和 AI 分析)
/// </summary>
public void AddRef()
{
// 原子递增:线程安全,避免多线程竞争导致计数错误
Interlocked.Increment(ref _refCount);
}
#endregion
#region --- (Disposal & Pool Return) ---
/// <summary>
/// [消费者调用] 释放引用计数
/// 核心逻辑:引用归零后自动将帧归还至帧池,实现内存复用
/// </summary>
public void Dispose()
{
// 原子递减:线程安全,确保计数准确
if (Interlocked.Decrement(ref _refCount) <= 0)
{
// 引用归零:所有消费者均已释放,将帧归还池复用
_pool.Return(this);
}
}
#endregion
}

View File

@@ -0,0 +1,64 @@
using SHH.CameraSdk;
/// <summary>
/// 全局帧处理中心(静态类)
/// 功能:接收驱动层的帧数据与决策,封装为处理任务并投递至处理管道,是驱动层与处理管道的桥梁
/// 核心修复:初始化全链路追踪上下文并绑定到任务,避免空引用异常;管道满时记录丢弃日志并释放帧资源
/// </summary>
public static class GlobalProcessingCenter
{
#region --- (Private Resources) ---
/// <summary> 全局帧处理管道实例 </summary>
/// <remarks> 固定容量 50采用 DropWrite 模式,生产端永不阻塞 </remarks>
private static readonly ProcessingPipeline _pipeline = new ProcessingPipeline(capacity: 50);
#endregion
#region --- (Core Task Submission) ---
/// <summary>
/// 提交帧数据与决策到处理中心
/// 功能:封装帧为处理任务,初始化追踪上下文,投递至管道;投递失败时记录丢弃日志并释放资源
/// </summary>
/// <param name="deviceId">产生帧的设备唯一标识</param>
/// <param name="frame">待处理的智能帧数据</param>
/// <param name="decision">帧处理决策(包含是否保留、分发目标等信息)</param>
public static void Submit(long deviceId, SmartFrame frame, FrameDecision decision)
{
// 1. 初始化全链路追踪上下文:绑定决策信息,记录帧进入处理中心的初始日志
var context = new FrameContext
{
FrameSequence = decision.Sequence,
Timestamp = decision.Timestamp,
IsCaptured = true,
TargetAppIds = decision.TargetAppIds // 记录帧的分发目标列表
};
// 添加初始日志:标记帧由驱动层提交至处理中心
context.AddLog("Driver: Submitted to Global Center");
// 2. 封装为处理任务关联设备ID、帧数据、决策、追踪上下文
var task = new ProcessingTask
{
DeviceId = deviceId,
Frame = frame,
Decision = decision,
Context = context // 绑定上下文,修复空引用问题
};
// 3. 尝试投递任务到处理管道
if (!_pipeline.TrySubmit(task))
{
// 投递失败:管道已满,记录丢弃原因并更新上下文状态
context.DropReason = "GlobalPipelineFull";
context.IsCaptured = false;
// 归档丢弃日志到全局遥测,用于问题排查
GlobalTelemetry.RecordLog(decision.Sequence, context);
// 释放帧资源:避免内存泄漏
frame.Dispose();
}
}
#endregion
}

View File

@@ -0,0 +1,157 @@
namespace SHH.CameraSdk;
/// <summary>
/// 全局流分发器(静态类 | 线程安全)
/// 核心职责:
/// <para>1. 接收处理完成的帧任务,基于 AppId 路由策略实现帧的精准定向分发</para>
/// <para>2. 隔离 UI 预览、AI 分析等不同消费场景,支撑多模块并行消费</para>
/// 设计特性:
/// <para>✅ 线程安全:基于 ConcurrentDictionary 实现并发订阅/取消订阅</para>
/// <para>✅ 精准路由:按 TargetAppIds 点对点投递,避免广播风暴</para>
/// <para>✅ 异常隔离:单个订阅者异常不影响其他模块消费</para>
/// </summary>
public static class GlobalStreamDispatcher
{
#region --- 1. (Predefined Subscription Channels) ---
/// <summary>
/// UI 预览订阅通道:供 UI 模块订阅帧数据,用于实时画面显示
/// 回调参数:(设备唯一标识, 处理后的智能帧数据)
/// 特性:低延迟优先,支持画面渲染相关的轻量级处理
/// </summary>
public static event Action<long, SmartFrame>? OnPreviewFrame;
/// <summary>
/// AI 分析订阅通道:供 AI 模块订阅帧数据,用于行为识别/人脸检测/车牌识别等
/// 回调参数:(设备唯一标识, 处理后的智能帧数据)
/// 特性:高吞吐优先,支持复杂算法处理,延迟容忍度较高
/// </summary>
public static event Action<long, SmartFrame>? OnAnalysisFrame;
#endregion
#region --- 2. (Dynamic Routing Table) ---
/// <summary>
/// 动态订阅路由表Key = 业务 AppIdValue = 帧处理多播委托
/// 实现ConcurrentDictionary 保证高并发场景下的读写安全
/// 用途:支持自定义业务模块的精准订阅,扩展帧消费能力
/// </summary>
private static readonly ConcurrentDictionary<string, Action<long, SmartFrame>> _routingTable = new();
#endregion
#region --- 3. (Subscription Management API) ---
/// <summary>
/// 精准订阅:为指定 AppId 注册帧处理回调
/// 线程安全:支持多线程并发调用,委托自动合并(多播)
/// </summary>
/// <param name="appId">业务唯一标识(需与 FrameController.Register 中的 AppId 一致)</param>
/// <param name="handler">帧处理回调函数</param>
/// <exception cref="ArgumentNullException">appId 或 handler 为空时抛出</exception>
public static void Subscribe(string appId, Action<long, SmartFrame> handler)
{
// 入参合法性校验
if (string.IsNullOrWhiteSpace(appId))
throw new ArgumentNullException(nameof(appId), "AppId 不能为空");
if (handler == null)
throw new ArgumentNullException(nameof(handler), "帧处理回调不能为空");
// 线程安全添加/更新委托:新订阅追加,重复订阅合并
_routingTable.AddOrUpdate(
key: appId,
addValue: handler,
updateValueFactory: (_, existingHandler) => existingHandler + handler
);
}
/// <summary>
/// 取消订阅:移除指定 AppId 的帧处理回调
/// 线程安全:支持多线程并发调用,无订阅时静默处理
/// </summary>
/// <param name="appId">业务唯一标识</param>
/// <param name="handler">需要移除的帧处理回调</param>
public static void Unsubscribe(string appId, Action<long, SmartFrame> handler)
{
if (string.IsNullOrWhiteSpace(appId) || handler == null)
return;
// 尝试获取当前委托并移除目标回调
if (_routingTable.TryGetValue(appId, out var currentHandler))
{
var updatedHandler = currentHandler - handler;
if (updatedHandler == null)
{
// 委托为空时移除路由项,避免内存泄漏
_routingTable.TryRemove(appId, out _);
}
else
{
// 委托非空时更新路由表
_routingTable.TryUpdate(appId, updatedHandler, currentHandler);
}
}
}
#endregion
#region --- 4. (Core Dispatch Logic) ---
/// <summary>
/// 帧任务分发入口:基于任务的 TargetAppIds 实现精准点对点投递
/// 核心优化:摒弃广播模式,仅投递到指定订阅者,降低系统资源消耗
/// </summary>
/// <param name="task">处理完成的帧任务(包含目标 AppId 列表、帧数据、上下文)</param>
/// <exception cref="ArgumentNullException">task 为空时抛出</exception>
public static void Dispatch(ProcessingTask task)
{
// 入参合法性校验
if (task == null)
throw new ArgumentNullException(nameof(task), "帧任务不能为空");
var deviceId = task.DeviceId;
var frame = task.Frame;
var targetAppIds = task.Decision.TargetAppIds;
var sequence = task.Decision.Sequence;
// 记录分发日志
task.Context.AddLog($"开始分发帧任务 [Seq:{sequence}],目标 AppId 列表:[{string.Join(", ", targetAppIds)}]");
// 遍历目标 AppId 列表,执行精准投递
foreach (var appId in targetAppIds)
{
// 1. 优先匹配动态路由表中的自定义订阅者
if (_routingTable.TryGetValue(appId, out var customHandler))
{
try
{
customHandler.Invoke(deviceId, frame);
task.Context.AddLog($"帧任务 [Seq:{sequence}] 成功投递到自定义 AppId: {appId}");
}
catch (Exception ex)
{
// 单个订阅者异常隔离,不影响其他分发流程
task.Context.AddLog($"帧任务 [Seq:{sequence}] 投递到 AppId:{appId} 失败:{ex.Message}");
Console.WriteLine($"[DispatchError] AppId={appId}, DeviceId={deviceId}, Error={ex.Message}");
}
}
// 2. 匹配预设的全局通道(兼容旧版订阅逻辑)
switch (appId.ToUpperInvariant())
{
case "UI_PREVIEW":
OnPreviewFrame?.Invoke(deviceId, frame);
break;
case "AI_ANALYSIS":
OnAnalysisFrame?.Invoke(deviceId, frame);
break;
}
}
// 分发完成后记录遥测数据
GlobalTelemetry.RecordLog(sequence, task.Context);
}
#endregion
}

View File

@@ -0,0 +1,148 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧处理管道(后台处理核心)
/// 功能:接收帧处理任务,在后台单线程执行二次处理(如打水印、裁剪),并分发至目标订阅者
/// 核心特性:
/// <para>1. 有界通道+DropWrite模式生产端永不阻塞管道满时丢弃新任务避免内存积压</para>
/// <para>2. 单线程处理CPU占用恒定避免多线程竞争导致的性能抖动</para>
/// <para>3. 引用计数管理:确保帧数据安全转移与释放,防止内存泄漏</para>
/// </summary>
public class ProcessingPipeline
{
#region --- (Private Resources & States) ---
/// <summary> 任务队列(有界通道):存储待处理的帧任务 </summary>
private readonly Channel<ProcessingTask> _queue;
/// <summary> 取消令牌源:用于终止后台处理循环 </summary>
private readonly CancellationTokenSource _cts = new();
#endregion
#region --- (Constructor & Initialization) ---
/// <summary>
/// 初始化帧处理管道
/// </summary>
/// <param name="capacity">管道最大容量超过该值时新任务将被丢弃DropWrite模式</param>
public ProcessingPipeline(int capacity)
{
// 创建有界通道,配置核心特性
_queue = Channel.CreateBounded<ProcessingTask>(new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.DropWrite, // 管道满时丢弃新写入的任务
SingleReader = true, // 单线程读取保证处理顺序与CPU稳定性
SingleWriter = false // 支持多线程写入(如多相机同时提交任务)
});
// 启动后台处理循环(长期运行任务,标记为 LongRunning 提升调度优先级)
Task.Factory.StartNew(ProcessLoopAsync, TaskCreationOptions.LongRunning);
}
#endregion
#region --- (Task Submission) ---
/// <summary>
/// 尝试提交帧处理任务到管道
/// 核心逻辑:非阻塞提交,失败时回滚帧引用计数,避免内存泄漏
/// </summary>
/// <param name="task">待处理的帧任务(包含帧数据、决策、追踪上下文)</param>
/// <returns>提交成功返回 true管道满导致提交失败返回 false</returns>
public bool TrySubmit(ProcessingTask task)
{
// 1. 帧引用计数+1将帧所有权从生产端转移到管道后台线程
task.Frame.AddRef();
try
{
// 2. 非阻塞写入管道:成功则任务进入队列等待处理
if (_queue.Writer.TryWrite(task))
{
return true;
}
// 3. 写入失败(管道满):回滚引用计数,释放帧内存
task.Frame.Dispose();
return false;
}
catch
{
// 异常场景下同样回滚引用计数,确保资源释放
task.Frame.Dispose();
return false;
}
}
#endregion
#region --- (Background Processing Loop) ---
/// <summary>
/// 后台处理循环:持续读取队列任务,执行二次处理与分发
/// </summary>
private async Task ProcessLoopAsync()
{
try
{
// 异步遍历队列:收到取消信号时退出循环
await foreach (var task in _queue.Reader.ReadAllAsync(_cts.Token))
{
// 使用 using 语句:处理完成后自动调用 Frame.Dispose(),引用计数-1
using (task.Frame)
{
// 执行具体的帧处理逻辑
ExecuteProcessing(task);
}
}
}
catch (OperationCanceledException)
{
// 收到取消信号,正常退出循环,无需处理
}
}
#endregion
#region --- (Frame Processing Execution) ---
/// <summary>
/// 执行帧二次处理与分发
/// 功能:对帧进行自定义加工(如打水印、格式转换),并通过分发器发送至目标订阅者
/// </summary>
/// <param name="task">待处理的帧任务</param>
private void ExecuteProcessing(ProcessingTask task)
{
try
{
// --- 二次处理车间可添加自定义加工逻辑10ms-50ms 耗时操作安全) ---
// 示例:给帧添加序列号水印(按需启用)
// string watermarkText = $"SEQ:{task.Decision.Sequence}";
// Cv2.PutText(
// img: task.Frame.InternalMat,
// text: watermarkText,
// org: new Point(10, 50),
// fontFace: HersheyFonts.HersheySimplex,
// fontScale: 1,
// color: Scalar.Red,
// thickness: 2
// );
// --- 帧分发:将处理后的帧交给全局分发器,按决策分发至目标订阅者 ---
GlobalStreamDispatcher.Dispatch(task);
}
catch (Exception ex)
{
// 捕获处理过程中的异常,避免影响后续任务执行
Console.WriteLine($"[PipelineError] 帧处理失败 (DeviceId: {task.DeviceId}, Seq: {task.Decision.Sequence}): {ex.Message}");
}
finally
{
// 归档追踪日志:将帧处理上下文存入全局遥测,支持后续排查与分析
GlobalTelemetry.RecordLog(task.Decision.Sequence, task.Context);
}
}
#endregion
}

View File

@@ -0,0 +1,40 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧处理任务模型
/// 功能:封装单帧数据的处理任务信息,包含帧数据、分发决策、全链路追踪上下文,是帧处理管道的核心数据载体
/// 用途:在全局处理中心与分发器之间传递,串联帧的二次处理、分发与追踪流程
/// </summary>
public class ProcessingTask
{
#region --- (Task Core Identification) ---
/// <summary> 设备唯一标识关联产生该帧的相机设备ID </summary>
public long DeviceId { get; set; }
#endregion
#region --- (Frame Core Data) ---
/// <summary> 待处理的智能帧数据(包含原始图像数据与引用计数管理) </summary>
/// <remarks> 非空约束:任务必须关联有效帧数据,不可为 null </remarks>
public SmartFrame Frame { get; set; } = null!;
#endregion
#region --- (Frame Distribution Decision) ---
/// <summary> 帧处理决策信息包含是否保留帧、分发目标AppId列表等 </summary>
/// <remarks> 非空约束:任务必须携带决策信息,指导后续分发逻辑 </remarks>
public FrameDecision Decision { get; set; } = null!;
#endregion
#region --- (Full-Link Tracing Context) ---
/// <summary> 帧全链路追踪上下文(用于记录帧处理过程中的日志、耗时、状态等信息) </summary>
/// <remarks> 非空约束:支持通过 AddLog 方法补充追踪日志,支撑问题排查 </remarks>
public FrameContext Context { get; set; } = null!;
#endregion
}

View File

@@ -0,0 +1,231 @@
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace SHH.CameraSdk;
/// <summary>
/// [调度协调层] 视频自愈调度器 (V3.3.4 流量削峰版)
/// 核心职责:监控所有相机设备的运行状态,实现断线自动重连、僵死状态复位,保障视频流稳定
/// 核心修复:
/// <para>1. [Bug τ] 线程池保护:引入并发节流阀,限制同时重连/探测的任务数,防止线程池饥饿</para>
/// </summary>
public class CameraCoordinator
{
#region --- (Private Resources & Configurations) ---
/// <summary> 已注册的相机设备集合(线程安全,支持并发添加与遍历) </summary>
private readonly ConcurrentBag<BaseVideoSource> _cameras = new();
/// <summary> 全局登录单行道锁 </summary>
/// <remarks> 限制同一时刻仅允许一个相机执行登录操作,避免 SDK 登录冲突 </remarks>
private readonly SemaphoreSlim _sdkLoginLock = new(1, 1);
/// <summary> 并发节流阀:限制同时进行探测/重连的任务数 </summary>
/// <remarks> 最大并发数 8避免百路相机同时重连导致 CPU 峰值过高、UI 卡顿 </remarks>
private readonly SemaphoreSlim _concurrencyLimiter = new(8);
/// <summary> 相机流存活判定阈值(秒):超过该时间无帧则判定为流中断 </summary>
private const int StreamAliveThresholdSeconds = 5;
/// <summary> Ping 探测超时时间(毫秒) </summary>
private const int PingTimeoutMs = 800;
/// <summary> TCP 探测超时时间(毫秒) </summary>
private const int TcpTimeoutMs = 1000;
/// <summary> 调度循环间隔(毫秒):每 5 秒执行一次全量设备状态校验 </summary>
private const int CoordinationLoopIntervalMs = 5000;
#endregion
#region --- (Camera Registration) ---
/// <summary>
/// 注册相机设备到调度器
/// 功能:将相机纳入全局状态监控与自愈管理
/// </summary>
/// <param name="camera">待注册的相机设备实例</param>
public void Register(BaseVideoSource camera) => _cameras.Add(camera);
#endregion
#region --- (Core Coordination Loop) ---
/// <summary>
/// 启动调度协调循环(长期运行任务)
/// 功能:周期性校验所有相机状态,执行自愈逻辑,支持取消
/// </summary>
/// <param name="token">取消令牌:用于终止调度循环</param>
public async Task RunCoordinationLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
// 削峰填谷式调度:通过并发节流阀控制任务并发数
var tasks = _cameras.Select(async cam =>
{
// 申请“重连/探测许可证”,无可用许可时阻塞等待
await _concurrencyLimiter.WaitAsync(token).ConfigureAwait(false);
try
{
// 安全执行状态调和(隔离单个相机的异常)
await SafeReconcileAsync(cam, token).ConfigureAwait(false);
}
finally
{
// 释放许可,允许其他相机执行任务
_concurrencyLimiter.Release();
}
});
// 等待所有相机的调和任务完成
await Task.WhenAll(tasks).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// 收到取消信号,退出循环
break;
}
catch (Exception ex)
{
// 捕获调度层全局异常,避免循环终止
Console.WriteLine($"[CoordinatorCritical] 调度循环异常: {ex.Message}");
}
try
{
// 等待下一个调度周期(支持响应取消)
await Task.Delay(CoordinationLoopIntervalMs, token).ConfigureAwait(false);
}
catch
{
// 延迟过程中收到取消信号,退出循环
break;
}
}
}
#endregion
#region --- (Safe Reconciliation Wrapper) ---
/// <summary>
/// 安全执行相机状态调和
/// 功能:隔离单个相机的异常,避免影响其他相机的调和逻辑
/// </summary>
/// <param name="cam">待调和的相机设备</param>
/// <param name="token">取消令牌</param>
private async Task SafeReconcileAsync(BaseVideoSource cam, CancellationToken token)
{
try
{
await ReconcileAsync(cam, token).ConfigureAwait(false);
}
catch
{
// 吞没单个相机的异常,确保其他相机正常调度
}
}
#endregion
#region --- (Reconciliation Logic) ---
/// <summary>
/// 相机状态调和(核心自愈逻辑)
/// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性
/// </summary>
/// <param name="cam">待调和的相机设备</param>
/// <param name="token">取消令牌</param>
private async Task ReconcileAsync(BaseVideoSource cam, CancellationToken token)
{
// 1. 计算距离上次收到帧的时间(秒)
long nowTick = Environment.TickCount64;
double secondsSinceLastFrame = (nowTick - cam.LastFrameTick) / 1000.0;
// 2. 判定流是否正常:设备在线 + 5秒内有帧
bool isFlowing = cam.IsOnline && secondsSinceLastFrame < StreamAliveThresholdSeconds;
// 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测
bool isPhysicalOk = isFlowing ? true : await ProbeHardwareAsync(cam).ConfigureAwait(false);
// 4. 状态调和决策:根据物理状态与设备状态的差异执行对应操作
if (isPhysicalOk && !cam.IsOnline && cam.IsRunning)
{
// 物理在线 + 设备离线 + 需运行 → 执行启动(加登录锁防止冲突)
bool lockTaken = false;
try
{
await _sdkLoginLock.WaitAsync(token).ConfigureAwait(false);
lockTaken = true;
// 双重校验:防止等待锁期间状态已变更
if (!cam.IsOnline)
{
await cam.StartAsync().ConfigureAwait(false);
}
}
finally
{
if (lockTaken)
{
_sdkLoginLock.Release();
}
}
}
else if (!isPhysicalOk && cam.IsOnline)
{
// 物理离线 + 设备在线 → 执行停止
await cam.StopAsync().ConfigureAwait(false);
}
else if (isPhysicalOk && cam.IsOnline && !isFlowing)
{
// 物理在线 + 设备在线 + 流中断 → 判定为僵死,执行复位
Console.WriteLine($"[自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中...");
await cam.StopAsync().ConfigureAwait(false);
}
}
#endregion
#region --- (Hardware Probing) ---
/// <summary>
/// 硬件连接探测:通过 Ping + TCP 双探测判定设备物理可达性
/// </summary>
/// <param name="cam">待探测的相机设备</param>
/// <returns>物理可达返回 true否则返回 false</returns>
private async Task<bool> ProbeHardwareAsync(BaseVideoSource cam)
{
// 1. 优先执行 Ping 探测(快速判定网络连通性)
try
{
using var ping = new Ping();
PingReply reply = await ping.SendPingAsync(cam.Config.IpAddress, PingTimeoutMs).ConfigureAwait(false);
if (reply.Status == IPStatus.Success)
{
return true;
}
}
catch
{
// Ping 探测失败,执行 TCP 探测兜底
}
// 2. TCP 探测:尝试连接设备端口(更精准的服务可达性判定)
try
{
using var tcpClient = new TcpClient();
using var cts = new CancellationTokenSource(TcpTimeoutMs);
await tcpClient.ConnectAsync(cam.Config.IpAddress, cam.Config.Port, cts.Token).ConfigureAwait(false);
return true;
}
catch
{
// TCP 探测失败,判定为物理不可达
return false;
}
}
#endregion
}

View File

@@ -0,0 +1,86 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧控制器(帧调度核心)
/// 功能:管理订阅者的帧需求,基于需求动态判定每帧的处理命运(保留/丢弃、分发目标)
/// 核心逻辑:采用“并集采样”策略,满足任意订阅者的帧率需求即保留帧,避免重复采样浪费资源
/// </summary>
public class FrameController
{
#region --- (Private Resources & States) ---
/// <summary> 订阅者帧需求集合(线程安全) </summary>
/// <remarks> Key订阅者AppIdValue该订阅者的帧需求配置 </remarks>
private readonly ConcurrentDictionary<string, FrameRequirement> _requirements = new();
/// <summary> 全局决策序列号(原子递增,确保决策唯一标识) </summary>
private long _globalSequence = 0;
#endregion
#region --- (Requirement Management) ---
/// <summary>
/// 注册/更新订阅者的帧需求
/// 功能:新增订阅者需求或更新已有订阅者的目标帧率
/// </summary>
/// <param name="appId">订阅者唯一标识(如 "RemoteClient_01"、"AI_Behavior_Engine"</param>
/// <param name="fps">目标帧率单位fps需大于0否则视为无效需求</param>
public void Register(string appId, int fps)
{
// 新增或更新需求:不存在则创建,存在则更新目标帧率
_requirements.AddOrUpdate(appId,
addValueFactory: _ => new FrameRequirement { AppId = appId, TargetFps = fps },
updateValueFactory: (_, oldRequirement) =>
{
oldRequirement.TargetFps = fps;
return oldRequirement;
});
}
#endregion
#region --- (Frame Decision Generation) ---
/// <summary>
/// [热路径] 判定当前物理帧是否需要保留并分发
/// 核心逻辑:并集采样,只要任意订阅者达到采样间隔,就保留该帧并分发至对应订阅者
/// </summary>
/// <param name="currentTick">当前系统时间戳(单位:毫秒,建议使用 Environment.TickCount64</param>
/// <returns>帧决策结果(包含是否保留、分发目标等信息)</returns>
public FrameDecision MakeDecision(long currentTick)
{
// 初始化决策对象,生成唯一序列号与时间戳
var decision = new FrameDecision
{
Sequence = Interlocked.Increment(ref _globalSequence), // 原子递增,线程安全
Timestamp = DateTime.Now
};
// 遍历所有订阅者需求,判定是否需要为该订阅者保留当前帧
foreach (var req in _requirements.Values)
{
// 跳过无效需求目标帧率≤0
if (req.TargetFps <= 0) continue;
// 计算该订阅者的采样间隔毫秒1000ms / 目标帧率
long interval = 1000 / req.TargetFps;
// 判定是否达到采样时间:当前时间 - 上次采样时间 ≥ 采样间隔允许1s内相位对齐自动合并
if (currentTick - req.LastCaptureTick >= interval)
{
// 加入分发目标列表
decision.TargetAppIds.Add(req.AppId);
// 更新该订阅者的上次采样时间,避免重复采样
req.LastCaptureTick = currentTick;
}
}
// 判定是否保留该帧存在分发目标则保留IsCaptured=true否则丢弃
decision.IsCaptured = decision.TargetAppIds.Count > 0;
return decision;
}
#endregion
}

View File

@@ -0,0 +1,34 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧决策结果模型
/// 功能:告知驱动层单帧数据的处理命运(保留/丢弃、分发目标),是帧调度的核心指令
/// 用途:由 FrameController 生成,传递给驱动层与分发器,指导帧的后续流转
/// </summary>
public class FrameDecision
{
#region --- (Decision Core Identification) ---
/// <summary> 决策序列号(全局唯一,关联帧的决策记录,用于追踪决策生命周期) </summary>
public long Sequence { get; set; }
/// <summary> 决策生成时间戳(记录决策的创建时刻,默认当前时间) </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
#endregion
#region --- (Frame Processing Decision) ---
/// <summary> 帧是否被保留true=保留并分发false=直接丢弃,不进行后续处理) </summary>
public bool IsCaptured { get; set; }
#endregion
#region --- (Frame Distribution Targets) ---
/// <summary> 帧分发目标应用ID列表记录该帧将服务的所有订阅者AppId </summary>
/// <remarks> 示例值:["WPF_Display_Main", "AI_Behavior_Engine"],仅当 IsCaptured 为 true 时有效 </remarks>
public List<string> TargetAppIds { get; } = new();
#endregion
}

View File

@@ -0,0 +1,37 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧需求定义模型
/// 功能:描述某个程序/模块对视频帧的消费需求,用于帧分发调度与帧率控制
/// 用途:配合 FrameController实现按订阅者需求精准分配帧资源避免资源浪费
/// </summary>
public class FrameRequirement
{
#region --- (Subscriber Core Identification) ---
/// <summary> 订阅者唯一ID如 "Client_A"、"AI_Service"、"WPF_Display_Main" </summary>
/// <remarks> 用于区分不同的帧消费模块,作为帧分发路由的关键标识 </remarks>
public string AppId { get; set; } = string.Empty;
#endregion
#region --- (Frame Requirement Parameters) ---
/// <summary> 目标帧率单位fps订阅者期望的每秒接收帧数0 表示无特定需求) </summary>
/// <remarks> 例如UI 预览需 8fpsAI 分析需 2fps按需分配以节省计算资源 </remarks>
public int TargetFps { get; set; } = 0;
/// <summary> 上次获取帧的时间点(单位:毫秒,通常为 Environment.TickCount64 </summary>
/// <remarks> 用于帧率控制算法,判断是否达到订阅者的目标帧率需求 </remarks>
public long LastCaptureTick { get; set; } = 0;
#endregion
#region --- (Requirement Status Control) ---
/// <summary> 需求是否激活true=正常接收帧false=暂停接收,保留配置) </summary>
/// <remarks> 支持动态启停订阅,无需删除需求配置,提升灵活性 </remarks>
public bool IsActive { get; set; } = true;
#endregion
}

View File

@@ -0,0 +1,50 @@
namespace SHH.CameraSdk;
/// <summary>
/// 相机健康度报告
/// 功能:封装相机的详细运行健康数据,包含状态、性能、故障统计等信息
/// 用途:用于运维分析、故障排查、设备健康度评估,提供比遥测快照更细致的健康指标
/// </summary>
public class CameraHealthReport
{
#region --- (Device Core Identification) ---
/// <summary> 设备唯一业务标识对应数据库ID或配置中的设备ID </summary>
public long DeviceId { get; set; }
/// <summary> 设备IP地址用于定位具体设备 </summary>
public string Ip { get; set; } = string.Empty;
#endregion
#region --- (Device Operation Status) ---
/// <summary> 设备当前运行状态(字符串形式,对应 VideoSourceStatus 枚举值,如 "Streaming"/"Faulted"/"Reconnecting" </summary>
public string Status { get; set; } = string.Empty;
/// <summary> 最后一次错误信息(无错误时建议设为空字符串,记录设备最近一次故障原因) </summary>
public string LastError { get; set; } = string.Empty;
#endregion
#region --- (Performance & Latency Metrics) ---
/// <summary> 实时帧率单位fps反映相机实际输出的有效帧率 </summary>
/// <remarks> 统计逻辑:需在 RaiseFrameReceived 事件中增加计数器,按时间窗口计算实时值 </remarks>
public double RealFps { get; set; }
/// <summary> 推流延迟(单位:毫秒,记录从相机推流到接收端成功接收的总延迟) </summary>
public double LatencyMs { get; set; }
#endregion
#region --- (Fault & Recovery Statistics) ---
/// <summary> 丢帧计数(因渲染过慢、缓冲区溢出等原因导致的丢弃帧数累计值) </summary>
public long DropFrames { get; set; }
/// <summary> 重连次数(哨兵机制触发的自动重连累计次数,反映设备网络稳定性) </summary>
public int ReconnectCount { get; set; }
#endregion
}

View File

@@ -0,0 +1,56 @@
namespace SHH.CameraSdk;
/// <summary>
/// 相机实时遥测数据快照
/// 功能:封装单台相机的实时运行状态、性能指标与健康度信息,用于监控面板展示、运维告警与数据分析
/// 特性数据为瞬时快照通常定期如1秒/次)更新,反映相机当前运行状况
/// </summary>
public class CameraTelemetryInfo
{
#region --- (Device Core Identification) ---
/// <summary> 设备唯一业务标识对应数据库ID或配置中的设备ID </summary>
public long DeviceId { get; set; }
/// <summary> 设备显示名称(如“北大门-枪机01”用于UI展示 </summary>
public string Name { get; set; } = string.Empty;
/// <summary> 设备IP地址用于网络连通性校验与远程访问 </summary>
public string IpAddress { get; set; } = string.Empty;
#endregion
#region --- (Device Operation Status) ---
/// <summary> 相机当前运行状态(字符串形式,对应 VideoSourceStatus 枚举值,如 "Playing"/"Faulted"/"Connecting" </summary>
public string Status { get; set; } = string.Empty;
/// <summary> 设备物理连接状态(通过 Ping/TCP 探测判定true=在线false=离线) </summary>
public bool IsOnline { get; set; }
/// <summary> 最后一次报错信息(无错误时为 null用于快速定位故障原因 </summary>
public string? LastErrorMessage { get; set; }
#endregion
#region --- (Performance Metrics) ---
/// <summary> 实时帧率单位fps反映相机取流与处理的实时速度 </summary>
public double Fps { get; set; }
/// <summary> 累计接收帧数(相机启动后接收的总帧数,用于统计数据完整性) </summary>
public long TotalFrames { get; set; }
#endregion
#region --- (Health & Statistics) ---
/// <summary> 设备健康度评分0-100分分数越高健康状态越好 </summary>
/// <remarks> 计算逻辑结合是否断线、实时FPS是否在正常范围、是否有报错等因素综合判定 </remarks>
public int HealthScore { get; set; }
/// <summary> 遥测数据统计时间戳(记录当前快照的生成时间,默认当前时间) </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
#endregion
}

View File

@@ -0,0 +1,21 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧消费者类型枚举
/// 功能:定义帧数据的消费场景/模块标识,用于帧分发路由、权限控制与遥测统计
/// 用途配合全局流分发器GlobalStreamDispatcher实现帧数据的精准定向分发
/// </summary>
public enum FrameConsumerType
{
/// <summary> UI 预览消费:用于前端界面实时显示(如 WPF/WinForm 控件、Web 页面渲染) </summary>
UI_Preview,
/// <summary> AI 分析消费:用于 AI 算法处理(如行为识别、人脸检测、车牌识别等耗时分析场景) </summary>
AI_Analysis,
/// <summary> 网络流消费:用于网络推流(如 RTSP/RTMP 推流、WebSocket 实时推送等) </summary>
Network_Stream,
/// <summary> 一次性截图消费:用于单次截图操作(如用户手动抓拍、定时快照等临时消费场景) </summary>
Snapshot_OneOff
}

View File

@@ -0,0 +1,61 @@
namespace SHH.CameraSdk;
/// <summary>
/// 帧全链路追踪上下文
/// 功能:记录单帧数据从产生到结束的完整生命周期信息,包含标识、决策结果、性能指标与日志流水
/// 用途:用于问题排查、性能分析、帧流转追溯,支撑全链路可观测性
/// </summary>
public class FrameContext
{
#region --- (Frame Core Identification) ---
/// <summary> 物理帧序号(全局唯一,关联帧的原始数据标识) </summary>
public long FrameSequence { get; set; }
/// <summary> 帧上下文创建时间戳(默认当前时间,记录帧进入追踪链路的时刻) </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
#endregion
#region --- (Frame Decision Results) ---
/// <summary> 帧是否被保留true=保留并分发false=被丢弃) </summary>
public bool IsCaptured { get; set; }
/// <summary> 帧丢弃原因(仅 IsCaptured 为 false 时有效,默认空字符串) </summary>
/// <remarks>示例值:"NoSubscribers"(无订阅者)、"PipelineFull"(处理管道满)、"FpsLimit"(帧率限制)</remarks>
public string DropReason { get; set; } = string.Empty;
/// <summary> 帧分发目标应用ID列表记录该帧最终分发给的所有订阅者标识合并结果 </summary>
/// <remarks>示例值:["WPF_Display_Main", "AI_Behavior_Engine"]</remarks>
public List<string> TargetAppIds { get; set; } = new();
#endregion
#region --- (Frame Performance Metrics) ---
/// <summary> 颜色转码耗时(单位:毫秒) </summary>
/// <remarks>记录帧数据格式转换(如 YUV→BGR的耗时用于性能瓶颈定位</remarks>
public double CvtColorCostMs { get; set; }
/// <summary> 二次处理耗时(单位:毫秒) </summary>
/// <remarks>记录帧在处理管道中的额外加工耗时如打水印、裁剪、AI预处理等</remarks>
public double ProcessCostMs { get; set; }
/// <summary> 帧总处理耗时(单位:毫秒) </summary>
/// <remarks>记录帧从进入追踪链路到处理完成/丢弃的总耗时,为性能优化提供数据支撑</remarks>
public double TotalCostMs { get; set; }
#endregion
#region --- (Frame Logs) ---
/// <summary> 帧生命周期日志流水(按时间顺序记录关键节点操作) </summary>
public List<string> Logs { get; } = new();
/// <summary> 新增帧日志(自动添加时间戳,格式:[HH:mm:ss.fff] 日志内容) </summary>
/// <param name="msg">日志内容(记录帧流转的关键节点,如“驱动提交帧数据”“管道处理完成”)</param>
public void AddLog(string msg) => Logs.Add($"[{DateTime.Now:HH:mm:ss.fff}] {msg}");
#endregion
}

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace SHH.CameraSdk;
/// <summary>
/// 帧追踪数据模型
/// 功能:记录单帧数据的生命周期关键信息,用于帧流转监控、丢帧分析与性能排查
/// 适用场景:配合全局遥测或调试工具,追溯帧的处理路径、耗时及最终状态
/// </summary>
public class FrameTrace
{
#region --- (Frame Core Identification) ---
/// <summary> 帧唯一序列号(全局唯一,用于关联帧的全生命周期) </summary>
public long FrameId { get; set; }
/// <summary> 帧产生时间戳(单位:毫秒,通常为 Environment.TickCount64 或 UTC 时间戳) </summary>
public long Timestamp { get; set; }
#endregion
#region --- (Frame Status Information) ---
/// <summary> 帧是否被丢弃true=已丢弃false=正常处理/分发) </summary>
public bool IsDropped { get; set; }
/// <summary> 帧丢弃原因(仅 IsDropped 为 true 时有效) </summary>
/// <remarks>示例值:"NoSubscribers"(无订阅者)、"FpsLimit"(帧率限制)、"PipelineFull"(处理管道满)</remarks>
public string DropReason { get; set; } = string.Empty;
/// <summary> 帧最终分发目标列表(记录该帧被发送到的订阅者/模块标识) </summary>
/// <remarks>示例值:["WPF_Display_Main", "AI_Behavior_Engine"]</remarks>
public List<string> Targets { get; set; } = new();
#endregion
#region --- (Frame Performance Metrics) ---
/// <summary> 帧处置总耗时(单位:毫秒) </summary>
/// <remarks>计算范围:从帧产生到最终处理完成/丢弃的总时间,用于性能瓶颈分析</remarks>
public double ProcessDurationMs { get; set; }
#endregion
}

View File

@@ -0,0 +1,66 @@
namespace SHH.CameraSdk;
/// <summary>
/// 全局遥测仓储(静态类)
/// 功能:存储并提供帧生命周期的追踪日志查询,采用环形缓冲区机制限制日志数量
/// 用途:用于问题排查、性能分析,记录每帧的处理流程、耗时、丢弃原因等信息
/// </summary>
public static class GlobalTelemetry
{
#region --- (Static Storage Resources) ---
/// <summary>
/// 环形日志缓冲区:存储帧追踪上下文(线程安全)
/// Key帧序列号FrameSequenceValue帧全链路追踪上下文
/// </summary>
private static readonly ConcurrentDictionary<long, FrameContext> _logs = new();
/// <summary>
/// 日志序列号队列:用于维护环形缓冲区的淘汰顺序(线程安全)
/// 作用:记录帧日志的插入顺序,超过最大数量时淘汰最早的记录
/// </summary>
private static readonly ConcurrentQueue<long> _keys = new();
/// <summary>
/// 最大日志保留数量:限制环形缓冲区仅保留最近 200 条帧追踪记录
/// 目的:防止日志过多导致内存占用飙升
/// </summary>
private const int MaxLogCount = 200;
#endregion
#region --- (Core Operation Methods) ---
/// <summary>
/// 记录帧追踪日志
/// 功能:将帧上下文存入缓冲区,超过最大数量时自动淘汰最早的记录
/// </summary>
/// <param name="frameSeq">帧序列号(唯一标识某一帧)</param>
/// <param name="context">帧全链路追踪上下文(含处理日志、耗时、丢弃原因等)</param>
public static void RecordLog(long frameSeq, FrameContext context)
{
// 存入日志缓冲区(存在相同序列号时会覆盖,确保最新记录)
_logs[frameSeq] = context;
// 记录序列号到队列,用于后续淘汰逻辑
_keys.Enqueue(frameSeq);
// 环形缓冲区淘汰逻辑:超过最大数量时,移除最早插入的记录
if (_keys.Count > MaxLogCount && _keys.TryDequeue(out var oldKey))
{
_logs.TryRemove(oldKey, out _);
}
}
/// <summary>
/// 获取最近的帧追踪日志
/// 功能:按时间戳降序返回缓冲区中的所有记录(最新记录在前)
/// </summary>
/// <returns>帧追踪上下文集合(最多 MaxLogCount 条)</returns>
public static IEnumerable<FrameContext> GetRecentLogs()
{
// 按帧上下文的时间戳降序排序,确保最新记录优先返回
return _logs.Values.OrderByDescending(x => x.Timestamp);
}
#endregion
}