海康摄像头取流示例初始签入
This commit is contained in:
62
SHH.CameraSdk/Abstractions/Enums/DeviceBrand.cs
Normal file
62
SHH.CameraSdk/Abstractions/Enums/DeviceBrand.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频源物理/逻辑品牌类型
|
||||
/// 职责:用于工厂模式匹配具体的 IVideoSource 实现类,并定义基础通信协议栈
|
||||
/// </summary>
|
||||
public enum DeviceBrand
|
||||
{
|
||||
/// <summary>
|
||||
/// 未知
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 海康威视 (HikVision)
|
||||
/// 技术路径:基于海康私有 SDK (HCNetSDK.dll / PlayCtrl.dll)。
|
||||
/// 特性:支持全功能控制(PTZ、对讲、配置、报警回传)。
|
||||
/// </summary>
|
||||
HikVision,
|
||||
|
||||
/// <summary>
|
||||
/// 大华 (Dahua)
|
||||
/// 技术路径:基于大华私有 SDK (dhnetsdk.dll / dhplay.dll)。
|
||||
/// 特性:支持全功能控制,与海康私有协议不兼容。
|
||||
/// </summary>
|
||||
Dahua,
|
||||
|
||||
/// <summary>
|
||||
/// USB 摄像头 / 虚拟摄像头
|
||||
/// 技术路径:基于 DirectShow 或 Windows Media Foundation。
|
||||
/// 特性:通常通过 OpenCV (VideoCapture) 或 DirectShowLib 直接读取本地硬件引用。
|
||||
/// </summary>
|
||||
Usb,
|
||||
|
||||
/// <summary>
|
||||
/// 标准 RTSP 流媒体
|
||||
/// 技术路径:基于标准 RTSP/RTP 协议 (RFC 2326)。
|
||||
/// 特性:跨品牌兼容,通常使用 FFmpeg 或 GStreamer 库取流,仅支持音视频,不支持云台控制。
|
||||
/// </summary>
|
||||
RtspGeneral,
|
||||
|
||||
/// <summary>
|
||||
/// 三恒自研 WebSocket 流
|
||||
/// 技术路径:基于 WebSocket 传输的自定义二进制或 Base64 帧。
|
||||
/// 特性:专用于 Web 或云端推送场景的私有流媒体格式。
|
||||
/// </summary>
|
||||
WebSocketShine,
|
||||
|
||||
/// <summary>
|
||||
/// 本地视频文件
|
||||
/// 技术路径:基于文件 IO 的离线解码。
|
||||
/// 特性:常用于算法演示、回放模拟,支持 Mp4, Avi, Mkv 等容器格式。
|
||||
/// </summary>
|
||||
File,
|
||||
|
||||
/// <summary>
|
||||
/// 未知/通用标准 (ONVIF)
|
||||
/// 技术路径:基于标准 ONVIF WebService。
|
||||
/// 特性:用于接入非主流厂商但符合 ONVIF 标准的设备,支持基础 PTZ。
|
||||
/// </summary>
|
||||
OnvifGeneral
|
||||
}
|
||||
16
SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs
Normal file
16
SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 网络传输协议类型
|
||||
/// </summary>
|
||||
public enum TransportProtocol
|
||||
{
|
||||
/// <summary> 可靠传输 (默认) </summary>
|
||||
Tcp = 0,
|
||||
|
||||
/// <summary> 快速传输 (可能丢包/花屏) </summary>
|
||||
Udp = 1,
|
||||
|
||||
/// <summary> 组播 (节省带宽) </summary>
|
||||
Multicast = 2
|
||||
}
|
||||
57
SHH.CameraSdk/Abstractions/Enums/VideoSourceStatus.cs
Normal file
57
SHH.CameraSdk/Abstractions/Enums/VideoSourceStatus.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频源逻辑状态枚举
|
||||
/// 描述了从配置加载到视频流稳定输出的完整生命周期
|
||||
/// </summary>
|
||||
public enum VideoSourceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 已断开/初始状态。
|
||||
/// 此时资源已释放,尚未执行 Login 或 Start 操作。
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// 正在尝试建立网络连接。
|
||||
/// 此时正在进行 Socket 握手或探测设备 IP 是否可达。
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// 正在进行身份验证。
|
||||
/// 连接已建立,正在提交 UserName/Password 调用 SDK 的 Login 接口。
|
||||
/// </summary>
|
||||
Authorizing,
|
||||
|
||||
/// <summary>
|
||||
/// 已登录/待机。
|
||||
/// 登录成功并获取到了设备元数据(Metadata),但尚未启动预览(RealPlay)。
|
||||
/// 适用于“仅管理,不看画面”的场景。
|
||||
/// </summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>
|
||||
/// 正常取流播放中
|
||||
/// </summary>
|
||||
Playing,
|
||||
|
||||
/// <summary>
|
||||
/// 正在取流/正常运行中。
|
||||
/// 预览句柄已开启,取流回调函数正在持续接收数据帧并进行解码。
|
||||
/// </summary>
|
||||
Streaming,
|
||||
|
||||
/// <summary>
|
||||
/// 自动重连中。
|
||||
/// 检测到网络抖动或心跳丢失,SDK 正在尝试内部恢复,此时视频流可能处于停滞状态。
|
||||
/// </summary>
|
||||
Reconnecting,
|
||||
|
||||
/// <summary>
|
||||
/// 故障/异常状态。
|
||||
/// 发生了不可恢复的错误(如密码错误、最大连接数限制、设备强制离线)。
|
||||
/// 进入此状态通常需要人工干预或调用 Stop 后重新 Start。
|
||||
/// </summary>
|
||||
Faulted
|
||||
}
|
||||
226
SHH.CameraSdk/Abstractions/Errors/CameraErrorCode.cs
Normal file
226
SHH.CameraSdk/Abstractions/Errors/CameraErrorCode.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 工业级相机归一化错误码 (修正全量版)
|
||||
/// 职责:跨厂家建立统一故障语义,支撑 HikErrorMapper 等驱动层的精准映射。
|
||||
/// </summary>
|
||||
public enum CameraErrorCode
|
||||
{
|
||||
[Description("操作成功")]
|
||||
Success = 0,
|
||||
|
||||
#region --- 1000-1499 运行环境与 SDK 基础故障 ---
|
||||
|
||||
[Description("SDK 未初始化")]
|
||||
SdkNotInitialized = 1000,
|
||||
|
||||
[Description("SDK 资源分配错误或本地内存不足")]
|
||||
LocalResourceError = 1001,
|
||||
|
||||
[Description("加载插件/组件失败:缺少 DLL 或依赖库")]
|
||||
ComponentLoadFailed = 1002,
|
||||
|
||||
[Description("组件版本不匹配")]
|
||||
ComponentVersionMismatch = 1003,
|
||||
|
||||
[Description("加载加密库失败(Ope nSSL/LibEay32)")]
|
||||
EncryptionLibError = 1004, // 已补齐 (海康 156)
|
||||
|
||||
[Description("函数调用顺序错误")]
|
||||
FunctionOrderError = 1005,
|
||||
|
||||
[Description("操作系统不支持该功能")]
|
||||
OsNotSupported = 1006,
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 1500-1999 网络通信与协议故障 ---
|
||||
|
||||
[Description("连接设备失败:设备离线")]
|
||||
NetworkUnreachable = 1500,
|
||||
|
||||
[Description("交互超时:网络拥塞或设备响应慢")]
|
||||
Timeout = 1501,
|
||||
|
||||
[Description("数据发送失败")]
|
||||
NetworkSendError = 1502,
|
||||
|
||||
[Description("数据接收失败")]
|
||||
NetworkRecvError = 1503,
|
||||
|
||||
[Description("网络套接字(Socket)异常")]
|
||||
SocketError = 1504,
|
||||
|
||||
[Description("IP 地址冲突")]
|
||||
IpConflict = 1505,
|
||||
|
||||
[Description("端口池耗尽或端口复用失败")]
|
||||
PortPoolExhausted = 1506,
|
||||
|
||||
[Description("连接已失效或未建立")]
|
||||
InvalidLink = 1507, // 已补齐 (海康 188)
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2000-2499 身份认证与权限管理 ---
|
||||
|
||||
[Description("用户名或密码错误")]
|
||||
InvalidCredentials = 2000,
|
||||
|
||||
[Description("用户权限不足")]
|
||||
AccessDenied = 2001,
|
||||
|
||||
[Description("用户不存在")]
|
||||
UserNotExist = 2002,
|
||||
|
||||
[Description("账号已被锁定(多次尝试失败)")]
|
||||
AccountLocked = 2003,
|
||||
|
||||
[Description("登录人数已达上限")]
|
||||
MaxUserExceeded = 2004,
|
||||
|
||||
[Description("会话已过期或已被强行踢出")]
|
||||
SessionExpired = 2005,
|
||||
|
||||
[Description("用户正在使用中(如正在对讲/升级)")]
|
||||
UserInUse = 2006, // 已补齐 (海康 74)
|
||||
|
||||
[Description("登录版本过低(不支持该协议)")]
|
||||
LoginVersionLow = 2007, // 已补齐 (海康 155)
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2500-2999 设备资源与负载限制 ---
|
||||
|
||||
[Description("设备连接数已达上限")]
|
||||
MaxConnectionsReached = 2500,
|
||||
|
||||
[Description("设备资源不足或内部忙")]
|
||||
DeviceResourceBusy = 2501,
|
||||
|
||||
[Description("通道接入数达到上限")]
|
||||
MaxQuantityExceeded = 2502,
|
||||
|
||||
[Description("主/子码流路数超限")]
|
||||
MaxStreamExceeded = 2503,
|
||||
|
||||
[Description("设备缓冲区不足/溢出")]
|
||||
DeviceBufferOverflow = 2504,
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3000-3499 视频预览、回放与解码 ---
|
||||
|
||||
[Description("预览失败或通道未编码")]
|
||||
PreviewFailed = 3000,
|
||||
|
||||
[Description("码流封装格式不支持")]
|
||||
StreamTypeNotSupport = 3001,
|
||||
|
||||
[Description("码流数据中断(丢包/心跳丢失)")]
|
||||
StreamInterrupted = 3002,
|
||||
|
||||
[Description("码流已加密(需二次认证)")]
|
||||
StreamEncrypted = 3003,
|
||||
|
||||
[Description("外接 IP 通道离线")]
|
||||
IpChannelOffline = 3004,
|
||||
|
||||
[Description("设备通道异常")]
|
||||
ChannelException = 3005, // 已补齐 (海康 18)
|
||||
|
||||
[Description("播放库(Player SDK)调用失败")]
|
||||
PlayerSdkFailed = 3006, // 已补齐 (海康 51)
|
||||
|
||||
[Description("音频设备忙(声卡被独占)")]
|
||||
AudioDeviceBusy = 3007, // 已补齐 (海康 69)
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3500-3999 存储管理故障 ---
|
||||
|
||||
[Description("存储设备通用错误")]
|
||||
StorageError = 3500,
|
||||
|
||||
[Description("设备无硬盘")]
|
||||
NoDisk = 3501,
|
||||
|
||||
[Description("硬盘已满")]
|
||||
DiskFull = 3502,
|
||||
|
||||
[Description("硬盘状态异常(格式化中或读写错)")]
|
||||
DiskStatusError = 3503,
|
||||
|
||||
[Description("尝试格式化只读硬盘")]
|
||||
DiskReadOnly = 3504,
|
||||
|
||||
[Description("存储池/NAS 目录无效")]
|
||||
StoragePoolError = 3505,
|
||||
|
||||
[Description("写入存储(Flash/文件)失败")]
|
||||
WriteStorageFailed = 3506, // 已补齐 (海康 48, 77)
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 4000-4499 硬件、参数与系统故障 ---
|
||||
|
||||
[Description("硬件内部故障")]
|
||||
HardwareFault = 4000,
|
||||
|
||||
[Description("通道号错误或不存在")]
|
||||
InvalidChannel = 4001,
|
||||
|
||||
[Description("参数错误(空指针或无效值)")]
|
||||
InvalidParameter = 4002,
|
||||
|
||||
[Description("视频信号丢失(黑屏/丢信号)")]
|
||||
VideoSignalLoss = 4003,
|
||||
|
||||
[Description("设备正在重启中")]
|
||||
DeviceRebooting = 4004,
|
||||
|
||||
[Description("需重启生效")]
|
||||
RebootRequired = 4005,
|
||||
|
||||
[Description("时间输入错误")]
|
||||
InvalidTimeInput = 4006, // 已补齐 (海康 32)
|
||||
|
||||
[Description("设备型号或版本不匹配")]
|
||||
DeviceMismatch = 4007, // 已补齐 (海康 80)
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 4500-4999 操作限制与通用状态 ---
|
||||
|
||||
[Description("设备不支持该功能")]
|
||||
NotSupported = 4500, // 已补齐 (海康 23)
|
||||
|
||||
[Description("修改或设置失败")]
|
||||
ModifyFailed = 4501,
|
||||
|
||||
[Description("不支持无阻塞抓图")]
|
||||
CaptureNotSupport = 4502,
|
||||
|
||||
[Description("设备忙")]
|
||||
DeviceBusy = 4503,
|
||||
|
||||
[Description("上次操作未完成")]
|
||||
OperationNotFinished = 4504,
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 9000-9999 系统级故障 ---
|
||||
|
||||
[Description("驱动未实现该功能")]
|
||||
NotImplemented = 9001,
|
||||
|
||||
[Description("程序异常")]
|
||||
ProgramException = 9998,
|
||||
|
||||
[Description("未知错误")]
|
||||
Unknown = 9999
|
||||
|
||||
#endregion
|
||||
}
|
||||
107
SHH.CameraSdk/Abstractions/Errors/CameraException.cs
Normal file
107
SHH.CameraSdk/Abstractions/Errors/CameraException.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频 SDK 统一异常类 (V3.3.1 修复版)
|
||||
/// 核心职责:
|
||||
/// <para>1. 封装标准化错误码、厂商原始错误码、设备品牌信息</para>
|
||||
/// <para>2. 记录异常上下文快照,辅助故障定位与复盘</para>
|
||||
/// 协作关系:
|
||||
/// <para>1. 与 <see cref="HikErrorMapper"/> 配合:实现厂商错误码→标准错误码的转换</para>
|
||||
/// <para>2. 与 <see cref="RecoveryPolicy"/> 配合:提供错误码输入,驱动故障自愈决策</para>
|
||||
/// </summary>
|
||||
public class CameraException : Exception
|
||||
{
|
||||
#region --- 核心异常属性 (Core Exception Properties) ---
|
||||
|
||||
/// <summary>
|
||||
/// 归一化后的标准错误码
|
||||
/// 业务用途:作为 RecoveryPolicy 的决策输入,屏蔽厂商差异
|
||||
/// </summary>
|
||||
public CameraErrorCode ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 厂商原始错误码(如海康 NET_DVR_GetLastError、大华 SDK 原生错误码)
|
||||
/// 业务用途:厂商文档对照、深度问题排查
|
||||
/// </summary>
|
||||
public int RawErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生异常的设备品牌
|
||||
/// 业务用途:区分不同厂商的错误码规则,辅助错误映射
|
||||
/// </summary>
|
||||
public DeviceBrand Brand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 异常发生时的上下文快照(只读集合,防止外部篡改)
|
||||
/// 存储内容:设备IP、通道号、操作参数、SDK句柄、时间戳等案发现场信息
|
||||
/// 业务用途:故障复盘时还原现场,快速定位根因
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Context { get; init; } = new Dictionary<string, object>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 (Constructors) ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 CameraException 实例
|
||||
/// </summary>
|
||||
/// <param name="errorCode">归一化标准错误码</param>
|
||||
/// <param name="message">异常描述信息</param>
|
||||
/// <param name="brand">设备品牌</param>
|
||||
/// <param name="rawErrorCode">厂商原始错误码(默认 0)</param>
|
||||
/// <param name="innerException">内部异常(默认 null)</param>
|
||||
public CameraException(
|
||||
CameraErrorCode errorCode,
|
||||
string message,
|
||||
DeviceBrand brand,
|
||||
int rawErrorCode = 0,
|
||||
Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
Brand = brand;
|
||||
RawErrorCode = rawErrorCode;
|
||||
// 初始化上下文字典为可写的 Dictionary,兼容 WithContext 方法
|
||||
Context = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 工具方法 (Utility Methods) ---
|
||||
|
||||
/// <summary>
|
||||
/// 链式添加上下文信息(Builder 模式)
|
||||
/// 业务用途:在抛出异常前,逐步追加案发现场信息
|
||||
/// </summary>
|
||||
/// <param name="key">上下文键(如 "DeviceIp", "ChannelIndex")</param>
|
||||
/// <param name="value">上下文值</param>
|
||||
/// <returns>当前异常实例(支持链式调用)</returns>
|
||||
public CameraException WithContext(string key, object value)
|
||||
{
|
||||
// 强制转换为可写的 Dictionary,保证上下文可追加
|
||||
if (Context is Dictionary<string, object> contextDict)
|
||||
{
|
||||
contextDict[key] = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写 ToString 方法,输出标准化异常日志
|
||||
/// 格式:[CameraError] Brand: {品牌} | Code: {标准码}({原始码}) | Message: {描述} | Context: {上下文}
|
||||
/// </summary>
|
||||
/// <returns>格式化的异常字符串</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var contextStr = Context.Count > 0
|
||||
? $" | Context: {string.Join(", ", Context.Select(kvp => $"{kvp.Key}={kvp.Value}"))}"
|
||||
: string.Empty;
|
||||
|
||||
return $"[CameraError] Brand: {Brand} | Code: {ErrorCode}({RawErrorCode}) | Message: {Message}{contextStr}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
55
SHH.CameraSdk/Abstractions/Errors/RecoveryAction.cs
Normal file
55
SHH.CameraSdk/Abstractions/Errors/RecoveryAction.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 故障恢复决策建议枚举
|
||||
/// 核心职责:定义标准化的故障自愈动作指令,指导 <see cref="RecoveryPolicy"/> 与 <see cref="BaseVideoSource"/> 执行差异化恢复逻辑
|
||||
/// 设计原则:按“无动作→自动恢复→降级→致命停止→人工介入”的优先级划分,覆盖全场景故障处理
|
||||
/// </summary>
|
||||
public enum RecoveryAction
|
||||
{
|
||||
#region --- 0. 基础状态 ---
|
||||
|
||||
/// <summary>
|
||||
/// 正常状态,无需执行任何恢复动作
|
||||
/// 适用场景:错误码为 Success、设备运行正常
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 1. 自动恢复动作 ---
|
||||
|
||||
/// <summary>
|
||||
/// 自动指数退避重试
|
||||
/// 适用场景:网络抖动、超时、设备资源繁忙等**暂时性故障**
|
||||
/// 执行标准:采用 2^n * 1000ms 算法计算延迟,上限 2 分钟,避免频繁重试加剧系统负载
|
||||
/// </summary>
|
||||
RetryWithBackoff,
|
||||
|
||||
/// <summary>
|
||||
/// 降级运行
|
||||
/// 适用场景:主码流超限、高清分辨率不支持等**非致命功能降级场景**
|
||||
/// 执行标准:自动切换到备用方案(如主码流→子码流、4K→1080P),保证基础功能可用
|
||||
/// </summary>
|
||||
Degrade,
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 终止与人工动作 ---
|
||||
|
||||
/// <summary>
|
||||
/// 致命停止,禁止继续重试
|
||||
/// 适用场景:密码错误、账号锁定、IP 拉黑等**不可自愈的认证/权限类故障**
|
||||
/// 执行标准:立即停止自愈引擎,推送告警信息到运维平台,记录详细错误日志
|
||||
/// </summary>
|
||||
FatalStop,
|
||||
|
||||
/// <summary>
|
||||
/// 需要人工介入处理
|
||||
/// 适用场景:硬件故障、磁盘满、SDK 组件缺失等**软件无法修复的底层故障**
|
||||
/// 执行标准:触发告警通知,标记设备状态为 Faulted,等待运维人员排查
|
||||
/// </summary>
|
||||
ManualIntervention
|
||||
|
||||
#endregion
|
||||
}
|
||||
100
SHH.CameraSdk/Abstractions/Errors/RecoveryPolicy.cs
Normal file
100
SHH.CameraSdk/Abstractions/Errors/RecoveryPolicy.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// [决策引擎] 故障自愈策略 (V3.3.1 修复版)
|
||||
/// 核心职责:根据设备错误码特征,智能裁决系统应采取的恢复动作,实现故障自动化处理
|
||||
/// 关键修复(Bug R):
|
||||
/// <para>1. 致命错误防护:对 InvalidCredentials/AccountLocked 等错误禁止重试,防止账号被锁、IP 拉黑</para>
|
||||
/// <para>2. 未知错误保守策略:对 Unknown 错误采用 ManualIntervention,避免未知风险扩散</para>
|
||||
/// 设计原则:最小化风险、最大化自愈率,区分可重试/不可重试/需人工干预的错误类型
|
||||
/// </summary>
|
||||
public static class RecoveryPolicy
|
||||
{
|
||||
#region --- 1. 核心决策逻辑:错误码→自愈动作映射 ---
|
||||
|
||||
/// <summary>
|
||||
/// 根据相机错误码判定对应的故障自愈动作
|
||||
/// </summary>
|
||||
/// <param name="code">设备上报的错误码</param>
|
||||
/// <returns>标准化的自愈动作指令</returns>
|
||||
public static RecoveryAction GetAction(CameraErrorCode code)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
// ========== 场景 A: 网络类故障 (可自愈) ==========
|
||||
// 策略:指数退避重试
|
||||
// 理由:网络波动、超时、闪断为暂时性故障,延迟重试大概率恢复
|
||||
CameraErrorCode.NetworkUnreachable or
|
||||
CameraErrorCode.NetworkSendError or
|
||||
CameraErrorCode.NetworkRecvError or
|
||||
CameraErrorCode.Timeout or
|
||||
CameraErrorCode.SocketError or
|
||||
CameraErrorCode.StreamInterrupted or
|
||||
CameraErrorCode.DeviceRebooting => RecoveryAction.RetryWithBackoff,
|
||||
|
||||
// ========== 场景 B: 资源繁忙类故障 (可自愈) ==========
|
||||
// 策略:指数退避重试
|
||||
// 理由:设备连接数满、缓冲区溢出,等待资源释放后可恢复
|
||||
CameraErrorCode.DeviceResourceBusy or
|
||||
CameraErrorCode.DeviceBufferOverflow or
|
||||
CameraErrorCode.DeviceBusy or
|
||||
CameraErrorCode.OperationNotFinished or
|
||||
CameraErrorCode.PortPoolExhausted or
|
||||
CameraErrorCode.MaxConnectionsReached or
|
||||
CameraErrorCode.MaxStreamExceeded => RecoveryAction.RetryWithBackoff,
|
||||
|
||||
// ========== 场景 C: 致命错误 (不可自愈,禁止重试) ==========
|
||||
// 策略:立即停止
|
||||
// 理由:密码错误、账号锁定、组件缺失等故障,重试无意义且会加剧风险(账号锁死、日志爆炸)
|
||||
CameraErrorCode.InvalidCredentials or
|
||||
CameraErrorCode.AccessDenied or
|
||||
CameraErrorCode.UserNotExist or
|
||||
CameraErrorCode.AccountLocked or
|
||||
CameraErrorCode.SessionExpired or
|
||||
CameraErrorCode.InvalidChannel or
|
||||
CameraErrorCode.IpConflict or
|
||||
CameraErrorCode.SdkNotInitialized or
|
||||
CameraErrorCode.ComponentLoadFailed or
|
||||
CameraErrorCode.EncryptionLibError => RecoveryAction.FatalStop,
|
||||
|
||||
// ========== 场景 D: 硬件故障 (需人工干预) ==========
|
||||
// 策略:人工介入
|
||||
// 理由:硬盘损坏、存储满等故障属于硬件层面,软件无法修复
|
||||
CameraErrorCode.HardwareFault or
|
||||
CameraErrorCode.StorageError or
|
||||
CameraErrorCode.DiskFull or
|
||||
CameraErrorCode.DiskReadOnly => RecoveryAction.ManualIntervention,
|
||||
|
||||
// ========== 场景 E: 正常状态 ==========
|
||||
CameraErrorCode.Success => RecoveryAction.None,
|
||||
|
||||
// ========== 场景 F: 未知错误 (关键修复 Bug R) ==========
|
||||
// 旧策略:盲目重试 → 新策略:人工干预
|
||||
// 理由:未知错误可能包含 IP 拉黑、协议不兼容等严重问题,重试会扩大风险
|
||||
_ => RecoveryAction.ManualIntervention
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 辅助算法:指数退避延迟计算 ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取建议的指数退避延迟时间(毫秒)
|
||||
/// 算法公式:delay = min(2^n * 1000, 120000),n = 当前重试次数
|
||||
/// 限流规则:第一次 2s → 第二次 4s → ... → 第六次 64s → 上限 120s(2分钟)
|
||||
/// </summary>
|
||||
/// <param name="retryCount">当前重试次数(从 1 开始计数)</param>
|
||||
/// <returns>延迟毫秒数</returns>
|
||||
public static int GetRetryDelay(int retryCount)
|
||||
{
|
||||
// 限制重试次数最大为 7,防止指数爆炸导致数值溢出
|
||||
int exponent = Math.Min(retryCount, 7);
|
||||
// 计算指数退避秒数
|
||||
int delaySeconds = (int)Math.Pow(2, exponent);
|
||||
// 转换为毫秒并限制上限为 2 分钟(120000ms)
|
||||
return Math.Min(delaySeconds * 1000, 120000);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
91
SHH.CameraSdk/Abstractions/IVideoSource.cs
Normal file
91
SHH.CameraSdk/Abstractions/IVideoSource.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// [核心契约] 工业级视频源接口 (V3.3.1 终极定稿)
|
||||
/// 核心职责:定义所有视频源设备的标准化生命周期、状态观测与数据分发能力
|
||||
/// 关键修复:
|
||||
/// <para>1. [Fix Bug δ] 新增 UpdateConfig 接口,支持运行时配置热更新</para>
|
||||
/// <para>2. 强化资源管理契约:继承 IDisposable/IAsyncDisposable,规范非托管资源释放</para>
|
||||
/// 适用场景:海康/大华/宇视等不同品牌相机的驱动适配、统一管理
|
||||
/// </summary>
|
||||
public interface IVideoSource : IDisposable, IAsyncDisposable
|
||||
{
|
||||
#region --- 1. 只读属性 (设备标识与状态观测) ---
|
||||
|
||||
/// <summary> 设备唯一业务标识(全局唯一,如数据库自增ID) </summary>
|
||||
long Id { get; }
|
||||
|
||||
/// <summary> 设备详细逻辑状态(如 Idle/Connecting/Playing/Faulted) </summary>
|
||||
VideoSourceStatus Status { get; }
|
||||
|
||||
/// <summary> 用户意图标识:是否需要保持设备运行状态 </summary>
|
||||
bool IsRunning { get; set; }
|
||||
|
||||
/// <summary> 设备物理在线状态(基于心跳/探测的实时感知结果) </summary>
|
||||
bool IsOnline { get; }
|
||||
|
||||
/// <summary> 设备能力元数据(只读,如分辨率、码流类型、支持的功能集) </summary>
|
||||
DeviceMetadata Metadata { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 事件契约 (数据分发与状态通知) ---
|
||||
|
||||
/// <summary>
|
||||
/// 视频帧接收事件(热路径,高频触发)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 1. 载荷类型:通常为 <see cref="OpenCvSharp.Mat"/> 或 <see cref="SmartFrame"/> 对象
|
||||
/// 2. 内存管理:订阅者必须负责载荷对象的 Dispose 操作,否则会导致内存泄漏
|
||||
/// 3. 性能约束:事件处理逻辑需控制在 10ms 内,避免阻塞取流线程
|
||||
/// </remarks>
|
||||
event Action<object>? FrameReceived;
|
||||
|
||||
/// <summary>
|
||||
/// 设备状态变更通知事件(结构化状态同步)
|
||||
/// </summary>
|
||||
/// <remarks> 携带状态变更前后的详细信息,用于监控告警、日志记录 </remarks>
|
||||
event EventHandler<StatusChangedEventArgs> StatusChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3. 核心方法 (生命周期与配置管理) ---
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动设备(完整流程:连接设备 → 登录鉴权 → 启动码流接收)
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">设备状态非法时抛出</exception>
|
||||
/// <exception cref="SdkCommunicationException">SDK 通信失败时抛出</exception>
|
||||
Task StartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步停止设备(完整流程:停止码流 → 登出设备 → 释放连接资源)
|
||||
/// </summary>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// [Fix Bug δ] 运行时更新设备配置
|
||||
/// </summary>
|
||||
/// <param name="newConfig">新的设备配置(如 IP、端口、用户名密码)</param>
|
||||
/// <remarks>
|
||||
/// 1. 生效机制:新配置不会立即生效,将在下次启动或自动重连时应用
|
||||
/// 2. 原子性保证:配置更新为原子操作,不会出现部分生效的情况
|
||||
/// 3. 适用场景:设备 IP 变更、密码修改等运维场景
|
||||
/// </remarks>
|
||||
void UpdateConfig(VideoSourceConfig newConfig);
|
||||
|
||||
/// <summary>
|
||||
/// 应用动态流配置补丁(无需重启,实时生效)
|
||||
/// </summary>
|
||||
/// <param name="options">动态流参数(如主码流/子码流切换、分辨率调整)</param>
|
||||
/// <remarks> 适用于运行时按需调整码流参数,降低带宽占用 </remarks>
|
||||
void ApplyOptions(DynamicStreamOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// 强制刷新设备元数据,并返回元数据变更差异
|
||||
/// </summary>
|
||||
/// <returns>元数据变更差异对象(如分辨率变化、功能集变化)</returns>
|
||||
Task<MetadataDiff> RefreshMetadataAsync();
|
||||
|
||||
#endregion
|
||||
}
|
||||
107
SHH.CameraSdk/Abstractions/Models/ChannelMetadata.cs
Normal file
107
SHH.CameraSdk/Abstractions/Models/ChannelMetadata.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 通道级能力描述(镜头身份证)
|
||||
/// 核心职责:描述单个物理镜头或 NVR 通道的技术参数、功能支持特性与分辨率能力
|
||||
/// 协作场景:作为 <see cref="DeviceMetadata"/> 的子级数据,支撑设备能力自发现、配置合法性校验
|
||||
/// </summary>
|
||||
public class ChannelMetadata
|
||||
{
|
||||
#region --- 1. 通道基础标识 (Basic Identification) ---
|
||||
|
||||
/// <summary>
|
||||
/// 物理通道索引(从 1 开始计数,与 DVR/NVR 物理通道号一一对应)
|
||||
/// 业务用途:作为通道唯一标识,用于码流订阅、参数配置
|
||||
/// </summary>
|
||||
public int ChannelIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 通道名称(用于 OSD 叠加显示、UI 界面展示)
|
||||
/// 示例:"北大门主入口" "地下车库A区"
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 功能支持特性 (Capability Support) ---
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持云台控制(PTZ:Pan/Tilt/Zoom 平移/俯仰/变焦)
|
||||
/// 业务影响:决定 UI 是否显示云台控制按钮,是否允许下发 PTZ 指令
|
||||
/// </summary>
|
||||
public bool SupportPtz { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持音频输入(是否接入拾音器)
|
||||
/// 业务影响:决定是否开启音频解码、音频流推送功能
|
||||
/// </summary>
|
||||
public bool SupportAudioIn { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持 AI 智能分析(人脸检测、车牌识别、行为分析等)
|
||||
/// 业务影响:决定是否加载 AI 算法插件,是否接收智能事件上报
|
||||
/// </summary>
|
||||
public bool SupportAiAnalysis { get; init; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3. 分辨率能力 (Resolution Capabilities) ---
|
||||
|
||||
/// <summary>
|
||||
/// 通道支持的分辨率列表(只读集合,防止外部篡改)
|
||||
/// 格式示例:["1920x1080", "1280x720", "3840x2160"]
|
||||
/// 业务用途:前端清晰度选择下拉列表、动态配置分辨率合法性校验
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<string> SupportedResolutions { get; init; } = new ReadOnlyCollection<string>(new List<string>());
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 4. 构造函数 (Constructors) ---
|
||||
|
||||
/// <summary>
|
||||
/// 默认构造函数(用于序列化、初始状态初始化)
|
||||
/// </summary>
|
||||
public ChannelMetadata() { }
|
||||
|
||||
/// <summary>
|
||||
/// 简化构造函数(用于快速创建通道标识、克隆或比对场景)
|
||||
/// </summary>
|
||||
/// <param name="index">物理通道索引</param>
|
||||
/// <param name="name">通道名称</param>
|
||||
public ChannelMetadata(int index, string name)
|
||||
{
|
||||
ChannelIndex = index;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整构造函数(用于创建包含全量能力的通道元数据)
|
||||
/// </summary>
|
||||
/// <param name="index">物理通道索引</param>
|
||||
/// <param name="name">通道名称</param>
|
||||
/// <param name="supportPtz">是否支持云台</param>
|
||||
/// <param name="supportAudio">是否支持音频输入</param>
|
||||
/// <param name="supportAi">是否支持 AI 分析</param>
|
||||
/// <param name="resolutions">支持的分辨率列表</param>
|
||||
public ChannelMetadata(
|
||||
int index,
|
||||
string name,
|
||||
bool supportPtz = false,
|
||||
bool supportAudio = false,
|
||||
bool supportAi = false,
|
||||
IEnumerable<string>? resolutions = null)
|
||||
{
|
||||
ChannelIndex = index;
|
||||
Name = name;
|
||||
SupportPtz = supportPtz;
|
||||
SupportAudioIn = supportAudio;
|
||||
SupportAiAnalysis = supportAi;
|
||||
SupportedResolutions = new ReadOnlyCollection<string>(resolutions?.ToList() ?? new List<string>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
176
SHH.CameraSdk/Abstractions/Models/DeviceMetadata.cs
Normal file
176
SHH.CameraSdk/Abstractions/Models/DeviceMetadata.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 增强型设备元数据中心 (V3.3.1 修复版)
|
||||
/// 核心职责:
|
||||
/// <para>1. 封装设备的硬件参数、通道能力、功能集,提供能力自发现</para>
|
||||
/// <para>2. 支持元数据同步与差异对比,指导上层模块执行差异化处理</para>
|
||||
/// <para>3. 存储运维指标与 SDK 原生句柄,支撑故障诊断与性能调优</para>
|
||||
/// 设计特性:只读优先,通过版本号标记同步状态,避免并发修改冲突
|
||||
/// </summary>
|
||||
public class DeviceMetadata
|
||||
{
|
||||
#region --- 1. 设备级身份信息 (Identity) ---
|
||||
|
||||
/// <summary> 设备型号名称(如 DS-2CD3T47G2-LIU) </summary>
|
||||
public string ModelName { get; init; } = "Unknown";
|
||||
|
||||
/// <summary> 设备唯一序列号(全局唯一,用于设备溯源) </summary>
|
||||
public string SerialNumber { get; init; } = string.Empty;
|
||||
|
||||
/// <summary> 固件/系统版本号(用于判断 SDK 兼容性) </summary>
|
||||
public string FirmwareVersion { get; init; } = string.Empty;
|
||||
|
||||
/// <summary> 所属厂商/品牌(决定驱动适配逻辑) </summary>
|
||||
public DeviceBrand Brand { get; init; } = DeviceBrand.Unknown;
|
||||
|
||||
/// <summary> 元数据版本号(本地刷新计数,每次同步自增) </summary>
|
||||
public long Version { get; private set; }
|
||||
|
||||
/// <summary> 最后同步时间(标记元数据的最新有效时刻) </summary>
|
||||
public DateTime LastSyncedAt { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 级联能力模型 (Cascaded Capabilities) ---
|
||||
|
||||
private readonly ReadOnlyCollection<ChannelMetadata> _channels;
|
||||
|
||||
/// <summary> 通道元数据集合(只读,防止外部篡改) </summary>
|
||||
public ReadOnlyCollection<ChannelMetadata> Channels
|
||||
{
|
||||
get => _channels;
|
||||
init => _channels = value ?? new ReadOnlyCollection<ChannelMetadata>(new List<ChannelMetadata>());
|
||||
}
|
||||
|
||||
/// <summary> 设备总通道数量(IPC 通常为 1,NVR 为接入路数) </summary>
|
||||
public int ChannelCount => Channels.Count;
|
||||
|
||||
/// <summary> 索引器:通过通道号快速获取对应通道的能力描述 </summary>
|
||||
/// <param name="channelIndex">物理通道号</param>
|
||||
/// <returns>通道元数据 / 不存在则返回 null</returns>
|
||||
public ChannelMetadata? this[int channelIndex] =>
|
||||
Channels.FirstOrDefault(c => c.ChannelIndex == channelIndex);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3. 运维指标与非托管句柄 ---
|
||||
|
||||
/// <summary> 设备实时健康度字典(如 CPU 使用率、温度、内存占用等) </summary>
|
||||
public Dictionary<string, object> HealthMetrics { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 厂商 SDK 原始句柄/结构体快照
|
||||
/// <para>注意事项:</para>
|
||||
/// <para>1. 标记 [JsonIgnore] 防止序列化非托管指针导致程序崩溃</para>
|
||||
/// <para>2. 仅用于驱动层与 SDK 交互,上层业务禁止直接操作</para>
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public object? NativeHandle { get; init; }
|
||||
|
||||
/// <summary> 厂商扩展标签(如设备位置、安装时间、责任人等自定义信息) </summary>
|
||||
public Dictionary<string, string> Tags { get; init; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 4. 构造与同步 (Constructor & Sync) ---
|
||||
|
||||
/// <summary>
|
||||
/// 默认构造函数(用于 BaseVideoSource 初始状态,无通道数据)
|
||||
/// </summary>
|
||||
public DeviceMetadata() : this(Enumerable.Empty<ChannelMetadata>()) { }
|
||||
|
||||
/// <summary>
|
||||
/// 完整构造函数(初始化通道元数据集合)
|
||||
/// </summary>
|
||||
/// <param name="channels">通道元数据列表</param>
|
||||
public DeviceMetadata(IEnumerable<ChannelMetadata> channels)
|
||||
{
|
||||
// 转换为只读集合,确保通道数据不可变
|
||||
_channels = new ReadOnlyCollection<ChannelMetadata>(channels?.ToList() ?? new List<ChannelMetadata>());
|
||||
// 标记初始同步状态
|
||||
MarkSynced();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记元数据同步完成
|
||||
/// <para>作用:更新版本号与同步时间,用于差异对比的基准判断</para>
|
||||
/// </summary>
|
||||
public void MarkSynced()
|
||||
{
|
||||
Version++;
|
||||
LastSyncedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 5. 业务逻辑辅助方法 (Business Helpers) ---
|
||||
|
||||
/// <summary>
|
||||
/// 校验动态流配置的合法性(基于设备能力)
|
||||
/// </summary>
|
||||
/// <param name="options">待校验的动态配置项</param>
|
||||
/// <param name="errorMessage">校验失败时的详细原因</param>
|
||||
/// <returns>合法返回 true,非法返回 false</returns>
|
||||
public bool ValidateOptions(DynamicStreamOptions options, out string errorMessage)
|
||||
{
|
||||
errorMessage = string.Empty;
|
||||
if (options == null) return true;
|
||||
|
||||
// 示例校验规则:云台控制权限校验
|
||||
if (options.VendorExtensions?.ContainsKey("PtzAction") == true
|
||||
&& !Channels.Any(c => c.SupportPtz))
|
||||
{
|
||||
errorMessage = "该设备物理硬件不支持云台控制功能";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 可扩展其他校验规则:如分辨率合法性、码流类型支持性等
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 元数据差异比对逻辑(用于 BaseVideoSource.RefreshMetadataAsync 方法)
|
||||
/// </summary>
|
||||
/// <param name="other">最新拉取的设备元数据</param>
|
||||
/// <returns>元数据差异描述符</returns>
|
||||
public MetadataDiff CompareWith(DeviceMetadata other)
|
||||
{
|
||||
// 入参防护:对比对象为空则返回无差异
|
||||
if (other == null) return MetadataDiff.None;
|
||||
|
||||
return new MetadataDiff
|
||||
{
|
||||
// 1. 基础信息变更:任意通道名称变化则标记
|
||||
NameChanged = this.Channels.Any(c =>
|
||||
{
|
||||
var targetChannel = other[c.ChannelIndex];
|
||||
return targetChannel != null && targetChannel.Name != c.Name;
|
||||
}),
|
||||
|
||||
// 2. 能力集变更:PTZ/音频/AI 功能支持状态变化则标记
|
||||
CapabilityChanged = this.Channels.Any(c =>
|
||||
{
|
||||
var targetChannel = other[c.ChannelIndex];
|
||||
if (targetChannel == null) return false;
|
||||
|
||||
return targetChannel.SupportPtz != c.SupportPtz
|
||||
|| targetChannel.SupportAudioIn != c.SupportAudioIn
|
||||
|| targetChannel.SupportAiAnalysis != c.SupportAiAnalysis;
|
||||
}),
|
||||
|
||||
// 3. 致命变更:品牌不一致或通道数量变化(设备更换/替换场景)
|
||||
IsMajorChange = this.Brand != other.Brand || this.ChannelCount != other.ChannelCount,
|
||||
|
||||
// 4. 分辨率配置变更:任意通道的分辨率档位数量变化则标记
|
||||
ResolutionProfilesChanged = this.Channels.Any(c =>
|
||||
{
|
||||
var targetChannel = other[c.ChannelIndex];
|
||||
return targetChannel != null
|
||||
&& targetChannel.SupportedResolutions.Count != c.SupportedResolutions.Count;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
120
SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs
Normal file
120
SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频流动态配置项(运行时可调整参数容器)
|
||||
/// 核心职责:承载无需重启流即可动态调整的视频参数,支持局部更新
|
||||
/// 核心特性:
|
||||
/// <para>1. Nullable 模式:仅非空字段会触发参数更新,避免全量重置导致的性能抖动</para>
|
||||
/// <para>2. 分类管理:按画面策略、帧率控制、传输输出、厂商扩展划分参数,逻辑清晰</para>
|
||||
/// <para>3. 空值检查:通过 IsEmpty 判断是否存在有效配置,避免无效底层 SDK 调用</para>
|
||||
/// </summary>
|
||||
public class DynamicStreamOptions
|
||||
{
|
||||
#region --- 1. 画面策略 (Resolution & Scaling) ---
|
||||
|
||||
/// <summary>
|
||||
/// 目标输出宽度(像素)
|
||||
/// <para>Nullable 规则:null = 保持当前配置;非 null = 触发图像缩放逻辑</para>
|
||||
/// <para>注意事项:建议与 TargetHeight 成对设置,避免画面比例失衡</para>
|
||||
/// </summary>
|
||||
public int? TargetWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标输出高度(像素)
|
||||
/// <para>Nullable 规则:null = 保持当前配置;非 null = 触发图像缩放逻辑</para>
|
||||
/// <para>协作关系:与 TargetWidth 配合使用,若仅设置其一,会按原始宽高比自动计算另一值</para>
|
||||
/// </summary>
|
||||
public int? TargetHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图像放大开关
|
||||
/// <para>Nullable 规则:null = 保持当前策略;true = 允许放大;false = 禁止放大</para>
|
||||
/// <para>性能影响:禁止放大可节省插值计算资源,适合低性能设备</para>
|
||||
/// </summary>
|
||||
public bool? AllowEnlarge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图像缩小开关
|
||||
/// <para>Nullable 规则:null = 保持当前策略;true = 允许缩小;false = 禁止缩小</para>
|
||||
/// <para>适用场景:禁止缩小可保留原始画质,适合需要高清分析的场景</para>
|
||||
/// </summary>
|
||||
public bool? AllowShrink { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 频率控制 (Frame Rate Control) ---
|
||||
|
||||
/// <summary>
|
||||
/// 目标渲染/显示帧率(fps)
|
||||
/// <para>Nullable 规则:null = 不修改;0 = 跟随原始流速度;非 0 = 强制限定显示帧率</para>
|
||||
/// <para>作用域:仅影响 UI 预览层,不会改变底层码流的采集帧率</para>
|
||||
/// </summary>
|
||||
public int? TargetDisplayFps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标 AI 分析帧率(fps)
|
||||
/// <para>Nullable 规则:null = 不修改;非 null = 限定算法处理的输入帧率</para>
|
||||
/// <para>性能优化:降低此值可显著减少高分辨率下的 GPU/CPU 负荷(如 4K 从 30fps 降到 5fps)</para>
|
||||
/// </summary>
|
||||
public int? TargetAnalyzeFps { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3. 传输与输出 (Transmission & Output) ---
|
||||
|
||||
/// <summary>
|
||||
/// Web 推流开关
|
||||
/// <para>Nullable 规则:null = 保持当前状态;true = 启动推流;false = 停止推流</para>
|
||||
/// <para>协作组件:开启后会将处理后的视频帧推送到流媒体服务器(如 FFmpeg/RTSP 服务器)</para>
|
||||
/// </summary>
|
||||
public bool? EnableStreamOutput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渲染窗体句柄
|
||||
/// <para>Nullable 规则:null = 保持当前窗口;非 null = 切换到新窗口渲染</para>
|
||||
/// <para>适用场景:支持视频窗口拖拽、多显示器切换等交互操作</para>
|
||||
/// </summary>
|
||||
public IntPtr? RenderHandle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 码流类型切换
|
||||
/// <para>取值规则:0 = 主码流(高清/大带宽);1 = 子码流(标清/低延迟);2 = 第三码流</para>
|
||||
/// <para>Nullable 规则:null = 不切换;非 null = 执行码流切换</para>
|
||||
/// <para>注意事项:切换会销毁并重建预览句柄,可能导致短暂的画面中断</para>
|
||||
/// </summary>
|
||||
public int? StreamType { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 4. 厂商扩展 (Vendor Specific) ---
|
||||
|
||||
/// <summary>
|
||||
/// 厂商特有参数扩展字典
|
||||
/// <para>用途:存放无法标准化的品牌专属功能参数</para>
|
||||
/// <para>示例:海康 "FocusMode"=Auto/Manual;大华 "SmartH264"=true/false</para>
|
||||
/// <para>注意事项:键值对需与对应厂商 SDK 的参数名一致,否则无效</para>
|
||||
/// </summary>
|
||||
public Dictionary<string, object> VendorExtensions { get; set; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 5. 配置有效性检查 ---
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑空检查:判断当前配置包是否包含任何有效修改项
|
||||
/// <para>使用场景:调用 SDK 前判断,避免无意义的底层调用,提升性能</para>
|
||||
/// </summary>
|
||||
public bool IsEmpty =>
|
||||
TargetWidth is null &&
|
||||
TargetHeight is null &&
|
||||
AllowEnlarge is null &&
|
||||
AllowShrink is null &&
|
||||
TargetDisplayFps is null &&
|
||||
TargetAnalyzeFps is null &&
|
||||
EnableStreamOutput is null &&
|
||||
RenderHandle is null &&
|
||||
StreamType is null &&
|
||||
VendorExtensions.Count == 0;
|
||||
|
||||
#endregion
|
||||
}
|
||||
53
SHH.CameraSdk/Abstractions/Models/MetadataDiff.cs
Normal file
53
SHH.CameraSdk/Abstractions/Models/MetadataDiff.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 元数据变更差异描述符(只读结构体)
|
||||
/// 核心职责:对比设备当前运行元数据与最新拉取元数据的差异,明确变更类型及业务影响
|
||||
/// 协作场景:指导上层模块执行差异化处理(如仅刷新UI、重启流、调整功能按钮)
|
||||
/// </summary>
|
||||
public readonly struct MetadataDiff
|
||||
{
|
||||
#region --- 差异类型属性 (Change Type Properties) ---
|
||||
|
||||
/// <summary>
|
||||
/// 设备基础描述信息变更(如名称、位置)
|
||||
/// 业务影响:仅需刷新 UI 显示文字,无需中断当前流
|
||||
/// </summary>
|
||||
public bool NameChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备能力集变更(如新增/移除对讲、截图功能)
|
||||
/// 业务影响:需调整 UI 功能按钮的可用性,无需中断流
|
||||
/// </summary>
|
||||
public bool CapabilityChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 致命/破坏性变更(如设备型号、编码格式变更)
|
||||
/// 业务影响:必须销毁当前流实例并重启,否则会导致流异常或崩溃
|
||||
/// </summary>
|
||||
public bool IsMajorChange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率/帧率档位列表变更
|
||||
/// 业务影响:需重新校验当前流参数是否合法,非法则自动降级到可用档位
|
||||
/// </summary>
|
||||
public bool ResolutionProfilesChanged { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 全局变更标识:是否存在任何类型的元数据变更
|
||||
/// 业务用途:快速判断是否需要执行后续差异化处理逻辑
|
||||
/// </summary>
|
||||
public bool HasChanges => NameChanged || CapabilityChanged || IsMajorChange || ResolutionProfilesChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 快捷实例 (Quick Instances) ---
|
||||
|
||||
/// <summary>
|
||||
/// 无变更状态的快捷实例
|
||||
/// 业务用途:元数据刷新后无变化时直接返回,避免重复创建空对象
|
||||
/// </summary>
|
||||
public static MetadataDiff None => new();
|
||||
|
||||
#endregion
|
||||
}
|
||||
57
SHH.CameraSdk/Abstractions/Models/ResolutionProfile.cs
Normal file
57
SHH.CameraSdk/Abstractions/Models/ResolutionProfile.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频分辨率档位描述符(Record 类型,不可变对象)
|
||||
/// 核心职责:定义相机通道在特定编码格式下支持的分辨率、帧率上限及友好描述
|
||||
/// 协作场景:
|
||||
/// <para>1. 前端界面:展示清晰度选择下拉列表</para>
|
||||
/// <para>2. 配置校验:下发分辨率前判断是否符合硬件能力,防止超限黑屏</para>
|
||||
/// <para>3. 性能评估:通过总像素量计算编码/解码的计算负载与带宽压力</para>
|
||||
/// </summary>
|
||||
/// <param name="Width">画面像素宽度(如 1920、3840)</param>
|
||||
/// <param name="Height">画面像素高度(如 1080、2160)</param>
|
||||
/// <param name="MaxFps">该分辨率下硬件支持的最大输出帧率(防止超限配置)</param>
|
||||
/// <param name="Description">档位友好描述(如 "高清(1080P/H.265)")</param>
|
||||
public record ResolutionProfile(
|
||||
int Width,
|
||||
int Height,
|
||||
int MaxFps,
|
||||
string Description
|
||||
)
|
||||
{
|
||||
#region --- 计算属性 (Derived Properties) ---
|
||||
|
||||
/// <summary>
|
||||
/// 当前档位的总像素量
|
||||
/// 业务用途:衡量编码/解码的计算负载、网络传输的带宽压力
|
||||
/// 计算公式:Width * Height
|
||||
/// </summary>
|
||||
public long TotalPixels => (long)Width * Height;
|
||||
|
||||
/// <summary>
|
||||
/// 当前档位的屏幕宽高比
|
||||
/// 业务用途:前端渲染容器(WinForm/WPF/Web)自动调整比例,避免画面拉伸变形
|
||||
/// 保护逻辑:高度为 0 时返回 0,防止除零异常
|
||||
/// </summary>
|
||||
public double AspectRatio => Height == 0 ? 0 : (double)Width / Height;
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前档位是否属于高清范畴(行业标准:720P 及以上,即 1280x720 分辨率)
|
||||
/// 业务用途:前端分类展示、带宽策略选择
|
||||
/// </summary>
|
||||
public bool IsHighDefinition => Width >= 1280 && Height >= 720;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 重写方法 (Overridden Methods) ---
|
||||
|
||||
/// <summary>
|
||||
/// 格式化分辨率档位的显示文本
|
||||
/// 输出格式:友好描述 (宽x高@最大帧率fps)
|
||||
/// 示例:高清(1080P/H.265) (1920x1080@30fps)
|
||||
/// </summary>
|
||||
/// <returns>格式化的显示字符串</returns>
|
||||
public override string ToString() => $"{Description} ({Width}x{Height}@{MaxFps}fps)";
|
||||
|
||||
#endregion
|
||||
}
|
||||
80
SHH.CameraSdk/Abstractions/Models/StatusChangedEventArgs.cs
Normal file
80
SHH.CameraSdk/Abstractions/Models/StatusChangedEventArgs.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频源状态变更事件参数
|
||||
/// 核心职责:封装状态迁移的完整上下文信息,支撑三大业务场景
|
||||
/// <para>1. UI 层:实时反馈设备状态、显示错误提示</para>
|
||||
/// <para>2. 诊断层:记录异常堆栈、SDK 错误码,辅助问题定位</para>
|
||||
/// <para>3. 运维层:触发自动重连、告警推送等自动化决策</para>
|
||||
/// </summary>
|
||||
public class StatusChangedEventArgs : EventArgs
|
||||
{
|
||||
#region --- 事件核心属性 (Event Core Properties) ---
|
||||
|
||||
/// <summary>
|
||||
/// 变更后的目标状态
|
||||
/// 业务用途:
|
||||
/// 1. UI 层:控制状态图标颜色(如 Playing→绿色、Faulted→红色)
|
||||
/// 2. 运维层:状态为 Reconnecting 时触发重连策略调整
|
||||
/// </summary>
|
||||
public VideoSourceStatus NewStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态描述文本(可读)
|
||||
/// 业务用途:
|
||||
/// 1. UI 层:直接显示在状态栏或操作日志面板
|
||||
/// 2. 日志层:写入业务日志,便于人工排查
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的异常对象(可选)
|
||||
/// 业务用途:仅当 NewStatus = Faulted 时有效,提供异常堆栈、类型等代码级诊断信息
|
||||
/// </summary>
|
||||
public Exception? Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// SDK 底层原始错误码(可选)
|
||||
/// 业务用途:
|
||||
/// 1. 厂商适配:匹配海康 NET_DVR_GetLastError、大华 SDK 错误码等
|
||||
/// 2. 精准诊断:区分“用户锁定(153)”“密码错误(41)”“网络超时”等根因
|
||||
/// </summary>
|
||||
public int? LastErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 变更后的新句柄(可选)
|
||||
/// 业务用途:渲染器解绑/重绑场景,监听此值更新窗口句柄绑定关系
|
||||
/// </summary>
|
||||
public IntPtr? NewHandle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态变更发生的时间戳
|
||||
/// 业务用途:日志时序排序、状态迁移耗时统计
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; } = DateTime.Now;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 (Constructor) ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化状态变更事件参数
|
||||
/// </summary>
|
||||
/// <param name="status">变更后的目标状态</param>
|
||||
/// <param name="msg">可读的状态描述文本</param>
|
||||
/// <param name="ex">可选:关联的异常对象</param>
|
||||
/// <param name="errorCode">可选:SDK 底层错误码</param>
|
||||
public StatusChangedEventArgs(
|
||||
VideoSourceStatus status,
|
||||
string msg,
|
||||
Exception? ex = null,
|
||||
int? errorCode = null)
|
||||
{
|
||||
NewStatus = status;
|
||||
Message = msg;
|
||||
Exception = ex;
|
||||
LastErrorCode = errorCode;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
150
SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs
Normal file
150
SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频源基础配置对象 (V3.3.1 修复版)
|
||||
/// 核心职责:定义建立相机物理连接所需的所有标准化参数与厂商扩展参数
|
||||
/// 关键修复:
|
||||
/// <para>1. [Fix Bug U: 配置漂移] 驱动层接收配置时需创建副本,防止外部修改导致的连接异常</para>
|
||||
/// <para>2. 强化配置有效性校验,提前拦截非法参数</para>
|
||||
/// 注意事项:此类为引用类型,传递时建议使用深拷贝
|
||||
/// </summary>
|
||||
public class VideoSourceConfig
|
||||
{
|
||||
#region --- 1. 核心连接配置 (Core Connection Configurations) ---
|
||||
|
||||
/// <summary> 业务系统唯一标识(对应数据库自增ID,全局唯一) </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary> 设备显示名称(如:北大门-枪机01,用于前端展示) </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> 设备品牌(决定加载对应的驱动实现类) </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public DeviceBrand Brand { get; set; } = DeviceBrand.Unknown;
|
||||
|
||||
/// <summary> 设备 IP 地址或域名(如 192.168.1.100 或 camera.example.com) </summary>
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> 设备端口号(厂商默认值:海康8000、大华37777、RTSP 554) </summary>
|
||||
public ushort Port { get; set; }
|
||||
|
||||
/// <summary> 设备登录用户名(默认值:admin) </summary>
|
||||
public string Username { get; set; } = "admin";
|
||||
|
||||
/// <summary> 设备登录密码(默认空字符串,需根据实际设备配置) </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> 渲染句柄(可选):用于硬解码时直接绑定显示窗口,提升渲染性能 </summary>
|
||||
public IntPtr RenderHandle { get; set; } = IntPtr.Zero;
|
||||
|
||||
/// <summary> 物理通道号(IPC 通常为 1;NVR 对应接入的摄像头通道索引) </summary>
|
||||
public int ChannelIndex { get; set; } = 1;
|
||||
|
||||
/// <summary> 默认码流类型(0 = 主码流(高清),1 = 子码流(低带宽)) </summary>
|
||||
public int StreamType { get; set; } = 0;
|
||||
|
||||
/// <summary> 传输协议(TCP/UDP/Multicast,默认 TCP 保证可靠性) </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public TransportProtocol Transport { get; set; } = TransportProtocol.Tcp;
|
||||
|
||||
/// <summary> 连接超时时间(毫秒,默认 5000ms = 5秒) </summary>
|
||||
public int ConnectionTimeoutMs { get; set; } = 5000;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 2. 厂商扩展配置 (Vendor-Specific Extensions) ---
|
||||
|
||||
/// <summary>
|
||||
/// 厂商扩展参数字典
|
||||
/// 用途:存储无法标准化的品牌专属参数
|
||||
/// 示例:
|
||||
/// <code>
|
||||
/// {
|
||||
/// "RtspPath": "/h264/ch1/main/av_stream",
|
||||
/// "HikLoginMode": "ISAPI",
|
||||
/// "DaHuaStreamProtocol": "HTTP"
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public Dictionary<string, string> VendorArguments { get; set; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3. 配置工具方法 (Configuration Utility Methods) ---
|
||||
|
||||
/// <summary>
|
||||
/// 配置有效性校验:检查核心参数是否合法,非法则抛出异常
|
||||
/// 作用:提前拦截无效配置,避免驱动层连接时出现未知错误
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException">核心参数非法时抛出</exception>
|
||||
public void Validate()
|
||||
{
|
||||
if (Id <= 0)
|
||||
throw new ArgumentException("配置ID必须为正整数", nameof(Id));
|
||||
if (string.IsNullOrWhiteSpace(IpAddress))
|
||||
throw new ArgumentException("IP地址不能为空", nameof(IpAddress));
|
||||
if (Port == 0)
|
||||
throw new ArgumentException("端口号必须为有效数值", nameof(Port));
|
||||
if (Brand == DeviceBrand.Unknown)
|
||||
throw new ArgumentException("必须指定设备品牌", nameof(Brand));
|
||||
if (ChannelIndex <= 0)
|
||||
throw new ArgumentException("通道号必须为正整数", nameof(ChannelIndex));
|
||||
if (ConnectionTimeoutMs <= 0)
|
||||
throw new ArgumentException("连接超时时间必须大于0", nameof(ConnectionTimeoutMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成设备唯一连接指纹
|
||||
/// 用途:用于连接池去重、缓存键、日志标识等场景
|
||||
/// 格式:{Brand}://{Username}@{IpAddress}:{Port}/{ChannelIndex}
|
||||
/// </summary>
|
||||
/// <returns>唯一连接指纹字符串</returns>
|
||||
public string GetConnectionKey()
|
||||
{
|
||||
// 使用 StringBuilder 提升拼接性能,避免频繁创建字符串
|
||||
return new StringBuilder()
|
||||
.Append(Brand)
|
||||
.Append("://")
|
||||
.Append(Username)
|
||||
.Append('@')
|
||||
.Append(IpAddress)
|
||||
.Append(':')
|
||||
.Append(Port)
|
||||
.Append('/')
|
||||
.Append(ChannelIndex)
|
||||
.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建配置对象的深拷贝(防止外部修改导致配置漂移)
|
||||
/// </summary>
|
||||
/// <returns>新的配置副本</returns>
|
||||
public VideoSourceConfig DeepCopy()
|
||||
{
|
||||
var copy = new VideoSourceConfig
|
||||
{
|
||||
Id = this.Id,
|
||||
Name = this.Name,
|
||||
Brand = this.Brand,
|
||||
IpAddress = this.IpAddress,
|
||||
Port = this.Port,
|
||||
Username = this.Username,
|
||||
Password = this.Password,
|
||||
RenderHandle = this.RenderHandle,
|
||||
ChannelIndex = this.ChannelIndex,
|
||||
StreamType = this.StreamType,
|
||||
Transport = this.Transport,
|
||||
ConnectionTimeoutMs = this.ConnectionTimeoutMs
|
||||
};
|
||||
|
||||
// 深拷贝扩展参数字典
|
||||
foreach (var kvp in this.VendorArguments)
|
||||
{
|
||||
copy.VendorArguments.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
100
SHH.CameraSdk/Controllers/MonitorController.cs
Normal file
100
SHH.CameraSdk/Controllers/MonitorController.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频源实时状态监控 API 控制器
|
||||
/// 核心功能:提供相机设备遥测数据查询、单设备详情查询、设备截图获取接口
|
||||
/// 适用场景:Web 监控大屏、移动端状态查询、第三方系统集成
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MonitorController : ControllerBase
|
||||
{
|
||||
#region --- 依赖注入 (Dependency Injection) ---
|
||||
|
||||
/// <summary> 相机管理器实例:提供设备状态与遥测数据访问能力 </summary>
|
||||
private readonly CameraManager _cameraManager;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数:通过依赖注入获取 CameraManager 实例
|
||||
/// </summary>
|
||||
/// <param name="cameraManager">相机管理器</param>
|
||||
public MonitorController(CameraManager cameraManager)
|
||||
{
|
||||
_cameraManager = cameraManager;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- API 接口定义 (API Endpoints) ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取全量相机实时遥测数据快照(支持跨域)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 返回数据包含:设备ID、名称、IP地址、运行状态、在线状态、实时FPS、累计帧数、健康度评分、最后错误信息
|
||||
/// 适用场景:监控大屏首页数据看板
|
||||
/// [cite: 191, 194]
|
||||
/// </remarks>
|
||||
/// <returns>200 OK + 遥测数据列表</returns>
|
||||
[HttpGet("dashboard")]
|
||||
public IActionResult GetDashboard()
|
||||
{
|
||||
var telemetrySnapshot = _cameraManager.GetTelemetrySnapshot();
|
||||
return Ok(telemetrySnapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定相机的详细运行指标
|
||||
/// </summary>
|
||||
/// <param name="id">相机设备唯一标识</param>
|
||||
/// <returns>200 OK + 设备详情 | 404 Not Found</returns>
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult GetDeviceDetail(long id)
|
||||
{
|
||||
// 查询指定设备
|
||||
var device = _cameraManager.GetDevice(id);
|
||||
// 设备不存在返回 404
|
||||
if (device == null) return NotFound($"设备 ID: {id} 不存在");
|
||||
|
||||
// 构造设备详情返回对象
|
||||
var deviceDetail = new
|
||||
{
|
||||
device.Id,
|
||||
device.Status,
|
||||
device.IsOnline,
|
||||
device.RealFps,
|
||||
device.TotalFrames,
|
||||
device.Config.Name,
|
||||
device.Config.IpAddress
|
||||
};
|
||||
|
||||
return Ok(deviceDetail);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定相机的实时截图
|
||||
/// </summary>
|
||||
/// <param name="id">相机设备唯一标识</param>
|
||||
/// <returns>200 OK + JPEG 图片流 | 504 Gateway Timeout</returns>
|
||||
[HttpGet("snapshot/{id}")]
|
||||
public async Task<IActionResult> GetSnapshot(long id)
|
||||
{
|
||||
// 调用截图协调器获取实时截图,设置 2 秒超时
|
||||
// 超时保护:避免 HTTP 线程因设备异常长时间挂起
|
||||
var imageBytes = await SnapshotCoordinator.Instance.RequestSnapshotAsync(id, 2000);
|
||||
|
||||
// 截图超时或设备无响应,返回 504 超时状态码
|
||||
if (imageBytes == null)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status504GatewayTimeout, "截图请求超时或设备未响应");
|
||||
}
|
||||
|
||||
// 返回 JPEG 格式图片流,支持浏览器直接预览
|
||||
return File(imageBytes, "image/jpeg");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
171
SHH.CameraSdk/Core/Features/FrameConsumer.cs
Normal file
171
SHH.CameraSdk/Core/Features/FrameConsumer.cs
Normal 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
|
||||
}
|
||||
92
SHH.CameraSdk/Core/Features/SnapshotCoordinator.cs
Normal file
92
SHH.CameraSdk/Core/Features/SnapshotCoordinator.cs
Normal 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 = 设备ID,Value = 任务完成源,用于传递截图结果或超时信号 </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
|
||||
}
|
||||
233
SHH.CameraSdk/Core/Manager/CameraManager.cs
Normal file
233
SHH.CameraSdk/Core/Manager/CameraManager.cs
Normal 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
|
||||
}
|
||||
140
SHH.CameraSdk/Core/Memory/FramePool.cs
Normal file
140
SHH.CameraSdk/Core/Memory/FramePool.cs
Normal 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
|
||||
}
|
||||
95
SHH.CameraSdk/Core/Memory/SmartFrame.cs
Normal file
95
SHH.CameraSdk/Core/Memory/SmartFrame.cs
Normal 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
|
||||
}
|
||||
64
SHH.CameraSdk/Core/Pipeline/GlobalProcessingCenter.cs
Normal file
64
SHH.CameraSdk/Core/Pipeline/GlobalProcessingCenter.cs
Normal 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
|
||||
}
|
||||
157
SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs
Normal file
157
SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs
Normal 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 = 业务 AppId,Value = 帧处理多播委托
|
||||
/// 实现: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
|
||||
}
|
||||
148
SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs
Normal file
148
SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs
Normal 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
|
||||
}
|
||||
40
SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs
Normal file
40
SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs
Normal 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
|
||||
}
|
||||
231
SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs
Normal file
231
SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs
Normal 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
|
||||
}
|
||||
86
SHH.CameraSdk/Core/Scheduling/FrameController.cs
Normal file
86
SHH.CameraSdk/Core/Scheduling/FrameController.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 帧控制器(帧调度核心)
|
||||
/// 功能:管理订阅者的帧需求,基于需求动态判定每帧的处理命运(保留/丢弃、分发目标)
|
||||
/// 核心逻辑:采用“并集采样”策略,满足任意订阅者的帧率需求即保留帧,避免重复采样浪费资源
|
||||
/// </summary>
|
||||
public class FrameController
|
||||
{
|
||||
#region --- 私有资源与状态 (Private Resources & States) ---
|
||||
|
||||
/// <summary> 订阅者帧需求集合(线程安全) </summary>
|
||||
/// <remarks> Key:订阅者AppId,Value:该订阅者的帧需求配置 </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
|
||||
}
|
||||
34
SHH.CameraSdk/Core/Scheduling/FrameDecision.cs
Normal file
34
SHH.CameraSdk/Core/Scheduling/FrameDecision.cs
Normal 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
|
||||
}
|
||||
37
SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs
Normal file
37
SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs
Normal 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 预览需 8fps,AI 分析需 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
|
||||
}
|
||||
50
SHH.CameraSdk/Core/Telemetry/CameraHealthReport.cs
Normal file
50
SHH.CameraSdk/Core/Telemetry/CameraHealthReport.cs
Normal 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
|
||||
}
|
||||
56
SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs
Normal file
56
SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs
Normal 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
|
||||
}
|
||||
21
SHH.CameraSdk/Core/Telemetry/FrameConsumerType.cs
Normal file
21
SHH.CameraSdk/Core/Telemetry/FrameConsumerType.cs
Normal 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
|
||||
}
|
||||
61
SHH.CameraSdk/Core/Telemetry/FrameContext.cs
Normal file
61
SHH.CameraSdk/Core/Telemetry/FrameContext.cs
Normal 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
|
||||
}
|
||||
43
SHH.CameraSdk/Core/Telemetry/FrameTrace.cs
Normal file
43
SHH.CameraSdk/Core/Telemetry/FrameTrace.cs
Normal 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
|
||||
}
|
||||
66
SHH.CameraSdk/Core/Telemetry/GlobalTelemetry.cs
Normal file
66
SHH.CameraSdk/Core/Telemetry/GlobalTelemetry.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 全局遥测仓储(静态类)
|
||||
/// 功能:存储并提供帧生命周期的追踪日志查询,采用环形缓冲区机制限制日志数量
|
||||
/// 用途:用于问题排查、性能分析,记录每帧的处理流程、耗时、丢弃原因等信息
|
||||
/// </summary>
|
||||
public static class GlobalTelemetry
|
||||
{
|
||||
#region --- 静态存储资源 (Static Storage Resources) ---
|
||||
|
||||
/// <summary>
|
||||
/// 环形日志缓冲区:存储帧追踪上下文(线程安全)
|
||||
/// Key:帧序列号(FrameSequence),Value:帧全链路追踪上下文
|
||||
/// </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
|
||||
}
|
||||
512
SHH.CameraSdk/Drivers/BaseVideoSource.cs
Normal file
512
SHH.CameraSdk/Drivers/BaseVideoSource.cs
Normal file
@@ -0,0 +1,512 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版)
|
||||
/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。
|
||||
/// 修复记录:
|
||||
/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。
|
||||
/// 2. [Bug π] 管道安全:Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。
|
||||
/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。
|
||||
/// </summary>
|
||||
public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
|
||||
{
|
||||
#region --- 核心配置与锁机制 (Core Config & Locks) ---
|
||||
|
||||
// [Fix Bug δ] 核心配置对象
|
||||
// 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例
|
||||
protected VideoSourceConfig _config;
|
||||
|
||||
/// <summary>
|
||||
/// 状态同步锁
|
||||
/// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致的状态读取不一致
|
||||
/// </summary>
|
||||
private readonly object _stateSyncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// 生命周期互斥锁
|
||||
/// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致的状态机混乱
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _lifecycleLock = new(1, 1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 内部状态与基础设施 (Internal States & Infrastructure) ---
|
||||
|
||||
// 内部状态标志位
|
||||
private volatile bool _isOnline;
|
||||
private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// 状态通知队列 (有界)
|
||||
/// 特性:采用 DropOldest 策略,当消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β]
|
||||
/// </summary>
|
||||
private readonly Channel<StatusChangedEventArgs> _statusQueue;
|
||||
|
||||
// 状态分发器的取消令牌源
|
||||
private CancellationTokenSource? _distributorCts;
|
||||
|
||||
// [新增修复 Bug π] 分发任务引用
|
||||
// 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费
|
||||
private Task? _distributorTask;
|
||||
|
||||
// [Fix Bug V] 单调时钟
|
||||
// 作用:记录最后一次收到帧的系统 Tick,用于心跳检测,不受系统时间修改影响
|
||||
private long _lastFrameTick = 0;
|
||||
|
||||
/// <summary> 获取最后帧的时间戳 (线程安全读取) </summary>
|
||||
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
|
||||
|
||||
/// <summary> 视频帧回调事件 (热路径) </summary>
|
||||
public event Action<object>? FrameReceived;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 公开属性 (Public Properties) ---
|
||||
|
||||
public long Id => _config.Id;
|
||||
|
||||
public VideoSourceConfig Config => _config;
|
||||
|
||||
public VideoSourceStatus Status { get { lock (_stateSyncRoot) return _status; } }
|
||||
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
public bool IsOnline => _isOnline;
|
||||
|
||||
public DeviceMetadata Metadata { get; protected set; } = new();
|
||||
|
||||
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 遥测统计属性 (Telemetry Properties) ---
|
||||
|
||||
// [新增] 遥测统计专用字段
|
||||
private long _totalFramesReceived = 0; // 生命周期内总帧数
|
||||
private int _tempFrameCounter = 0; // 用于计算FPS的临时计数器
|
||||
private long _lastFpsCalcTick = 0; // 上次计算FPS的时间点
|
||||
private double _currentFps = 0.0; // 当前实时FPS
|
||||
|
||||
// [新增] 公开的遥测属性 (线程安全读取)
|
||||
public double RealFps => _currentFps;
|
||||
|
||||
public long TotalFrames => Interlocked.Read(ref _totalFramesReceived);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 (Constructor) ---
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数:初始化基础设施
|
||||
/// </summary>
|
||||
/// <param name="config">视频源基础配置(含设备连接信息、通道号等)</param>
|
||||
/// <exception cref="ArgumentNullException">配置为空时抛出</exception>
|
||||
protected BaseVideoSource(VideoSourceConfig config)
|
||||
{
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
// [Fix Bug U] 初始配置深拷贝
|
||||
// 防止外部引用修改导致内部状态不可控(配置防漂移)
|
||||
_config = CloneConfig(config);
|
||||
|
||||
// [Fix Bug β] 初始化有界通道
|
||||
// 容量 100,单读者多写者模式
|
||||
_statusQueue = Channel.CreateBounded<StatusChangedEventArgs>(new BoundedChannelOptions(100)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.DropOldest,
|
||||
SingleReader = true,
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
_distributorCts = new CancellationTokenSource();
|
||||
|
||||
// [关键逻辑] 启动后台状态分发循环
|
||||
// 明确持有 Task 引用,以便后续进行优雅关闭等待
|
||||
_distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 配置管理 (Config Management) ---
|
||||
|
||||
/// <summary>
|
||||
/// [修复 Bug δ] 更新配置实现
|
||||
/// 允许在不销毁实例的情况下更新 IP、端口等参数,新配置下次连接生效
|
||||
/// </summary>
|
||||
/// <param name="newConfig">新的视频源配置</param>
|
||||
public void UpdateConfig(VideoSourceConfig newConfig)
|
||||
{
|
||||
if (newConfig == null) return;
|
||||
|
||||
// 1. 获取生命周期锁
|
||||
// 虽然只是内存操作,但为了防止与 Start/Stop 并发导致读取到脏配置,仍需加锁
|
||||
_lifecycleLock.Wait();
|
||||
try
|
||||
{
|
||||
// 2. 执行深拷贝
|
||||
_config = CloneConfig(newConfig);
|
||||
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置已更新 ({_config.IpAddress}),下次连接生效。");
|
||||
}
|
||||
finally { _lifecycleLock.Release(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置深拷贝辅助方法
|
||||
/// [编译修复] 严格匹配源文件中的属性复制逻辑,确保 Dictionary 等引用类型被重新创建
|
||||
/// </summary>
|
||||
/// <param name="source">源配置对象</param>
|
||||
/// <returns>深拷贝后的配置实例</returns>
|
||||
private VideoSourceConfig CloneConfig(VideoSourceConfig source)
|
||||
{
|
||||
return new VideoSourceConfig
|
||||
{
|
||||
Id = source.Id,
|
||||
Brand = source.Brand,
|
||||
IpAddress = source.IpAddress,
|
||||
Port = source.Port,
|
||||
Username = source.Username,
|
||||
Password = source.Password,
|
||||
ChannelIndex = source.ChannelIndex,
|
||||
StreamType = source.StreamType,
|
||||
Transport = source.Transport,
|
||||
ConnectionTimeoutMs = source.ConnectionTimeoutMs,
|
||||
// 必须深拷贝字典,防止外部修改影响内部
|
||||
VendorArguments = source.VendorArguments != null
|
||||
? new Dictionary<string, string>(source.VendorArguments)
|
||||
: new Dictionary<string, string>()
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 生命周期控制 (Lifecycle Control) ---
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动设备连接
|
||||
/// 包含:状态校验、生命周期锁、非托管初始化、元数据刷新
|
||||
/// </summary>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
// [修复 Bug A] 必须加 ConfigureAwait(false)
|
||||
// 确保后续代码在线程池线程执行,防止 UI 线程死锁
|
||||
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// 1. 强制等待上一个生命周期动作完全结束
|
||||
// 防止快速点击 Start/Stop 导致的逻辑重叠
|
||||
await _pendingLifecycleTask.ConfigureAwait(false);
|
||||
|
||||
// 2. 状态幂等性检查
|
||||
if (_isOnline) return;
|
||||
|
||||
// 3. 更新状态为连接中
|
||||
UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand}...");
|
||||
|
||||
// 4. 执行具体的驱动启动逻辑 (带超时控制)
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
await OnStartAsync(cts.Token).ConfigureAwait(false);
|
||||
|
||||
// 5. 标记运行状态
|
||||
_isOnline = true;
|
||||
IsRunning = true;
|
||||
|
||||
// [Fix Bug D/J] 重置心跳
|
||||
// 给予初始宽限期,防止刚启动就被判定为僵尸流
|
||||
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000);
|
||||
|
||||
// 6. 更新状态为播放中并刷新元数据
|
||||
UpdateStatus(VideoSourceStatus.Playing, "流传输运行中");
|
||||
await RefreshMetadataAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 7. 异常处理:回滚状态
|
||||
_isOnline = false;
|
||||
UpdateStatus(VideoSourceStatus.Disconnected, $"启动失败: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
finally { _lifecycleLock.Release(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步停止设备连接
|
||||
/// 流程:标记离线→执行驱动停止逻辑→更新状态
|
||||
/// </summary>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
// [修复 Bug A] ConfigureAwait(false) 护体
|
||||
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// 1. 标记离线,阻断后续的数据处理
|
||||
_isOnline = false;
|
||||
|
||||
// 2. 执行具体的驱动停止逻辑
|
||||
await OnStopAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 3. 更新状态并释放锁
|
||||
UpdateStatus(VideoSourceStatus.Disconnected, "连接已断开");
|
||||
_lifecycleLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新设备元数据(能力集)
|
||||
/// 对比新旧元数据差异,更新设备支持的功能、通道信息等
|
||||
/// </summary>
|
||||
/// <returns>元数据差异描述符</returns>
|
||||
public async Task<MetadataDiff> RefreshMetadataAsync()
|
||||
{
|
||||
if (!_isOnline) return MetadataDiff.None;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 调用驱动层获取最新元数据
|
||||
var latestMetadata = await OnFetchMetadataAsync().ConfigureAwait(false);
|
||||
|
||||
// 2. 比对差异并更新
|
||||
if (latestMetadata != null && latestMetadata.ChannelCount > 0)
|
||||
{
|
||||
var diff = Metadata.CompareWith(latestMetadata);
|
||||
Metadata = latestMetadata;
|
||||
Metadata.MarkSynced(); // 标记同步时间
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"[MetadataWarning] {Id}: {ex.Message}"); }
|
||||
|
||||
return MetadataDiff.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用动态参数(如码流切换、OSD设置)
|
||||
/// 支持运行时调整画面分辨率、帧率、渲染句柄等
|
||||
/// </summary>
|
||||
/// <param name="options">动态配置项</param>
|
||||
public void ApplyOptions(DynamicStreamOptions options)
|
||||
{
|
||||
if (options == null || !_isOnline) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 校验参数合法性
|
||||
if (Metadata.ValidateOptions(options, out string error))
|
||||
{
|
||||
// 2. 调用驱动层应用参数
|
||||
OnApplyOptions(options);
|
||||
UpdateStatus(_status, "动态参数已应用");
|
||||
}
|
||||
else { Debug.WriteLine($"[OptionRejected] {error}"); }
|
||||
}
|
||||
catch (Exception ex) { Debug.WriteLine($"[ApplyOptionsError] {ex.Message}"); }
|
||||
}
|
||||
|
||||
// 虚方法:供子类重写具体的参数应用逻辑
|
||||
protected virtual void OnApplyOptions(DynamicStreamOptions options) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 帧处理辅助 (Frame Processing Helpers) ---
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有帧订阅者
|
||||
/// 用于优化性能:无订阅时可跳过解码等耗时操作
|
||||
/// </summary>
|
||||
/// <returns>有订阅者返回 true,否则返回 false</returns>
|
||||
protected bool HasFrameSubscribers() => FrameReceived != null;
|
||||
|
||||
/// <summary>
|
||||
/// 上报驱动层异常
|
||||
/// 将底层异常转换为 Reconnecting 状态,触发协调器介入自愈
|
||||
/// </summary>
|
||||
/// <param name="ex">相机统一异常对象</param>
|
||||
protected void ReportError(CameraException ex)
|
||||
{
|
||||
if (!_isOnline) return;
|
||||
|
||||
_isOnline = false;
|
||||
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK报错: {ex.Message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记收到一帧数据(心跳保活 + FPS计算)
|
||||
/// [修改] 增强了 FPS 计算逻辑,每1秒结算一次实时帧率
|
||||
/// </summary>
|
||||
protected void MarkFrameReceived()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
||||
// 1. 更新心跳时间 (原有逻辑)
|
||||
Interlocked.Exchange(ref _lastFrameTick, now);
|
||||
|
||||
// 2. 增加总帧数 (原子操作)
|
||||
Interlocked.Increment(ref _totalFramesReceived);
|
||||
|
||||
// 3. 计算实时帧率 (FPS)
|
||||
// 注意:这里不需要加锁,因为通常回调是单线程串行的
|
||||
// 即便有多线程微小竞争,对于FPS统计来说误差可忽略,优先保证性能
|
||||
_tempFrameCounter++;
|
||||
long timeDiff = now - _lastFpsCalcTick;
|
||||
|
||||
// 每 1000ms (1秒) 结算一次 FPS
|
||||
if (timeDiff >= 1000)
|
||||
{
|
||||
if (_lastFpsCalcTick > 0) // 忽略第一次冷启动的数据
|
||||
{
|
||||
// 计算公式: 帧数 / (时间间隔秒)
|
||||
_currentFps = Math.Round(_tempFrameCounter / (timeDiff / 1000.0), 1);
|
||||
}
|
||||
|
||||
_lastFpsCalcTick = now;
|
||||
_tempFrameCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发帧回调事件
|
||||
/// 向所有订阅者分发帧数据(热路径,尽量减少耗时操作)
|
||||
/// </summary>
|
||||
/// <param name="frameData">帧数据(通常为 OpenCvSharp.Mat 或 SmartFrame)</param>
|
||||
protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 状态分发 (Status Distribution) ---
|
||||
|
||||
/// <summary>
|
||||
/// 后台状态分发循环
|
||||
/// 负责将 Channel 中的状态变更事件调度到 StatusChanged 事件订阅者
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌,用于终止分发循环</param>
|
||||
private async Task StatusDistributorLoopAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
// [修复 Bug π] 关键修复点
|
||||
// 使用 CancellationToken.None 作为 WaitToReadAsync 的参数
|
||||
// 含义:即使 token 被取消,只要 Channel 里还有数据,就继续读取,直到 Channel 被 Complete 且为空
|
||||
while (await _statusQueue.Reader.WaitToReadAsync(CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
while (_statusQueue.Reader.TryRead(out var args))
|
||||
{
|
||||
// [Fix Bug M] 玻璃心防护:捕获用户层回调的异常,防止崩溃
|
||||
try
|
||||
{
|
||||
StatusChanged?.Invoke(this, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[UIEventError] {Id}: {ex.Message}");
|
||||
}
|
||||
|
||||
// 退出条件:仅当明确取消 且 队列已空 时才退出
|
||||
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0) return;
|
||||
}
|
||||
|
||||
// 双重检查退出条件
|
||||
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0) return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Debug.WriteLine($"[DistributorFatal] {Id}: {ex.Message}"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新设备状态并写入通道
|
||||
/// 线程安全,采用 DropOldest 策略防止状态队列溢出
|
||||
/// </summary>
|
||||
/// <param name="status">新状态</param>
|
||||
/// <param name="msg">状态描述信息</param>
|
||||
/// <param name="ex">可选:状态变更关联的异常</param>
|
||||
protected void UpdateStatus(VideoSourceStatus status, string msg, CameraException? ex = null)
|
||||
{
|
||||
lock (_stateSyncRoot)
|
||||
{
|
||||
_status = status;
|
||||
// 尝试写入有界通道,如果满了则丢弃旧数据(DropOldest策略在构造时指定)
|
||||
_statusQueue.Writer.TryWrite(new StatusChangedEventArgs(status, msg, ex, ex?.RawErrorCode));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 抽象方法 (Abstract Methods) ---
|
||||
|
||||
/// <summary>
|
||||
/// 驱动层启动逻辑(必须由具体驱动实现)
|
||||
/// 包含设备连接、登录、取流等底层操作
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
protected abstract Task OnStartAsync(CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 驱动层停止逻辑(必须由具体驱动实现)
|
||||
/// 包含设备登出、连接断开、资源释放等底层操作
|
||||
/// </summary>
|
||||
protected abstract Task OnStopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 驱动层元数据获取逻辑(必须由具体驱动实现)
|
||||
/// 用于获取设备型号、通道能力、固件版本等信息
|
||||
/// </summary>
|
||||
/// <returns>设备元数据实例</returns>
|
||||
protected abstract Task<DeviceMetadata> OnFetchMetadataAsync();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 资源清理 (Disposal) ---
|
||||
|
||||
/// <summary>
|
||||
/// [Fix Bug A: 死锁终结者] 同步销毁入口
|
||||
/// 原理:强制启动一个新的后台 Task 执行 DisposeAsync,并同步阻塞等待其完成
|
||||
/// 效果:彻底规避了在 UI 线程直接 wait 导致的死锁问题
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步销毁资源
|
||||
/// 包含:停止业务、关闭管道、断开事件引用、释放非托管资源
|
||||
/// </summary>
|
||||
public virtual async ValueTask DisposeAsync()
|
||||
{
|
||||
// 1. 停止业务逻辑
|
||||
await StopAsync().ConfigureAwait(false);
|
||||
|
||||
// 2. [Fix Bug π] 优雅关闭状态管道
|
||||
_statusQueue.Writer.TryComplete(); // 标记不再接受新数据
|
||||
_distributorCts?.Cancel(); // 通知消费者准备退出
|
||||
|
||||
if (_distributorTask != null)
|
||||
{
|
||||
// 3. 等待分发器处理完剩余消息
|
||||
// 给予 500ms 的宽限期,防止无限等待
|
||||
await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 4. [Fix Bug ε] 强力切断事件引用
|
||||
// 防止 UI 控件忘记取消订阅导致的内存泄漏
|
||||
FrameReceived = null;
|
||||
StatusChanged = null;
|
||||
|
||||
// 5. 释放基础资源
|
||||
_lifecycleLock.Dispose();
|
||||
_distributorCts?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 内部字段 (Internal Fields) ---
|
||||
|
||||
// 用于跟踪上一个未完成的生命周期任务
|
||||
private Task _pendingLifecycleTask = Task.CompletedTask;
|
||||
|
||||
#endregion
|
||||
}
|
||||
124
SHH.CameraSdk/Drivers/HikVision/HikErrorMapper.cs
Normal file
124
SHH.CameraSdk/Drivers/HikVision/HikErrorMapper.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// [海康] 错误码映射器 (V3.3.1 修复版)
|
||||
/// <para>职责:将海康原始错误码映射为系统统一标准枚举,实现跨厂家故障语义归一化。</para>
|
||||
/// <para>协作:为 HikVideoSource.ReportError 提供标准化故障信息,支撑协调器自愈决策。</para>
|
||||
/// </summary>
|
||||
public static class HikErrorMapper
|
||||
{
|
||||
#region --- 静态映射资源 (Static Mapping Resources) ---
|
||||
|
||||
/// <summary>
|
||||
/// 海康原始错误码 → 系统标准错误码 映射表(只读,初始化后不可修改)
|
||||
/// </summary>
|
||||
private static readonly ReadOnlyDictionary<uint, CameraErrorCode> _codeMap;
|
||||
|
||||
/// <summary>
|
||||
/// 海康原始错误码 → 中文描述 映射表(只读,初始化后不可修改)
|
||||
/// </summary>
|
||||
private static readonly ReadOnlyDictionary<uint, string> _descMap;
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数:初始化错误码映射表(程序启动时仅执行一次)
|
||||
/// </summary>
|
||||
static HikErrorMapper()
|
||||
{
|
||||
// 1. 初始化:海康原始错误码 → 系统标准错误码 映射
|
||||
var codeDict = new Dictionary<uint, CameraErrorCode>
|
||||
{
|
||||
{ 0, CameraErrorCode.Success },
|
||||
|
||||
// --- 基础环境相关错误 ---
|
||||
{ 3, CameraErrorCode.SdkNotInitialized }, // SDK未初始化 (对应 Bug S)
|
||||
{ 41, CameraErrorCode.LocalResourceError }, // 资源分配错误 (对应 Bug R)
|
||||
{ 121, CameraErrorCode.ComponentVersionMismatch }, // 动态库版本不匹配
|
||||
|
||||
// --- 网络通信相关错误 ---
|
||||
{ 7, CameraErrorCode.NetworkUnreachable }, // 连接设备失败(设备离线/网络不通)
|
||||
{ 10, CameraErrorCode.Timeout }, // 发送数据超时
|
||||
{ 11, CameraErrorCode.NetworkRecvError }, // 接收数据超时
|
||||
{ 73, CameraErrorCode.SocketError }, // Socket创建失败
|
||||
|
||||
// --- 身份认证相关错误 ---
|
||||
{ 1, CameraErrorCode.InvalidCredentials }, // 用户名或密码错误
|
||||
{ 2, CameraErrorCode.AccessDenied }, // 权限不足
|
||||
{ 47, CameraErrorCode.UserNotExist }, // 用户不存在
|
||||
{ 153, CameraErrorCode.AccountLocked }, // 用户名被锁定
|
||||
|
||||
// --- 设备资源相关错误 ---
|
||||
{ 4, CameraErrorCode.InvalidChannel }, // 通道号错误
|
||||
{ 5, CameraErrorCode.MaxConnectionsReached }, // 设备连接数超过最大限制 (对应 Bug W 幽灵登录后果)
|
||||
{ 23, CameraErrorCode.NotSupported }, // 设备不支持该功能
|
||||
|
||||
// --- 预览与播放相关错误 ---
|
||||
{ 18, CameraErrorCode.ChannelException }, // 设备通道处于错误状态
|
||||
{ 51, CameraErrorCode.PlayerSdkFailed }, // 调用播放库Player失败
|
||||
{ 105, CameraErrorCode.StreamTypeNotSupport } // 输入码流封装格式不支持
|
||||
};
|
||||
_codeMap = new ReadOnlyDictionary<uint, CameraErrorCode>(codeDict);
|
||||
|
||||
// 2. 初始化:海康原始错误码 → 中文描述 映射
|
||||
var descDict = new Dictionary<uint, string>
|
||||
{
|
||||
{ 0, "没有错误" },
|
||||
{ 1, "用户名或密码错误" },
|
||||
{ 2, "权限不足" },
|
||||
{ 3, "SDK未初始化" },
|
||||
{ 4, "通道号错误" },
|
||||
{ 5, "设备连接数超过最大" },
|
||||
{ 7, "连接设备失败(设备离线或网络不通)" },
|
||||
{ 9, "从设备接收数据失败" },
|
||||
{ 10, "向设备发送数据失败(超时)" },
|
||||
{ 11, "从设备接收数据失败(超时)" },
|
||||
{ 17, "参数错误" },
|
||||
{ 18, "设备通道处于错误状态" },
|
||||
{ 23, "设备不支持该功能" },
|
||||
{ 24, "设备忙" },
|
||||
{ 41, "SDK资源分配错误(内存不足)" },
|
||||
{ 43, "缓冲区已满" },
|
||||
{ 47, "用户不存在" },
|
||||
{ 51, "调用播放库Player失败" },
|
||||
{ 52, "登录设备用户数达到最大" },
|
||||
{ 55, "IP地址不匹配" },
|
||||
{ 56, "MAC地址不匹配" },
|
||||
{ 73, "Socket创建失败" },
|
||||
{ 105, "输入码流封装格式不支持" },
|
||||
{ 121, "动态库版本不匹配" },
|
||||
{ 153, "用户名被锁定" },
|
||||
};
|
||||
_descMap = new ReadOnlyDictionary<uint, string>(descDict);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 核心映射方法 (Core Mapping Methods) ---
|
||||
|
||||
/// <summary>
|
||||
/// 将海康原始错误码转换为系统统一标准错误码
|
||||
/// </summary>
|
||||
/// <param name="hikErrorCode">海康 SDK 返回的原始错误码</param>
|
||||
/// <returns>系统标准错误码(未匹配到时返回 CameraErrorCode.Unknown)</returns>
|
||||
public static CameraErrorCode Map(uint hikErrorCode)
|
||||
{
|
||||
// 尝试从映射表获取,未找到则返回未知错误
|
||||
return _codeMap.TryGetValue(hikErrorCode, out var code) ? code : CameraErrorCode.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取海康原始错误码的中文描述(含原始错误码)
|
||||
/// </summary>
|
||||
/// <param name="hikErrorCode">海康 SDK 返回的原始错误码</param>
|
||||
/// <returns>中文错误描述(格式:描述 (Code:原始错误码))</returns>
|
||||
public static string GetRawDescription(uint hikErrorCode)
|
||||
{
|
||||
if (_descMap.TryGetValue(hikErrorCode, out var desc))
|
||||
{
|
||||
return $"{desc} (Code:{hikErrorCode})";
|
||||
}
|
||||
// 未匹配到的错误码,返回默认描述
|
||||
return $"未知海康错误 (Code:{hikErrorCode})";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
37
SHH.CameraSdk/Drivers/HikVision/HikExtensions.cs
Normal file
37
SHH.CameraSdk/Drivers/HikVision/HikExtensions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 海康 SDK 扩展方法类
|
||||
/// 功能:提供海康 API 调用结果校验的快捷扩展,简化错误处理逻辑
|
||||
/// </summary>
|
||||
public static class HikExtensions
|
||||
{
|
||||
#region --- 结果校验扩展 (Result Validation Extensions) ---
|
||||
|
||||
/// <summary>
|
||||
/// 校验海康 API 调用结果是否成功
|
||||
/// 功能:若结果为 false(调用失败),自动捕获海康错误码并抛出统一异常
|
||||
/// </summary>
|
||||
/// <param name="result">海康 API 调用返回的布尔结果(true=成功,false=失败)</param>
|
||||
/// <param name="actionName">操作名称(用于异常信息描述,如“设备登录”“启动预览”)</param>
|
||||
/// <param name="brand">设备品牌(默认海康威视,无需手动指定)</param>
|
||||
/// <exception cref="CameraException">API 调用失败时抛出,包含标准错误码与原始错误描述</exception>
|
||||
public static void EnsureSuccess(this bool result, string actionName, DeviceBrand brand = DeviceBrand.HikVision)
|
||||
{
|
||||
// 调用成功则直接返回,无需后续处理
|
||||
if (result) return;
|
||||
|
||||
// 1. 获取海康 SDK 最后一次操作的原始错误码
|
||||
uint lastError = HikNativeMethods.NET_DVR_GetLastError();
|
||||
|
||||
// 2. 将海康原始错误码映射为系统统一标准错误码
|
||||
CameraErrorCode standardCode = HikErrorMapper.Map(lastError);
|
||||
|
||||
// 3. 抛出统一异常,携带操作名称、标准错误码、原始错误描述等上下文
|
||||
throw new CameraException(standardCode, $"{actionName} 失败", brand, (int)lastError)
|
||||
.WithContext("Action", actionName) // 附加操作名称上下文
|
||||
.WithContext("HikDesc", HikErrorMapper.GetRawDescription(lastError)); // 附加海康原始错误描述
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
495
SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs
Normal file
495
SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs
Normal file
@@ -0,0 +1,495 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 海康 HCNetSDK.dll 原生方法封装(静态部分类)
|
||||
/// 功能:包含设备登录、预览、PTZ控制、异常回调等核心 SDK 接口定义
|
||||
/// 注意:所有 API 均直接映射海康原生 DLL 函数,参数顺序与类型需严格匹配官方文档
|
||||
/// </summary>
|
||||
public static partial class HikNativeMethods
|
||||
{
|
||||
#region --- 基础配置 (Basic Configuration) ---
|
||||
|
||||
/// <summary>
|
||||
/// HCNetSDK.dll 动态库路径
|
||||
/// 说明:确保项目中该路径与实际文件位置一致,否则会导致 DllImport 调用失败
|
||||
/// </summary>
|
||||
private const string DllName = "Drivers\\Hikvision\\HCNetSDK.dll";
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 结构体定义 (Structures) ---
|
||||
|
||||
/// <summary>
|
||||
/// 设备信息结构体 (NET_DEVICEINFO_V30)
|
||||
/// 功能:存储设备序列号、通道数、协议类型、能力集等核心信息
|
||||
/// 注:登录设备成功后通过 NET_DVR_Login_V30 接口返回
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct NET_DEVICEINFO_V30
|
||||
{
|
||||
/// <summary> 设备序列号(长度48字节) </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)]
|
||||
public byte[] sSerialNumber;
|
||||
|
||||
/// <summary> 报警输入个数 </summary>
|
||||
public byte byAlarmInPortNum;
|
||||
|
||||
/// <summary> 报警输出个数 </summary>
|
||||
public byte byAlarmOutPortNum;
|
||||
|
||||
/// <summary> 硬盘个数 </summary>
|
||||
public byte byDiskNum;
|
||||
|
||||
/// <summary> 设备类型:1-DVR,2-ATM DVR,3-DVS 等 </summary>
|
||||
public byte byDVRType;
|
||||
|
||||
/// <summary> 模拟通道个数 </summary>
|
||||
public byte byChanNum;
|
||||
|
||||
/// <summary> 起始通道号(目前从1开始) </summary>
|
||||
public byte byStartChan;
|
||||
|
||||
/// <summary> 语音通道数 </summary>
|
||||
public byte byAudioChanNum;
|
||||
|
||||
/// <summary> 最大数字通道个数(低8位) </summary>
|
||||
public byte byIPChanNum;
|
||||
|
||||
/// <summary> 零通道编码个数 </summary>
|
||||
public byte byZeroChanNum;
|
||||
|
||||
/// <summary> 主码流传输协议类型:0-私有协议,1-RTSP,2-同时支持两者 </summary>
|
||||
public byte byMainProto;
|
||||
|
||||
/// <summary> 子码流传输协议类型:0-私有协议,1-RTSP,2-同时支持两者 </summary>
|
||||
public byte bySubProto;
|
||||
|
||||
/// <summary> 基础能力集(位掩码),位与结果为1表示支持对应功能 </summary>
|
||||
/// <remarks>
|
||||
/// bySupport & 0x1: 支持智能搜索<br/>
|
||||
/// bySupport & 0x2: 支持备份<br/>
|
||||
/// bySupport & 0x4: 支持压缩参数能力获取<br/>
|
||||
/// bySupport & 0x8: 支持多网卡<br/>
|
||||
/// bySupport & 0x10: 支持远程SADP<br/>
|
||||
/// bySupport & 0x20: 支持Raid卡功能<br/>
|
||||
/// bySupport & 0x40: 支持IPSAN目录查找<br/>
|
||||
/// bySupport & 0x80: 支持RTP over RTSP
|
||||
/// </remarks>
|
||||
public byte bySupport;
|
||||
|
||||
/// <summary> 能力集扩充(位掩码),位与结果为1表示支持对应功能 </summary>
|
||||
/// <remarks>
|
||||
/// bySupport1 & 0x1: 支持SNMP v30<br/>
|
||||
/// bySupport1 & 0x2: 支持区分回放和下载<br/>
|
||||
/// bySupport1 & 0x4: 支持布防优先级<br/>
|
||||
/// bySupport1 & 0x8: 智能设备支持布防时间段扩展<br/>
|
||||
/// bySupport1 & 0x10: 支持多磁盘数(超过33个)<br/>
|
||||
/// bySupport1 & 0x20: 支持RTSP over HTTP<br/>
|
||||
/// bySupport1 & 0x80: 支持车牌新报警信息(2012-9-28),且支持NET_DVR_IPPARACFG_V40结构体
|
||||
/// </remarks>
|
||||
public byte bySupport1;
|
||||
|
||||
/// <summary> 能力集扩充(位掩码),位与结果为1表示支持对应功能 </summary>
|
||||
/// <remarks>
|
||||
/// bySupport2 & 0x1: 解码器支持通过URL取流解码<br/>
|
||||
/// bySupport2 & 0x2: 支持FTP V40<br/>
|
||||
/// bySupport2 & 0x4: 支持ANR<br/>
|
||||
/// bySupport2 & 0x8: 支持CCD的通道参数配置<br/>
|
||||
/// bySupport2 & 0x10: 支持布防报警回传信息(仅抓拍机报警,新老报警结构)<br/>
|
||||
/// bySupport2 & 0x20: 支持单独获取设备状态子项<br/>
|
||||
/// bySupport2 & 0x40: 是码流加密设备
|
||||
/// </remarks>
|
||||
public byte bySupport2;
|
||||
|
||||
/// <summary> 设备型号 </summary>
|
||||
public ushort wDevType;
|
||||
|
||||
/// <summary> 能力集扩充(位掩码),位与结果为1表示支持对应功能 </summary>
|
||||
/// <remarks>
|
||||
/// bySupport3 & 0x1: 支持多码流<br/>
|
||||
/// bySupport3 & 0x4: 支持按组配置(通道图像参数、报警输入参数等)<br/>
|
||||
/// bySupport3 & 0x8: 支持TCP/UDP/多播预览的延时预览字段<br/>
|
||||
/// bySupport3 & 0x10: 支持获取报警主机主要状态(V40)<br/>
|
||||
/// bySupport3 & 0x20: 支持通过DDNS域名解析取流
|
||||
/// </remarks>
|
||||
public byte bySupport3;
|
||||
|
||||
/// <summary> 多码流支持标识(按位表示) </summary>
|
||||
/// <remarks>0-不支持,1-支持;bit1-码流3,bit2-码流4,bit7-主码流,bit8-子码流</remarks>
|
||||
public byte byMultiStreamProto;
|
||||
|
||||
/// <summary> 起始数字通道号(0表示无效) </summary>
|
||||
public byte byStartDChan;
|
||||
|
||||
/// <summary> 起始数字对讲通道号(0表示无效) </summary>
|
||||
public byte byStartDTalkChan;
|
||||
|
||||
/// <summary> 数字通道个数(高8位) </summary>
|
||||
public byte byHighDChanNum;
|
||||
|
||||
/// <summary> 能力集扩充(位掩码),位与结果为1表示支持对应功能 </summary>
|
||||
public byte bySupport4;
|
||||
|
||||
/// <summary> 支持语种能力(按位表示) </summary>
|
||||
/// <remarks>
|
||||
/// byLanguageType = 0: 老设备<br/>
|
||||
/// byLanguageType & 0x1: 支持中文<br/>
|
||||
/// byLanguageType & 0x2: 支持英文
|
||||
/// </remarks>
|
||||
public byte byLanguageType;
|
||||
|
||||
/// <summary> 音频输入通道数 </summary>
|
||||
public byte byVoiceInChanNum;
|
||||
|
||||
/// <summary> 音频输入起始通道号 </summary>
|
||||
public byte byStartVoiceInChanNo;
|
||||
|
||||
/// <summary> 保留字段(必须置0) </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
|
||||
public byte[] byRes3;
|
||||
|
||||
/// <summary> AES算法加密/解密能力 </summary>
|
||||
public byte byMirrorCap;
|
||||
|
||||
/// <summary> 起始数字通道号(扩展) </summary>
|
||||
public ushort wStartIPChanNo;
|
||||
|
||||
/// <summary> 保留字段(必须置0) </summary>
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
|
||||
public byte[] byRes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览参数结构体 (NET_DVR_PREVIEWINFO)
|
||||
/// 功能:配置实时预览的通道、码流类型、连接方式等参数
|
||||
/// 注:用于 NET_DVR_RealPlay_V40 接口的输入参数
|
||||
/// </summary>
|
||||
[StructLayoutAttribute(LayoutKind.Sequential)]
|
||||
public struct NET_DVR_PREVIEWINFO
|
||||
{
|
||||
/// <summary> 通道号(模拟通道从1开始) </summary>
|
||||
public Int32 lChannel;
|
||||
|
||||
/// <summary> 码流类型:0-主码流,1-子码流,2-码流3,3-码流4,以此类推 </summary>
|
||||
public uint dwStreamType;
|
||||
|
||||
/// <summary> 连接方式:0-TCP,1-UDP,2-多播,3-RTP,4-RTP/RTSP,5-RTSP/HTTP </summary>
|
||||
public uint dwLinkMode;
|
||||
|
||||
/// <summary> 播放窗口句柄 </summary>
|
||||
/// <remarks>IntPtr.Zero 表示不让 SDK 直接渲染,仅获取原始流数据</remarks>
|
||||
public IntPtr hPlayWnd;
|
||||
|
||||
/// <summary> 取流模式:0-非阻塞,1-阻塞(阻塞模式超时5秒返回) </summary>
|
||||
/// <remarks>阻塞模式不适合轮询取流操作</remarks>
|
||||
public bool bBlocked;
|
||||
|
||||
/// <summary> 是否启用回放录像:0-不启用,1-启用 </summary>
|
||||
public bool bPassbackRecord;
|
||||
|
||||
/// <summary> 预览模式:0-正常预览,1-延迟预览 </summary>
|
||||
public byte byPreviewMode;
|
||||
|
||||
/// <summary> 流ID(lChannel为0xffffffff时启用,长度32字节) </summary>
|
||||
[MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = STREAM_ID_LEN, ArraySubType = UnmanagedType.I1)]
|
||||
public byte[] byStreamID;
|
||||
|
||||
/// <summary> 应用层协议类型:0-私有协议,1-RTSP协议 </summary>
|
||||
public byte byProtoType;
|
||||
|
||||
/// <summary> 保留字段(必须置0) </summary>
|
||||
public byte byRes1;
|
||||
|
||||
/// <summary> 码流编解码类型:0-通用编码数据,1-热成像原始数据(含温度加密信息) </summary>
|
||||
public byte byVideoCodingType;
|
||||
|
||||
/// <summary> 播放库最大缓冲帧数(范围1-50,0表示默认1帧) </summary>
|
||||
public uint dwDisplayBufNum;
|
||||
|
||||
/// <summary> NPQ模式:0-直连,1-过流媒体 </summary>
|
||||
public byte byNPQMode;
|
||||
|
||||
/// <summary> 保留字段(必须置0) </summary>
|
||||
[MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 215, ArraySubType = UnmanagedType.I1)]
|
||||
public byte[] byRes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 时间结构体 (NET_DVR_TIME)
|
||||
/// [Fix Bug P: 结构体炸弹] 修复结构体对齐问题,避免栈内存覆盖导致随机崩溃
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 原问题:ushort/byte 混合定义导致结构体总大小不足16字节,SDK写入时覆盖栈变量<br/>
|
||||
/// 修复方案:使用 Pack=4 对齐,成员类型统一为 uint(4字节),与 C++ DWORD 匹配
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct NET_DVR_TIME
|
||||
{
|
||||
public uint dwYear; // 年份
|
||||
public uint dwMonth; // 月份(1-12)
|
||||
public uint dwDay; // 日期(1-31)
|
||||
public uint dwHour; // 小时(0-23)
|
||||
public uint dwMinute; // 分钟(0-59)
|
||||
public uint dwSecond; // 秒(0-59)
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 常量定义 (Constants) ---
|
||||
|
||||
/// <summary> 流ID长度(32字节) </summary>
|
||||
public const int STREAM_ID_LEN = 32;
|
||||
|
||||
/// <summary> 数据类型常量:系统头数据 </summary>
|
||||
public const int NET_DVR_SYSHEAD = 1;
|
||||
|
||||
/// <summary> 数据类型常量:视频流数据(H.264/H.265) </summary>
|
||||
public const int NET_DVR_STREAMDATA = 2;
|
||||
|
||||
/// <summary> 数据类型常量:音频数据 </summary>
|
||||
public const int NET_DVR_AUDIOSTREAMDATA = 3;
|
||||
|
||||
/// <summary> 命令常量:获取时间配置 </summary>
|
||||
public const uint NET_DVR_GET_TIMECFG = 118;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- PTZ 控制相关 (PTZ Control) ---
|
||||
|
||||
/// <summary> PTZ命令常量:镜头控制 </summary>
|
||||
public const uint ZOOM_IN = 11; // 焦距变大(拉近)
|
||||
public const uint ZOOM_OUT = 12; // 焦距变小(拉远)
|
||||
public const uint FOCUS_NEAR = 13; // 焦点前调
|
||||
public const uint FOCUS_FAR = 14; // 焦点后调
|
||||
public const uint IRIS_OPEN = 15; // 光圈扩大
|
||||
public const uint IRIS_CLOSE = 16; // 光圈缩小
|
||||
|
||||
/// <summary> PTZ命令常量:方向控制 </summary>
|
||||
public const uint TILT_UP = 21; // 云台上仰
|
||||
public const uint TILT_DOWN = 22; // 云台下俯
|
||||
public const uint PAN_LEFT = 23; // 云台左转
|
||||
public const uint PAN_RIGHT = 24; // 云台右转
|
||||
public const uint UP_LEFT = 25; // 上左移动
|
||||
public const uint UP_RIGHT = 26; // 上右移动
|
||||
public const uint DOWN_LEFT = 27; // 下左移动
|
||||
public const uint DOWN_RIGHT = 28; // 下右移动
|
||||
public const uint PAN_AUTO = 29; // 自动扫描
|
||||
|
||||
/// <summary> PTZ命令常量:辅助功能 </summary>
|
||||
public const uint LIGHT_PWRON = 2; // 接通灯光电源
|
||||
public const uint WIPER_PWRON = 3; // 接通雨刷开关
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 异常回调相关 (Exception Callback) ---
|
||||
|
||||
/// <summary> 异常类型常量 </summary>
|
||||
public const int EXCEPTION_EXCHANGE = 0x8000; // 用户交互时异常
|
||||
public const int EXCEPTION_AUDIOEXCHANGE = 0x8001; // 语音对讲异常
|
||||
public const int EXCEPTION_ALARM = 0x8002; // 报警异常
|
||||
public const int EXCEPTION_PREVIEW = 0x8003; // 网络预览异常
|
||||
public const int EXCEPTION_SERIAL = 0x8004; // 透明通道异常
|
||||
public const int EXCEPTION_RECONNECT = 0x8005; // 预览时重连成功
|
||||
|
||||
/// <summary>
|
||||
/// 异常消息回调委托
|
||||
/// 功能:SDK 发生异常时触发,返回异常类型、用户ID、相关句柄等信息
|
||||
/// </summary>
|
||||
/// <param name="dwType">异常类型(对应 EXCEPTION_XXX 常量)</param>
|
||||
/// <param name="lUserID">用户ID(NET_DVR_Login_V30 返回值)</param>
|
||||
/// <param name="lHandle">异常关联句柄(预览句柄/报警句柄等)</param>
|
||||
/// <param name="pUser">用户自定义数据指针</param>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate void EXCEPTION_CALLBACK(uint dwType, int lUserID, int lHandle, IntPtr pUser);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 预览回调相关 (Preview Callback) ---
|
||||
|
||||
/// <summary>
|
||||
/// 预览数据回调委托
|
||||
/// 功能:实时预览时触发,返回原始流数据(系统头/视频流/音频流)
|
||||
/// </summary>
|
||||
/// <param name="lRealHandle">预览句柄(NET_DVR_RealPlay_V40 返回值)</param>
|
||||
/// <param name="dwDataType">数据类型(对应 NET_DVR_XXX 数据类型常量)</param>
|
||||
/// <param name="pBuffer">数据缓冲区指针</param>
|
||||
/// <param name="dwBufSize">缓冲区大小(字节)</param>
|
||||
/// <param name="pUser">用户自定义数据指针</param>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate void REALDATACALLBACK(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- SDK 基础接口 (Basic SDK Interfaces) ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 SDK
|
||||
/// 功能:调用所有其他 SDK 接口的前提,必须先初始化再使用
|
||||
/// </summary>
|
||||
/// <returns>初始化成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_Init();
|
||||
|
||||
/// <summary>
|
||||
/// 释放 SDK 资源
|
||||
/// 功能:程序退出前调用,释放 SDK 占用的非托管资源(网络连接、内存等)
|
||||
/// </summary>
|
||||
/// <returns>释放成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_Cleanup();
|
||||
|
||||
/// <summary>
|
||||
/// 获取最后一次操作的错误码
|
||||
/// 功能:API 调用失败后,通过此接口获取具体错误原因
|
||||
/// </summary>
|
||||
/// <returns>错误码(需结合海康官方文档查询含义)</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern uint NET_DVR_GetLastError();
|
||||
|
||||
/// <summary>
|
||||
/// 设置网络连接超时时间和连接尝试次数
|
||||
/// </summary>
|
||||
/// <param name="dwWaitTime">超时时间(毫秒),推荐 3000ms</param>
|
||||
/// <param name="dwTryTimes">连接尝试次数,推荐 1 次</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes);
|
||||
|
||||
/// <summary>
|
||||
/// 设置自动重连功能
|
||||
/// </summary>
|
||||
/// <param name="dwInterval">重连间隔(毫秒),推荐 10000ms</param>
|
||||
/// <param name="bEnableRecon">是否启用重连:0-禁用,1-启用</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_SetReconnect(uint dwInterval, bool bEnableRecon);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 设备登录/登出接口 (Device Login/Logout) ---
|
||||
|
||||
/// <summary>
|
||||
/// 用户注册设备(登录)
|
||||
/// 功能:建立与设备的连接,获取用户ID(后续接口调用的核心标识)
|
||||
/// </summary>
|
||||
/// <param name="sDVRIP">设备IP地址</param>
|
||||
/// <param name="wDVRPort">设备端口号(海康默认8000)</param>
|
||||
/// <param name="sUserName">登录用户名(默认 admin)</param>
|
||||
/// <param name="sPassword">登录密码</param>
|
||||
/// <param name="lpDeviceInfo">输出参数:设备信息结构体</param>
|
||||
/// <returns>登录成功返回用户ID(非负整数),失败返回 -1</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern int NET_DVR_Login_V30(string sDVRIP, Int32 wDVRPort, string sUserName, string sPassword, ref NET_DEVICEINFO_V30 lpDeviceInfo);
|
||||
|
||||
/// <summary>
|
||||
/// 用户注销(登出)
|
||||
/// 功能:断开与设备的连接,释放用户ID关联的资源
|
||||
/// </summary>
|
||||
/// <param name="lUserID">用户ID(NET_DVR_Login_V30 返回值)</param>
|
||||
/// <returns>登出成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_Logout(int lUserID);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 预览控制接口 (Preview Control) ---
|
||||
|
||||
/// <summary>
|
||||
/// 实时预览(V40版本,支持回调)
|
||||
/// 功能:启动设备实时取流,通过回调获取原始流数据
|
||||
/// </summary>
|
||||
/// <param name="lUserID">用户ID(NET_DVR_Login_V30 返回值)</param>
|
||||
/// <param name="lpPreviewInfo">预览参数结构体</param>
|
||||
/// <param name="fRealDataCallBack_V30">流数据回调函数</param>
|
||||
/// <param name="pUser">用户自定义数据指针</param>
|
||||
/// <returns>预览成功返回预览句柄(非负整数),失败返回 -1</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern int NET_DVR_RealPlay_V40(int lUserID, ref NET_DVR_PREVIEWINFO lpPreviewInfo, REALDATACALLBACK fRealDataCallBack_V30, IntPtr pUser);
|
||||
|
||||
/// <summary>
|
||||
/// 停止预览
|
||||
/// 功能:停止实时取流,释放预览句柄关联的资源
|
||||
/// </summary>
|
||||
/// <param name="lRealHandle">预览句柄(NET_DVR_RealPlay_V40 返回值)</param>
|
||||
/// <returns>停止成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_StopRealPlay(int lRealHandle);
|
||||
|
||||
/// <summary>
|
||||
/// 强制生成I帧
|
||||
/// 功能:主动触发设备发送I帧,优化视频流解码延时
|
||||
/// </summary>
|
||||
/// <param name="lUserID">用户ID</param>
|
||||
/// <param name="lChannel">通道号</param>
|
||||
/// <returns>操作成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_MakeKeyFrame(int lUserID, int lChannel);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- PTZ 控制接口 (PTZ Control Interfaces) ---
|
||||
|
||||
/// <summary>
|
||||
/// 云台控制(带速度)
|
||||
/// 功能:控制云台旋转、镜头缩放、光圈调节等操作
|
||||
/// </summary>
|
||||
/// <param name="lUserID">用户ID</param>
|
||||
/// <param name="lChannel">通道号</param>
|
||||
/// <param name="dwPTZCommand">PTZ控制命令(对应 PTZ 命令常量)</param>
|
||||
/// <param name="dwStop">启停标识:0-开始,1-停止</param>
|
||||
/// <param name="dwSpeed">控制速度(1-7,数值越大速度越快)</param>
|
||||
/// <returns>操作成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_PTZControlWithSpeed_Other(int lUserID, int lChannel, uint dwPTZCommand, uint dwStop, uint dwSpeed);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 异常回调接口 (Exception Callback Interfaces) ---
|
||||
|
||||
/// <summary>
|
||||
/// 设置连接超时时间和重连策略(兼容旧版本)
|
||||
/// </summary>
|
||||
/// <param name="dwInterval">重连间隔(毫秒),建议 3000</param>
|
||||
/// <param name="bEnableRecon">是否启用重连:1-启用,0-禁用</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_SetReconnect(uint dwInterval, int bEnableRecon);
|
||||
|
||||
/// <summary>
|
||||
/// 注册异常、重连等消息的回调函数
|
||||
/// 功能:绑定异常回调委托,接收 SDK 层面的异常通知
|
||||
/// </summary>
|
||||
/// <param name="nMessage">消息类型(0 表示所有消息)</param>
|
||||
/// <param name="hWnd">窗口句柄(可为 IntPtr.Zero)</param>
|
||||
/// <param name="fExceptionCallBack">异常回调函数委托</param>
|
||||
/// <param name="pUser">用户自定义数据指针</param>
|
||||
/// <returns>注册成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool NET_DVR_SetExceptionCallBack_V30(uint nMessage, IntPtr hWnd, EXCEPTION_CALLBACK fExceptionCallBack, IntPtr pUser);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 通用配置接口 (General Configuration Interfaces) ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取设备配置
|
||||
/// 功能:通用接口,根据命令号获取设备特定配置(如时间配置、通道参数等)
|
||||
/// </summary>
|
||||
/// <param name="lUserID">用户ID</param>
|
||||
/// <param name="dwCommand">配置命令号(如 NET_DVR_GET_TIMECFG)</param>
|
||||
/// <param name="lChannel">通道号(-1 表示设备级配置)</param>
|
||||
/// <param name="lpOutBuffer">输出缓冲区指针(存储配置数据)</param>
|
||||
/// <param name="dwOutBufferSize">输出缓冲区大小(字节)</param>
|
||||
/// <param name="lpBytesReturned">输出参数:实际返回的数据大小(字节)</param>
|
||||
/// <returns>获取成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
|
||||
public static extern bool NET_DVR_GetDVRConfig(
|
||||
int lUserID,
|
||||
uint dwCommand,
|
||||
int lChannel,
|
||||
IntPtr lpOutBuffer,
|
||||
uint dwOutBufferSize,
|
||||
ref uint lpBytesReturned);
|
||||
|
||||
#endregion
|
||||
}
|
||||
347
SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs
Normal file
347
SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 海康播放库 PlayCtrl.dll 的封装
|
||||
/// 完全参考官方 WinPlayCtrl.cs 定义,提供解码、播放、端口管理等核心能力
|
||||
/// </summary>
|
||||
public static class HikPlayMethods
|
||||
{
|
||||
#region --- 基础配置 (Basic Configuration) ---
|
||||
|
||||
/// <summary>
|
||||
/// PlayCtrl.dll 动态库路径
|
||||
/// 注意:请确保项目中该路径与实际文件位置一致,否则会导致 DllImport 失败
|
||||
/// </summary>
|
||||
private const string DllName = @"Drivers\\Hikvision\\PlayCtrl.dll";
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 常量定义 (Constants) ---
|
||||
|
||||
/// <summary> 最大支持的通道数 </summary>
|
||||
public const int PLAYM4_MAX_SUPPORTS = 500;
|
||||
|
||||
// 流模式常量
|
||||
public const int STREAME_REALTIME = 0; // 实时流模式
|
||||
public const int STREAME_FILE = 1; // 文件流模式
|
||||
|
||||
// 帧类型常量(音频/视频)
|
||||
public const int T_AUDIO16 = 101; // 16位音频帧
|
||||
public const int T_AUDIO8 = 100; // 8位音频帧
|
||||
public const int T_UYVY = 1; // UYVY格式视频帧
|
||||
public const int T_YV12 = 3; // YV12格式视频帧(常用)
|
||||
public const int T_RGB32 = 7; // RGB32格式视频帧
|
||||
|
||||
// 显示缓冲区大小常量
|
||||
public const int MAX_DIS_FRAMES = 50; // 最大显示缓冲帧数
|
||||
public const int MIN_DIS_FRAMES = 1; // 最小显示缓冲帧数
|
||||
|
||||
// 源缓冲区大小常量(单位:字节)
|
||||
public const int SOURCE_BUF_MAX = 1024 * 100000; // 最大源缓冲区(100MB)
|
||||
public const int SOURCE_BUF_MIN = 1024 * 50; // 最小源缓冲区(50KB)
|
||||
|
||||
// 错误码常量(PlayCtrl.dll 返回错误标识)
|
||||
public const int PLAYM4_NOERROR = 0; // 无错误
|
||||
public const int PLAYM4_PARA_OVER = 1; // 参数超出范围
|
||||
public const int PLAYM4_ORDER_ERROR = 2; // 函数调用顺序错误
|
||||
public const int PLAYM4_TIMER_ERROR = 3; // 定时器初始化错误
|
||||
public const int PLAYM4_DEC_VIDEO_ERROR = 4; // 视频解码错误
|
||||
public const int PLAYM4_DEC_AUDIO_ERROR = 5; // 音频解码错误
|
||||
public const int PLAYM4_ALLOC_MEMORY_ERROR = 6; // 内存分配错误
|
||||
public const int PLAYM4_OPEN_FILE_ERROR = 7; // 打开文件错误
|
||||
public const int PLAYM4_CREATE_OBJ_ERROR = 8; // 创建对象错误
|
||||
public const int PLAYM4_CREATE_DDRAW_ERROR = 9; // 创建DirectDraw错误
|
||||
public const int PLAYM4_CREATE_OFFSCREEN_ERROR = 10;// 创建离屏表面错误
|
||||
public const int PLAYM4_BUF_OVER = 11; // 缓冲区溢出
|
||||
public const int PLAYM4_CREATE_SOUND_ERROR = 12; // 创建音频设备错误
|
||||
public const int PLAYM4_SET_VOLUME_ERROR = 13; // 设置音量错误
|
||||
public const int PLAYM4_SUPPORT_FILE_ONLY = 14; // 仅支持文件流
|
||||
public const int PLAYM4_SUPPORT_STREAM_ONLY = 15; // 仅支持实时流
|
||||
public const int PLAYM4_SYS_NOT_SUPPORT = 16; // 系统不支持该功能
|
||||
public const int PLAYM4_FILEHEADER_UNKNOWN = 17; // 文件头格式未知
|
||||
public const int PLAYM4_VERSION_INCORRECT = 18; // 版本不匹配
|
||||
public const int PLAYM4_INIT_DECODER_ERROR = 19; // 解码器初始化错误
|
||||
public const int PLAYM4_CHECK_FILE_ERROR = 20; // 校验文件错误
|
||||
public const int PLAYM4_INIT_TIMER_ERROR = 21; // 初始化定时器错误
|
||||
public const int PLAYM4_BLT_ERROR = 22; // 图像绘制错误
|
||||
public const int PLAYM4_UPDATE_ERROR = 23; // 更新显示错误
|
||||
|
||||
// PTZ控制命令常量(保留自定义逻辑,官方示例未包含)
|
||||
public const uint ZOOM_IN = 11; // 焦距变大(拉近)
|
||||
public const uint ZOOM_OUT = 12; // 焦距变小(拉远)
|
||||
public const uint FOCUS_NEAR = 13; // 焦点前调
|
||||
public const uint FOCUS_FAR = 14; // 焦点后调
|
||||
public const uint IRIS_OPEN = 15; // 光圈扩大
|
||||
public const uint IRIS_CLOSE = 16; // 光圈缩小
|
||||
public const uint TILT_UP = 21; // 云台上仰
|
||||
public const uint TILT_DOWN = 22; // 云台下俯
|
||||
public const uint PAN_LEFT = 23; // 云台左转
|
||||
public const uint PAN_RIGHT = 24; // 云台右转
|
||||
public const uint UP_LEFT = 25; // 上左移动
|
||||
public const uint UP_RIGHT = 26; // 上右移动
|
||||
public const uint DOWN_LEFT = 27; // 下左移动
|
||||
public const uint DOWN_RIGHT = 28; // 下右移动
|
||||
public const uint PAN_AUTO = 29; // 云台自动扫描
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 结构体定义 (Structs) ---
|
||||
|
||||
/// <summary>
|
||||
/// 帧信息结构体:存储解码后帧的宽高、帧率、序号等关键信息
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FRAME_INFO
|
||||
{
|
||||
public int nWidth; // 帧宽度(像素)
|
||||
public int nHeight; // 帧高度(像素)
|
||||
public int nStamp; // 时间戳
|
||||
public int nType; // 帧类型(对应 T_XXX 常量)
|
||||
public int nFrameRate; // 帧率(fps)
|
||||
public uint dwFrameNum; // 帧序号
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 帧位置结构体:存储文件流中帧的位置、时间等信息
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FRAME_POS
|
||||
{
|
||||
public int nFilePos; // 文件中的位置偏移
|
||||
public int nFrameNum; // 帧序号
|
||||
public int nFrameTime; // 帧时间(毫秒)
|
||||
public int nErrorFrameNum; // 错误帧数
|
||||
public IntPtr pErrorTime; // 错误时间数组指针
|
||||
public int nErrorLostFrameNum; // 丢失的错误帧数
|
||||
public int nErrorFrameSize; // 错误帧大小
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 帧类型结构体:存储帧数据缓冲区、大小等信息
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct FRAME_TYPE
|
||||
{
|
||||
[MarshalAs(UnmanagedType.LPStr)]
|
||||
public string pDataBuf; // 帧数据缓冲区指针
|
||||
public int nSize; // 缓冲区大小(字节)
|
||||
public int nFrameNum; // 帧序号
|
||||
public bool bIsAudio; // 是否为音频帧
|
||||
public int nReserved; // 保留字段(置0)
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 委托定义 (Delegates) ---
|
||||
|
||||
/// <summary>
|
||||
/// 解码回调委托 (对应官方 DECCBFUN)
|
||||
/// 功能:解码完成后触发,返回解码后的帧数据
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="pBuf">解码后数据缓冲区指针</param>
|
||||
/// <param name="nSize">缓冲区大小(字节)</param>
|
||||
/// <param name="pFrameInfo">帧信息结构体(引用传递)</param>
|
||||
/// <param name="nReserved1">保留字段1(置0)</param>
|
||||
/// <param name="nReserved2">保留字段2(置0)</param>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate void DECCBFUN(
|
||||
int nPort,
|
||||
IntPtr pBuf,
|
||||
int nSize,
|
||||
ref FRAME_INFO pFrameInfo,
|
||||
int nReserved1,
|
||||
int nReserved2
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 显示回调委托 (对应官方 DISPLAYCBFUN)
|
||||
/// 功能:帧数据准备显示时触发,用于自定义渲染逻辑
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="pBuf">显示数据缓冲区指针</param>
|
||||
/// <param name="nSize">缓冲区大小(字节)</param>
|
||||
/// <param name="nWidth">显示宽度(像素)</param>
|
||||
/// <param name="nHeight">显示高度(像素)</param>
|
||||
/// <param name="nStamp">时间戳</param>
|
||||
/// <param name="nType">数据类型(对应 T_XXX 常量)</param>
|
||||
/// <param name="nReserved">保留字段(置0)</param>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate void DISPLAYCBFUN(
|
||||
int nPort,
|
||||
IntPtr pBuf,
|
||||
int nSize,
|
||||
int nWidth,
|
||||
int nHeight,
|
||||
int nStamp,
|
||||
int nType,
|
||||
int nReserved
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 文件结束回调委托
|
||||
/// 功能:文件流播放完成时触发
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="pUser">用户自定义数据指针</param>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate void FILEENDCALLBACK(int nPort, IntPtr pUser);
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- API 导入 (Dll Imports) ---
|
||||
|
||||
/// <summary>
|
||||
/// 获取闲置的播放端口。
|
||||
/// <para>[警告] 此函数非线程安全,且端口资源有限(最多500个)。</para>
|
||||
/// <para>高并发场景下必须加全局锁,防止端口分配冲突。</para>
|
||||
/// </summary>
|
||||
/// <param name="nPort">输出参数:获取到的闲置端口号(输出-1表示失败)</param>
|
||||
/// <returns>获取成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_GetPort(ref int nPort);
|
||||
|
||||
/// <summary>
|
||||
/// 释放播放端口
|
||||
/// 功能:不再使用端口时调用,避免端口资源泄漏
|
||||
/// </summary>
|
||||
/// <param name="nPort">要释放的端口号</param>
|
||||
/// <returns>释放成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_FreePort(int nPort);
|
||||
|
||||
/// <summary>
|
||||
/// 打开流
|
||||
/// 功能:初始化端口的流缓冲区,准备接收流数据
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="pFileHeadBuf">文件头数据缓冲区指针(实时流可为空)</param>
|
||||
/// <param name="nSize">缓冲区大小(字节)</param>
|
||||
/// <param name="nBufPoolSize">流缓冲区池大小(字节)</param>
|
||||
/// <returns>打开成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_OpenStream(int nPort, IntPtr pFileHeadBuf, uint nSize, uint nBufPoolSize);
|
||||
|
||||
/// <summary>
|
||||
/// 关闭流
|
||||
/// 功能:停止接收流数据,释放流缓冲区资源
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <returns>关闭成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_CloseStream(int nPort);
|
||||
|
||||
/// <summary>
|
||||
/// 开始播放
|
||||
/// 功能:启动解码和显示流程,绑定到指定窗口句柄
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="hWnd">显示窗口句柄(IntPtr.Zero 表示不绑定窗口)</param>
|
||||
/// <returns>启动成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_Play(int nPort, IntPtr hWnd);
|
||||
|
||||
/// <summary>
|
||||
/// 停止播放
|
||||
/// 功能:停止解码和显示,释放播放相关资源
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <returns>停止成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_Stop(int nPort);
|
||||
|
||||
/// <summary>
|
||||
/// 输入流数据
|
||||
/// 功能:向播放端口推送原始流数据,供解码使用
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="pBuf">流数据缓冲区指针</param>
|
||||
/// <param name="nSize">数据大小(字节)</param>
|
||||
/// <returns>输入成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_InputData(int nPort, IntPtr pBuf, uint nSize);
|
||||
|
||||
/// <summary>
|
||||
/// 设置解码回调 (Ex版本)
|
||||
/// 功能:绑定解码完成后的回调函数,用于获取解码后的帧数据
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="DecCBFun">解码回调函数委托</param>
|
||||
/// <param name="pDest">目标缓冲区指针(可为空)</param>
|
||||
/// <param name="nDestSize">目标缓冲区大小(字节)</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_SetDecCallBackEx(int nPort, DECCBFUN DecCBFun, IntPtr pDest, int nDestSize);
|
||||
|
||||
/// <summary>
|
||||
/// 设置流打开模式
|
||||
/// 功能:指定端口接收的流类型(实时流/文件流)
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="nMode">流模式(对应 STREAME_XXX 常量)</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_SetStreamOpenMode(int nPort, uint nMode);
|
||||
|
||||
/// <summary>
|
||||
/// 设置显示缓冲区数量
|
||||
/// 功能:调整显示缓冲帧数,平衡流畅度与延迟
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="nNum">缓冲帧数(范围:MIN_DIS_FRAMES ~ MAX_DIS_FRAMES)</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_SetDisplayBuf(int nPort, uint nNum);
|
||||
|
||||
/// <summary>
|
||||
/// 设置叠加模式
|
||||
/// 功能:配置图像叠加方式及透明色
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="bOverlay">是否启用叠加(1=启用,0=禁用)</param>
|
||||
/// <param name="colorKey">透明色键值</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_SetOverlayMode(int nPort, int bOverlay, uint colorKey);
|
||||
|
||||
/// <summary>
|
||||
/// 获取最后一次错误码
|
||||
/// 功能:API 调用失败后,获取具体错误原因(对应 PLAYM4_XXX 错误常量)
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <returns>错误码(0 表示无错误)</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern uint PlayM4_GetLastError(int nPort);
|
||||
|
||||
/// <summary>
|
||||
/// 设置视频解码模式
|
||||
/// 功能:切换硬解码/软解码模式(补充:用于硬件加速优化)
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <param name="nMode">解码模式(0=软解码,1=硬解码,具体值参考官方文档)</param>
|
||||
/// <returns>设置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_SetDecVideoMode(int nPort, int nMode);
|
||||
|
||||
/// <summary>
|
||||
/// 获取源缓冲区剩余空间
|
||||
/// 功能:查询端口流缓冲区的剩余可用空间(字节)
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <returns>剩余空间大小(字节)</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern uint PlayM4_GetSourceBufferRemain(int nPort);
|
||||
|
||||
/// <summary>
|
||||
/// 重置源缓冲区
|
||||
/// 功能:清空端口流缓冲区中的未解码数据
|
||||
/// </summary>
|
||||
/// <param name="nPort">播放端口号</param>
|
||||
/// <returns>重置成功返回 true,失败返回 false</returns>
|
||||
[DllImport(DllName)]
|
||||
public static extern bool PlayM4_ResetSourceBuffer(int nPort);
|
||||
|
||||
#endregion
|
||||
}
|
||||
125
SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs
Normal file
125
SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// [驱动支持层] 海康 SDK 全局资源管理器 (V3.3.1 修复版)
|
||||
/// <para>核心修复:</para>
|
||||
/// <para>1. [Bug S] 引用计数保护:增加下溢检测,防止异常销毁流程导致的计数器错乱。</para>
|
||||
/// <para>2. [Bug C] 禁用内部重连:确保 SDK 不会背着上层偷偷重连,彻底消除僵尸连接。</para>
|
||||
/// </summary>
|
||||
public static class HikSdkManager
|
||||
{
|
||||
#region --- 全局状态与锁 (Global States & Locks) ---
|
||||
|
||||
/// <summary>
|
||||
/// 全局引用计数器。
|
||||
/// 只有当计数从 0 变 1 时才进行物理初始化,从 1 变 0 时才物理卸载。
|
||||
/// </summary>
|
||||
private static int _referenceCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 静态同步锁。
|
||||
/// 用于保护 _referenceCount 的原子操作,防止多线程并发 Start/Stop 时导致的初始化冲突。
|
||||
/// </summary>
|
||||
private static readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 播放库预热状态标记。
|
||||
/// 用于避免重复执行硬件探测(首次预热后后续直接返回)。
|
||||
/// </summary>
|
||||
private static bool _isWarmedUp = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- SDK 初始化与卸载 (SDK Initialization & Uninstallation) ---
|
||||
|
||||
/// <summary>
|
||||
/// 全局初始化海康 SDK 环境。
|
||||
/// <para>此方法是幂等的,内部会自动增加引用计数,支持多线程并发调用。</para>
|
||||
/// </summary>
|
||||
/// <returns>初始化成功返回 true;若 SDK 核心组件(HCNetSDK.dll)加载失败则返回 false。</returns>
|
||||
public static bool Initialize()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// 引用计数为 0 时执行物理初始化(仅首次调用时触发)
|
||||
if (_referenceCount == 0)
|
||||
{
|
||||
// [物理初始化] 调用海康核心 DLL 接口,初始化 SDK 基础环境
|
||||
if (!HikNativeMethods.NET_DVR_Init()) return false;
|
||||
|
||||
// --- 工业级可靠性设置(注释保留,按需启用)---
|
||||
//// 1. 登录超时设置 (3000ms):
|
||||
//// 在高并发场景下,快速失败比无限重试更有利于系统调度。
|
||||
//HikNativeMethods.NET_DVR_SetConnectTime(3000, 1);
|
||||
|
||||
//// 2. [Fix Bug C: 双重重连冲突]
|
||||
//// 设计思路:禁用海康 SDK 内部的自动重连机制(bEnableRecon = false)。
|
||||
//// 理由:SDK 内部重连是非透明的,无法与我们的上层协调器 (Coordinator) 状态机完美对齐。
|
||||
//// 统一由外层协调器负责“检测断线 -> 销毁旧句柄 -> 重新登录”,确保状态的一致性。
|
||||
//HikNativeMethods.NET_DVR_SetReconnect(10000, false);
|
||||
}
|
||||
|
||||
// 引用计数递增,记录当前活跃的 SDK 使用者数量
|
||||
_referenceCount++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局卸载海康 SDK 环境。
|
||||
/// <para>当所有相机实例都停止并释放后(引用计数归 0),会真正释放非托管资源。</para>
|
||||
/// </summary>
|
||||
public static void Uninitialize()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// [Fix Bug S: 引用计数溢出保护]
|
||||
// 确保不会因为意外的多次调用导致计数器变为负数,避免逻辑异常
|
||||
if (_referenceCount > 0)
|
||||
{
|
||||
_referenceCount--;
|
||||
|
||||
// 引用计数归 0 时执行物理卸载,关闭 SDK 所有隐形线程与资源
|
||||
if (_referenceCount == 0)
|
||||
{
|
||||
// [物理卸载] 释放 SDK 占用的非托管资源(如网络连接、内存缓冲区)
|
||||
HikNativeMethods.NET_DVR_Cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 播放库预热 (PlayCtrl Warm-up) ---
|
||||
|
||||
/// <summary>
|
||||
/// [核心策略] 强制冷启动诱发
|
||||
/// 职责:在系统真正取流前,强行触发一次 PlayCtrl.dll 的硬件探测(声卡、显卡、DirectDraw)
|
||||
/// 目的:规避首次取流时的 12-18 秒延迟,提前完成硬件初始化
|
||||
/// </summary>
|
||||
public static void ForceWarmUp()
|
||||
{
|
||||
// 已预热过则直接返回,避免重复执行
|
||||
if (_isWarmedUp) return;
|
||||
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 正在进行播放库硬件探测预热,请稍候...");
|
||||
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
int tempPort = -1;
|
||||
|
||||
// 第一次调用 PlayM4_GetPort:触发 PlayCtrl.dll 底层硬件初始化(耗时主要集中在这里)
|
||||
if (HikPlayMethods.PlayM4_GetPort(ref tempPort))
|
||||
{
|
||||
// 必须释放临时端口,避免端口资源泄漏(PlayCtrl.dll 端口数量有限)
|
||||
HikPlayMethods.PlayM4_FreePort(tempPort);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
_isWarmedUp = true;
|
||||
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 预热完成!耗时: {sw.ElapsedMilliseconds}ms。后续调用将恢复正常。");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
386
SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs
Normal file
386
SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// [海康驱动] 工业级视频源实现 V3.3.1 (极高并发修正版)
|
||||
/// 核心职责:负责海康威视设备 (SDK) 的物理连接、取流、解码与资源管理。
|
||||
/// 核心修复记录:
|
||||
/// 1. [Bug X] 异步竞争:引入 Epoch 世代验证,防止超时取消后的幽灵任务覆盖新连接。
|
||||
/// 2. [Bug Y] 内存踩踏:解码回调加锁,防止多核环境下共享 Mat 被并发读写引发 AV 异常。
|
||||
/// 3. [Bug α] 端口抢占:PlayM4_GetPort 全局加锁,防止高并发启动时的播放端口串位。
|
||||
/// 4. [Bug H/W/T/E] 继承之前的路由分发、幽灵句柄、零 GC、异步启动等修复。
|
||||
/// </summary>
|
||||
public class HikVideoSource : BaseVideoSource
|
||||
{
|
||||
#region --- 静态资源 (Global Resources) ---
|
||||
|
||||
// 静态路由表 (Fix Bug H: 友军误伤)
|
||||
// 作用:将海康 SDK 的全局回调(仅带 UserID)精准路由到具体的 HikVideoSource 实例
|
||||
private static readonly ConcurrentDictionary<int, HikVideoSource> _instances = new();
|
||||
|
||||
// 全局异常回调委托(防止 GC 回收)
|
||||
private static readonly HikNativeMethods.EXCEPTION_CALLBACK _globalExceptionCallback = StaticOnSdkException;
|
||||
|
||||
// [Fix Bug α: 端口抢占]
|
||||
// 背景:海康播放库 PlayCtrl.dll 的 PlayM4_GetPort 函数内部使用了非线程安全的全局计数器。
|
||||
// 作用:使用全局静态锁强制串行化端口申请操作,防止高并发启动时分配到相同的 Port。
|
||||
private static readonly object _globalPortLock = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 实例成员 (Instance Members) ---
|
||||
|
||||
private int _userId = -1; // SDK 登录句柄(-1 表示未登录)
|
||||
private int _realPlayHandle = -1; // 预览句柄 (网络层,-1 表示未开启预览)
|
||||
private int _playPort = -1; // 播放端口 (解码层,-1 表示未分配端口)
|
||||
|
||||
private readonly object _initLock = new(); // 初始化/清理互斥锁:保护启动/停止流程的原子性
|
||||
private readonly object _bufferLock = new(); // 解码缓冲区锁 (Fix Bug Y: 防止多线程并发读写内存)
|
||||
|
||||
// [Fix Bug X: 异步状态竞争]
|
||||
// 作用:连接世代计数器,每次 StartAsync 调用时自增
|
||||
// 原理:异步任务执行过程中验证是否为最新请求,避免幽灵任务覆盖状态
|
||||
private volatile int _connectionEpoch = 0;
|
||||
|
||||
// 回调委托引用:必须持有以防止 P/Invoke 过程中被 GC 回收,导致回调崩溃
|
||||
private HikNativeMethods.REALDATACALLBACK? _realDataCallBack;
|
||||
private HikPlayMethods.DECCBFUN? _decCallBack;
|
||||
|
||||
// 内存复用对象 (Fix Bug T):复用非托管内存块,减少 LOH (大对象堆) 分配压力
|
||||
private Mat? _sharedYuvMat;
|
||||
private Mat? _sharedBgrMat;
|
||||
|
||||
// 帧对象池:实现零 GC 分配,避免频繁创建/销毁 Mat 导致的性能抖动
|
||||
private FramePool? _framePool;
|
||||
private bool _isPoolReady = false; // 帧池初始化状态标记
|
||||
|
||||
// 帧需求控制器:管理不同订阅者(UI/AI)的帧率需求,实现按需分发
|
||||
public FrameController Controller { get; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 (Constructor) ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化海康视频源实例
|
||||
/// </summary>
|
||||
/// <param name="config">视频源基础配置(含设备IP、账号、码流类型等)</param>
|
||||
public HikVideoSource(VideoSourceConfig config) : base(config) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 核心生命周期 (Core Lifecycle) ---
|
||||
|
||||
/// <summary>
|
||||
/// [异步启动核心] (含 Bug E/X 修复)
|
||||
/// 流程:SDK环境初始化 → 注册全局回调 → 物理登录设备 → 路由注册 → 开启网络预览
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌:用于终止超时或中断的启动流程</param>
|
||||
protected override async Task OnStartAsync(CancellationToken token)
|
||||
{
|
||||
// [Fix Bug X] 记录当前启动世代,标记一次新的启动请求
|
||||
int currentEpoch = Interlocked.Increment(ref _connectionEpoch);
|
||||
|
||||
// [Fix Bug E] 切换到后台线程执行:避免阻塞UI/上下文线程(登录为同步阻塞操作)
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// [Fix Bug X] 世代验证:若已存在新的启动请求,直接放弃当前任务
|
||||
if (currentEpoch != _connectionEpoch) return;
|
||||
|
||||
// 初始化海康 SDK 环境(引用计数管理,确保资源不重复加载)
|
||||
if (!HikSdkManager.Initialize())
|
||||
throw new CameraException(CameraErrorCode.SdkNotInitialized, "SDK初始化失败", DeviceBrand.HikVision);
|
||||
|
||||
try
|
||||
{
|
||||
// 注册全局异常回调:捕获断线、重连等SDK层面异常
|
||||
HikNativeMethods.NET_DVR_SetExceptionCallBack_V30(0, IntPtr.Zero, _globalExceptionCallback, IntPtr.Zero);
|
||||
|
||||
// 执行设备物理登录(阻塞调用,网络异常时可能耗时数秒)
|
||||
var devInfo = new HikNativeMethods.NET_DEVICEINFO_V30();
|
||||
int newUserId = HikNativeMethods.NET_DVR_Login_V30(
|
||||
_config.IpAddress, _config.Port, _config.Username, _config.Password, ref devInfo);
|
||||
|
||||
// [Fix Bug X] 登录后再次验证世代:避免超时后产生的幽灵句柄
|
||||
if (currentEpoch != _connectionEpoch)
|
||||
{
|
||||
if (newUserId >= 0) HikNativeMethods.NET_DVR_Logout(newUserId); // 释放僵尸句柄
|
||||
throw new OperationCanceledException("启动任务已过期(被新的请求抢占)");
|
||||
}
|
||||
|
||||
_userId = newUserId;
|
||||
if (_userId < 0)
|
||||
{
|
||||
uint err = HikNativeMethods.NET_DVR_GetLastError();
|
||||
throw new CameraException(HikErrorMapper.Map(err), $"登录失败: {err}", DeviceBrand.HikVision, (int)err);
|
||||
}
|
||||
|
||||
// [Bug H] 路由注册:将 UserID 与当前实例绑定,支持全局回调路由
|
||||
_instances.TryAdd(_userId, this);
|
||||
|
||||
// 开启网络预览(取流):失败则抛出异常,触发资源清理
|
||||
if (!StartRealPlay())
|
||||
{
|
||||
uint err = HikNativeMethods.NET_DVR_GetLastError();
|
||||
throw new CameraException(HikErrorMapper.Map(err), $"预览失败: {err}", DeviceBrand.HikVision, (int)err);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// [Fix Bug W] 异常清理:启动失败时执行完整资源释放,防止句柄泄漏
|
||||
CleanupSync();
|
||||
throw;
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步停止设备:终止取流、解码,释放所有资源
|
||||
/// </summary>
|
||||
protected override async Task OnStopAsync()
|
||||
{
|
||||
// [Fix Bug X] 停止时递增世代:立即使所有正在进行的启动任务失效
|
||||
Interlocked.Increment(ref _connectionEpoch);
|
||||
|
||||
// 在后台线程执行同步清理逻辑,避免阻塞调用线程
|
||||
await Task.Run(() => CleanupSync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [同步清理核心] (含 Bug Y 锁保护)
|
||||
/// 职责:按“停止取流→释放解码资源→释放内存→注销登录”顺序销毁,防止非托管崩溃
|
||||
/// </summary>
|
||||
private void CleanupSync()
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
// 1. 停止网络取流:释放预览句柄
|
||||
if (_realPlayHandle >= 0)
|
||||
{
|
||||
HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle);
|
||||
_realPlayHandle = -1;
|
||||
}
|
||||
|
||||
// 2. 停止解码并释放播放端口:避免端口资源泄漏
|
||||
if (_playPort >= 0)
|
||||
{
|
||||
HikPlayMethods.PlayM4_Stop(_playPort);
|
||||
HikPlayMethods.PlayM4_CloseStream(_playPort);
|
||||
HikPlayMethods.PlayM4_FreePort(_playPort);
|
||||
_playPort = -1;
|
||||
}
|
||||
|
||||
// [Fix Bug Y] 内存释放保护:确保解码回调未在使用内存
|
||||
lock (_bufferLock)
|
||||
{
|
||||
_sharedYuvMat?.Dispose(); _sharedYuvMat = null;
|
||||
_sharedBgrMat?.Dispose(); _sharedBgrMat = null;
|
||||
}
|
||||
|
||||
// 3. 注销登录:先移除路由映射,再释放登录句柄
|
||||
if (_userId >= 0)
|
||||
{
|
||||
_instances.TryRemove(_userId, out _);
|
||||
HikNativeMethods.NET_DVR_Logout(_userId);
|
||||
_userId = -1;
|
||||
}
|
||||
|
||||
// 4. 释放帧对象池:清理复用内存
|
||||
_framePool?.Dispose();
|
||||
_framePool = null;
|
||||
_isPoolReady = false;
|
||||
}
|
||||
|
||||
// 5. 减少SDK全局引用计数:确保最后一个实例销毁时卸载SDK
|
||||
HikSdkManager.Uninitialize();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 网络取流 (Network Streaming) ---
|
||||
|
||||
/// <summary>
|
||||
/// 开启网络取流:配置预览参数,绑定流数据回调
|
||||
/// </summary>
|
||||
/// <returns>取流开启成功返回 true,失败返回 false</returns>
|
||||
private bool StartRealPlay()
|
||||
{
|
||||
var previewInfo = new HikNativeMethods.NET_DVR_PREVIEWINFO
|
||||
{
|
||||
hPlayWnd = IntPtr.Zero, // 句柄为空:SDK不直接渲染,通过回调获取原始流数据
|
||||
lChannel = _config.ChannelIndex, // 设备通道号(从配置读取)
|
||||
dwStreamType = (uint)_config.StreamType, // 码流类型(主码流/子码流,从配置读取)
|
||||
bBlocked = false // 非阻塞取流:避免长时间阻塞线程
|
||||
};
|
||||
|
||||
// 绑定网络流回调:接收SDK推送的原始流数据
|
||||
_realDataCallBack = new HikNativeMethods.REALDATACALLBACK(SafeOnRealDataReceived);
|
||||
_realPlayHandle = HikNativeMethods.NET_DVR_RealPlay_V40(_userId, ref previewInfo, _realDataCallBack, IntPtr.Zero);
|
||||
|
||||
return _realPlayHandle >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网络流数据回调 (RealDataCallBack)
|
||||
/// 职责:接收 SDK 原始流数据,系统头用于初始化播放库,流数据用于解码
|
||||
/// </summary>
|
||||
private void SafeOnRealDataReceived(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 预览句柄无效时直接返回,避免无效处理
|
||||
if (_realPlayHandle == -1) return;
|
||||
|
||||
// 处理系统头:初始化播放库(仅首次接收系统头时执行)
|
||||
if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1)
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
// 双重校验:防止多线程下重复初始化
|
||||
if (_realPlayHandle == -1 || _playPort != -1) return;
|
||||
|
||||
// [Fix Bug α: 端口抢占] 全局锁保护端口申请,避免并发冲突
|
||||
DateTime timeStart = DateTime.Now;
|
||||
bool getPortSuccess;
|
||||
lock (_globalPortLock)
|
||||
{
|
||||
getPortSuccess = HikPlayMethods.PlayM4_GetPort(ref _playPort);
|
||||
}
|
||||
var useTime = Math.Round((DateTime.Now - timeStart).TotalSeconds, 1);
|
||||
|
||||
if (!getPortSuccess) return;
|
||||
|
||||
// 关键配置:设置播放缓冲区为最小值1,减少延时(禁止播放库积压数据)
|
||||
HikPlayMethods.PlayM4_SetDisplayBuf(_playPort, 1);
|
||||
|
||||
// 初始化播放库:设置流模式→打开流→绑定解码回调→开始解码
|
||||
HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); // 0=实时流模式
|
||||
if (!HikPlayMethods.PlayM4_OpenStream(_playPort, pBuffer, dwBufSize, 2 * 1024 * 1024))
|
||||
{
|
||||
HikPlayMethods.PlayM4_FreePort(_playPort);
|
||||
_playPort = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
_decCallBack = new HikPlayMethods.DECCBFUN(SafeOnDecodingCallBack);
|
||||
HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0);
|
||||
HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
// 处理流数据:将原始流数据传入播放库解码
|
||||
else if (dwDataType == HikNativeMethods.NET_DVR_STREAMDATA && _playPort != -1)
|
||||
{
|
||||
HikPlayMethods.PlayM4_InputData(_playPort, pBuffer, dwBufSize);
|
||||
}
|
||||
}
|
||||
catch { /* 吞没回调异常:防止回调崩溃导致整个SDK进程退出 */ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 解码与帧分发 (Decoding & Frame Distribution) ---
|
||||
|
||||
/// <summary>
|
||||
/// 解码回调 (DecCallBack)
|
||||
/// 职责:接收解码后的 YUV 数据,转码为 BGR 格式,通过帧池复用内存并分发
|
||||
/// </summary>
|
||||
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
|
||||
{
|
||||
// 汇报心跳:更新帧接收时间,防止哨兵判定设备僵死
|
||||
MarkFrameReceived();
|
||||
|
||||
// 1. 帧分发决策:根据订阅者需求判断是否需要保留当前帧(耗时<0.01ms)
|
||||
var decision = Controller.MakeDecision(Environment.TickCount64);
|
||||
if (!decision.IsCaptured) return;
|
||||
|
||||
int width = pFrameInfo.nWidth;
|
||||
int height = pFrameInfo.nHeight;
|
||||
|
||||
// 2. 初始化帧池:首次解码时创建,按实际分辨率分配内存
|
||||
if (!_isPoolReady)
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
if (!_isPoolReady)
|
||||
{
|
||||
_framePool?.Dispose();
|
||||
// 帧池配置:CV_8UC3=BGR格式,初始3帧,最大5帧(平衡内存与性能)
|
||||
_framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5);
|
||||
_isPoolReady = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_framePool == null) return;
|
||||
|
||||
// 3. 从帧池获取内存:零GC分配,池满时返回null(直接丢帧,避免积压)
|
||||
SmartFrame smartFrame = _framePool.Get();
|
||||
try
|
||||
{
|
||||
if (smartFrame == null) return; // 帧池满,丢弃当前帧
|
||||
|
||||
try
|
||||
{
|
||||
// 4. YUV转BGR:直接写入帧池内存,无中间对象分配
|
||||
using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
|
||||
{
|
||||
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
|
||||
}
|
||||
|
||||
// 5. 对外分发帧数据:通过基类事件通知订阅者(零拷贝)
|
||||
RaiseFrameReceived(smartFrame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 异常时释放帧:避免内存泄漏
|
||||
smartFrame.Dispose();
|
||||
Debug.WriteLine($"[DecodingError] {ex.Message}");
|
||||
}
|
||||
|
||||
// 6. 提交到全局处理中心:后续由管道处理二次加工与分发
|
||||
GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 释放驱动层引用:驱动职责结束,引用计数-1(由消费者/管道管理后续生命周期)
|
||||
smartFrame.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 异常处理 (Exception Handling) ---
|
||||
|
||||
/// <summary>
|
||||
/// 全局异常回调处理
|
||||
/// 职责:将 SDK 全局异常(仅带 UserID)路由到对应的 HikVideoSource 实例
|
||||
/// </summary>
|
||||
private static void StaticOnSdkException(uint dwType, int lUserID, int lHandle, IntPtr pUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通过 UserID 查找实例,触发实例内异常处理逻辑
|
||||
if (_instances.TryGetValue(lUserID, out var instance))
|
||||
{
|
||||
instance.ReportError(new CameraException(
|
||||
CameraErrorCode.NetworkUnreachable,
|
||||
$"SDK全局报警异常: 0x{dwType:X}",
|
||||
DeviceBrand.HikVision));
|
||||
}
|
||||
}
|
||||
catch { /* 吞没异常:避免全局回调崩溃 */ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 元数据获取 (Metadata Fetching) ---
|
||||
|
||||
/// <summary>
|
||||
/// 占位实现:暂未实现设备元数据获取逻辑
|
||||
/// 注:实际场景需补充,用于获取设备型号、通道能力等信息
|
||||
/// </summary>
|
||||
protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
|
||||
|
||||
#endregion
|
||||
}
|
||||
147
SHH.CameraSdk/Program.cs
Normal file
147
SHH.CameraSdk/Program.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using SHH.CameraSdk;
|
||||
using System.Diagnostics;
|
||||
|
||||
// ==============================================================================
|
||||
// 1. 基础设施初始化
|
||||
// ==============================================================================
|
||||
InitHardwareEnv();
|
||||
using var cameraManager = new CameraManager();
|
||||
|
||||
// ==============================================================================
|
||||
// 2. 启动 Web 监控与诊断服务
|
||||
// ==============================================================================
|
||||
var app = await StartWebMonitoring(cameraManager);
|
||||
|
||||
// ==============================================================================
|
||||
// 3. 业务编排:配置设备与流控策略 (8+2 演示)
|
||||
// ==============================================================================
|
||||
await ConfigureBusinessLogic(cameraManager);
|
||||
|
||||
// ==============================================================================
|
||||
// 4. 启动引擎与交互
|
||||
// ==============================================================================
|
||||
Console.WriteLine("\n[系统] 正在启动全局管理引擎...");
|
||||
await cameraManager.StartAsync();
|
||||
|
||||
Console.WriteLine(">> 系统就绪。访问 http://localhost:5000/swagger 查看诊断信息。");
|
||||
Console.WriteLine(">> 按 'S' 键退出...");
|
||||
|
||||
while (Console.ReadKey(true).Key != ConsoleKey.S) { Thread.Sleep(100); }
|
||||
|
||||
Console.WriteLine("[系统] 正在停机...");
|
||||
await app.StopAsync();
|
||||
|
||||
|
||||
// ==============================================================================
|
||||
// Local Functions (方法拆分)
|
||||
// ==============================================================================
|
||||
|
||||
static void InitHardwareEnv()
|
||||
{
|
||||
Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.3 分层版) ===");
|
||||
Console.WriteLine("[硬件] 海康驱动预热中...");
|
||||
HikNativeMethods.NET_DVR_Init();
|
||||
HikSdkManager.ForceWarmUp(); // 强制加载 PlayCtrl.dll
|
||||
Console.WriteLine("[硬件] 预热完成。");
|
||||
}
|
||||
|
||||
static async Task<WebApplication> StartWebMonitoring(CameraManager manager)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
|
||||
// 注入服务
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "SHH Camera Diagnostics", Version = "v1" });
|
||||
});
|
||||
builder.Services.AddCors(o => o.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
|
||||
|
||||
// 关键:注入单例 Manager
|
||||
builder.Services.AddSingleton(manager);
|
||||
|
||||
var webApp = builder.Build();
|
||||
|
||||
// 配置管道
|
||||
webApp.UseSwagger();
|
||||
webApp.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Diagnostics V1"));
|
||||
webApp.UseCors("AllowAll");
|
||||
webApp.MapControllers();
|
||||
|
||||
// 异步启动,不阻塞主线程
|
||||
_ = webApp.RunAsync("http://0.0.0.0:5000");
|
||||
Console.WriteLine("[Web] 监控API已启动: http://localhost:5000");
|
||||
|
||||
return webApp;
|
||||
}
|
||||
|
||||
static async Task ConfigureBusinessLogic(CameraManager manager)
|
||||
{
|
||||
// 1. 配置设备
|
||||
var config = new VideoSourceConfig
|
||||
{
|
||||
Id = 101,
|
||||
Brand = DeviceBrand.HikVision,
|
||||
IpAddress = "172.16.41.206",
|
||||
Port = 8000,
|
||||
Username = "admin",
|
||||
Password = "abcd1234",
|
||||
StreamType = 0 // 主码流
|
||||
};
|
||||
manager.AddDevice(config);
|
||||
|
||||
if (manager.GetDevice(101) is HikVideoSource hikCamera)
|
||||
{
|
||||
// 2. 注册需求 (告诉控制器我要什么)
|
||||
// ----------------------------------------------------
|
||||
hikCamera.Controller.Register("WPF_Display_Main", 8); // UI 要 8 帧
|
||||
hikCamera.Controller.Register("AI_Behavior_Engine", 2); // AI 要 2 帧
|
||||
|
||||
// 1. 注册差异化需求 (给每个消费者唯一的 AppId)
|
||||
// ----------------------------------------------------
|
||||
// 模拟:A 进程(如远程预览)带宽有限,只要 3fps
|
||||
hikCamera.Controller.Register("Process_A_Remote", 3);
|
||||
|
||||
// 模拟:B 进程(如本地大屏)性能强劲,要 8fps
|
||||
hikCamera.Controller.Register("Process_B_Local", 8);
|
||||
|
||||
// 模拟:AI 引擎
|
||||
hikCamera.Controller.Register("AI_Engine_Core", 2);
|
||||
|
||||
// 2. 精准订阅 (Subscribe 替代了 +=)
|
||||
// ----------------------------------------------------
|
||||
|
||||
// [消费者 A] - 绝对只会收到 3fps
|
||||
GlobalStreamDispatcher.Subscribe("Process_A_Remote", (deviceId, frame) =>
|
||||
{
|
||||
// 这里不需要判断 deviceId,也不需要判断 frame 类型
|
||||
// 能进这个回调,说明这帧就是专为 Process_A_Remote 准备的
|
||||
if (deviceId == 101)
|
||||
{
|
||||
Console.WriteLine($"[Process A] 远程推流一帧 (3fps节奏)");
|
||||
}
|
||||
});
|
||||
|
||||
// [消费者 B] - 绝对只会收到 8fps
|
||||
GlobalStreamDispatcher.Subscribe("Process_B_Local", (deviceId, frame) =>
|
||||
{
|
||||
if (deviceId == 101)
|
||||
{
|
||||
Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)");
|
||||
}
|
||||
});
|
||||
|
||||
// [消费者 AI]
|
||||
GlobalStreamDispatcher.Subscribe("AI_Engine_Core", (deviceId, frame) =>
|
||||
{
|
||||
if (deviceId == 101)
|
||||
{
|
||||
Console.WriteLine($" >>> [AI] 分析一帧...");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
33
SHH.CameraSdk/SHH.CameraSdk.csproj
Normal file
33
SHH.CameraSdk/SHH.CameraSdk.csproj
Normal file
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Drivers\NativeLibraryLoader.cs" />
|
||||
<Compile Remove="Drivers\VideoSourceFactory.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.20250507" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System.Collections.Concurrent" />
|
||||
<Using Include="System.Collections.ObjectModel" />
|
||||
<Using Include="System.Diagnostics" />
|
||||
<Using Include="System.Runtime.InteropServices" />
|
||||
<Using Include="System.Text" />
|
||||
<Using Include="System.Text.Json.Serialization" />
|
||||
<Using Include="System.Threading.Channels" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user