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

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

View File

@@ -0,0 +1,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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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 → 上限 120s2分钟
/// </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
}

View 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
}

View 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>
/// 是否支持云台控制PTZPan/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
}

View 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 通常为 1NVR 为接入路数) </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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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 通常为 1NVR 对应接入的摄像头通道索引) </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
}

View 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
}

View File

@@ -0,0 +1,171 @@
using OpenCvSharp;
namespace SHH.CameraSdk;
/// <summary>
/// [消费者] 专用渲染线程(零延迟设计)
/// 核心策略:
/// <para>1. 容量为1的阻塞队列仅保留最新一帧杜绝帧堆积</para>
/// <para>2. 非阻塞入队+主动丢帧:渲染慢时直接丢弃新帧,确保主线程不阻塞</para>
/// <para>3. 引用计数联动:丢帧时立即释放引用,内存自动回池复用</para>
/// </summary>
public class FrameConsumer : IDisposable
{
#region --- (Private Resources & States) ---
/// <summary> 帧缓冲队列容量1仅存储最新一帧保证零延迟渲染 </summary>
/// <remarks> BlockingCollection 封装线程安全操作GetConsumingEnumerable 支持取消令牌 </remarks>
private readonly BlockingCollection<SmartFrame> _frameBuffer = new(1);
/// <summary> 取消令牌源:用于终止渲染循环 </summary>
private readonly CancellationTokenSource _cts = new();
/// <summary> 后台渲染任务 </summary>
private Task? _renderTask;
/// <summary> OpenCV 窗口名称 </summary>
private readonly string _windowName;
#endregion
#region --- (Constructor & Lifecycle) ---
/// <summary>
/// 初始化帧渲染消费者
/// </summary>
/// <param name="windowName">OpenCV 显示窗口名称</param>
public FrameConsumer(string windowName = "Zero Latency Preview")
{
_windowName = windowName;
}
/// <summary>
/// 启动渲染线程
/// </summary>
public void Start()
{
// 防止重复启动
if (_renderTask != null) return;
// 启动长期运行的渲染任务,提升线程调度优先级
_renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning);
Console.WriteLine($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}");
}
/// <summary>
/// 停止渲染线程并清理资源
/// </summary>
public void Stop()
{
// 发送取消信号,终止渲染循环
_cts.Cancel();
// 标记队列完成添加,触发 GetConsumingEnumerable 退出遍历
_frameBuffer.CompleteAdding();
// 等待渲染任务结束最多等待1秒防止卡死
if (_renderTask != null)
{
Task.WaitAny(_renderTask, Task.Delay(1000));
_renderTask = null;
}
// 清理队列残余帧:释放所有未消费帧的引用,防止内存泄漏
while (_frameBuffer.TryTake(out var residualFrame))
{
residualFrame.Dispose();
}
Console.WriteLine($"[Consumer] 渲染线程已停止,窗口: {_windowName}");
}
#endregion
#region --- (Frame Enqueue & Render Logic) ---
/// <summary>
/// [生产端入口] 接收帧并尝试入队(非阻塞)
/// </summary>
/// <param name="frame">待渲染的智能帧</param>
public void Enqueue(SmartFrame frame)
{
// 防护:线程已停止,直接释放帧引用
if (_cts.IsCancellationRequested)
{
frame.Dispose();
return;
}
// 核心零延迟策略:非阻塞尝试入队
// 队列满 → 上一帧未渲染完成 → 丢弃当前帧,释放引用
if (!_frameBuffer.TryAdd(frame))
{
frame.Dispose();
// Debug.WriteLine($"[Drop] 渲染线程[{_windowName}]处理过慢,丢弃一帧");
}
// 入队成功 → 帧由队列托管,等待渲染线程消费
}
/// <summary>
/// 后台渲染循环(核心逻辑)
/// </summary>
private void RenderLoop()
{
// 创建 OpenCV 显示窗口
Cv2.NamedWindow(_windowName, WindowFlags.Normal);
try
{
// 阻塞遍历队列:队列空时等待,收到取消信号时退出
foreach (var frame in _frameBuffer.GetConsumingEnumerable(_cts.Token))
{
try
{
// 渲染有效性校验Mat 未释放且不为空
if (frame.InternalMat != null && !frame.InternalMat.IsDisposed)
{
// 零拷贝渲染:直接引用 InternalMat无内存复制开销
Cv2.ImShow(_windowName, frame.InternalMat);
// 1ms 等待 UI 事件响应(必须调用,否则窗口无响应)
Cv2.WaitKey(1);
}
}
catch (Exception ex)
{
Debug.WriteLine($"[RenderError] 窗口[{_windowName}]渲染失败: {ex.Message}");
}
finally
{
// 至关重要:渲染完成后释放帧引用
// 引用计数归零 → 帧自动回池复用,避免内存泄漏
frame.Dispose();
}
}
}
catch (OperationCanceledException)
{
// 正常取消,无需处理
Debug.WriteLine($"[RenderInfo] 窗口[{_windowName}]渲染循环已取消");
}
finally
{
// 销毁 OpenCV 窗口,释放 UI 资源
Cv2.DestroyWindow(_windowName);
}
}
#endregion
#region --- (Disposal) ---
/// <summary>
/// 释放所有资源
/// </summary>
public void Dispose()
{
Stop();
_frameBuffer.Dispose();
_cts.Dispose();
}
#endregion
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
}

View 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
}

View 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
}

View 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-DVR2-ATM DVR3-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-RTSP2-同时支持两者 </summary>
public byte byMainProto;
/// <summary> 子码流传输协议类型0-私有协议1-RTSP2-同时支持两者 </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-码流3bit2-码流4bit7-主码流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-码流33-码流4以此类推 </summary>
public uint dwStreamType;
/// <summary> 连接方式0-TCP1-UDP2-多播3-RTP4-RTP/RTSP5-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> 流IDlChannel为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-500表示默认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 对齐,成员类型统一为 uint4字节与 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">用户IDNET_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">用户IDNET_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">用户IDNET_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
}

View 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
}

View 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
}

View 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
View 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] 分析一帧...");
}
});
}
}

View 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>