From 6281f4248eec5f0c5abd8dda3117b7ea234428af Mon Sep 17 00:00:00 2001 From: twice109 <3518499@qq.com> Date: Fri, 26 Dec 2025 03:18:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=B7=E5=BA=B7=E6=91=84=E5=83=8F=E5=A4=B4?= =?UTF-8?q?=E5=8F=96=E6=B5=81=E7=A4=BA=E4=BE=8B=E5=88=9D=E5=A7=8B=E7=AD=BE?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abstractions/Enums/DeviceBrand.cs | 62 +++ .../Abstractions/Enums/TransportProtocol.cs | 16 + .../Abstractions/Enums/VideoSourceStatus.cs | 57 ++ .../Abstractions/Errors/CameraErrorCode.cs | 226 ++++++++ .../Abstractions/Errors/CameraException.cs | 107 ++++ .../Abstractions/Errors/RecoveryAction.cs | 55 ++ .../Abstractions/Errors/RecoveryPolicy.cs | 100 ++++ SHH.CameraSdk/Abstractions/IVideoSource.cs | 91 ++++ .../Abstractions/Models/ChannelMetadata.cs | 107 ++++ .../Abstractions/Models/DeviceMetadata.cs | 176 ++++++ .../Models/DynamicStreamOptions.cs | 120 ++++ .../Abstractions/Models/MetadataDiff.cs | 53 ++ .../Abstractions/Models/ResolutionProfile.cs | 57 ++ .../Models/StatusChangedEventArgs.cs | 80 +++ .../Abstractions/Models/VideoSourceConfig.cs | 150 +++++ .../Controllers/MonitorController.cs | 100 ++++ SHH.CameraSdk/Core/Features/FrameConsumer.cs | 171 ++++++ .../Core/Features/SnapshotCoordinator.cs | 92 ++++ SHH.CameraSdk/Core/Manager/CameraManager.cs | 233 ++++++++ SHH.CameraSdk/Core/Memory/FramePool.cs | 140 +++++ SHH.CameraSdk/Core/Memory/SmartFrame.cs | 95 ++++ .../Core/Pipeline/GlobalProcessingCenter.cs | 64 +++ .../Core/Pipeline/GlobalStreamDispatcher.cs | 157 ++++++ .../Core/Pipeline/ProcessingPipeline.cs | 148 +++++ SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs | 40 ++ .../Core/Resilience/CameraCoordinator.cs | 231 ++++++++ .../Core/Scheduling/FrameController.cs | 86 +++ .../Core/Scheduling/FrameDecision.cs | 34 ++ .../Core/Scheduling/FrameRequirement.cs | 37 ++ .../Core/Telemetry/CameraHealthReport.cs | 50 ++ .../Core/Telemetry/CameraTelemetryInfo.cs | 56 ++ .../Core/Telemetry/FrameConsumerType.cs | 21 + SHH.CameraSdk/Core/Telemetry/FrameContext.cs | 61 +++ SHH.CameraSdk/Core/Telemetry/FrameTrace.cs | 43 ++ .../Core/Telemetry/GlobalTelemetry.cs | 66 +++ SHH.CameraSdk/Drivers/BaseVideoSource.cs | 512 ++++++++++++++++++ .../Drivers/HikVision/HikErrorMapper.cs | 124 +++++ .../Drivers/HikVision/HikExtensions.cs | 37 ++ .../Drivers/HikVision/HikNativeMethods.cs | 495 +++++++++++++++++ .../Drivers/HikVision/HikPlayMethods.cs | 347 ++++++++++++ .../Drivers/HikVision/HikSdkManager.cs | 125 +++++ .../Drivers/HikVision/HikVideoSource.cs | 386 +++++++++++++ SHH.CameraSdk/Program.cs | 147 +++++ SHH.CameraSdk/SHH.CameraSdk.csproj | 33 ++ 44 files changed, 5588 insertions(+) create mode 100644 SHH.CameraSdk/Abstractions/Enums/DeviceBrand.cs create mode 100644 SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs create mode 100644 SHH.CameraSdk/Abstractions/Enums/VideoSourceStatus.cs create mode 100644 SHH.CameraSdk/Abstractions/Errors/CameraErrorCode.cs create mode 100644 SHH.CameraSdk/Abstractions/Errors/CameraException.cs create mode 100644 SHH.CameraSdk/Abstractions/Errors/RecoveryAction.cs create mode 100644 SHH.CameraSdk/Abstractions/Errors/RecoveryPolicy.cs create mode 100644 SHH.CameraSdk/Abstractions/IVideoSource.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/ChannelMetadata.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/DeviceMetadata.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/MetadataDiff.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/ResolutionProfile.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/StatusChangedEventArgs.cs create mode 100644 SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs create mode 100644 SHH.CameraSdk/Controllers/MonitorController.cs create mode 100644 SHH.CameraSdk/Core/Features/FrameConsumer.cs create mode 100644 SHH.CameraSdk/Core/Features/SnapshotCoordinator.cs create mode 100644 SHH.CameraSdk/Core/Manager/CameraManager.cs create mode 100644 SHH.CameraSdk/Core/Memory/FramePool.cs create mode 100644 SHH.CameraSdk/Core/Memory/SmartFrame.cs create mode 100644 SHH.CameraSdk/Core/Pipeline/GlobalProcessingCenter.cs create mode 100644 SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs create mode 100644 SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs create mode 100644 SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs create mode 100644 SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs create mode 100644 SHH.CameraSdk/Core/Scheduling/FrameController.cs create mode 100644 SHH.CameraSdk/Core/Scheduling/FrameDecision.cs create mode 100644 SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs create mode 100644 SHH.CameraSdk/Core/Telemetry/CameraHealthReport.cs create mode 100644 SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs create mode 100644 SHH.CameraSdk/Core/Telemetry/FrameConsumerType.cs create mode 100644 SHH.CameraSdk/Core/Telemetry/FrameContext.cs create mode 100644 SHH.CameraSdk/Core/Telemetry/FrameTrace.cs create mode 100644 SHH.CameraSdk/Core/Telemetry/GlobalTelemetry.cs create mode 100644 SHH.CameraSdk/Drivers/BaseVideoSource.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/HikErrorMapper.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/HikExtensions.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs create mode 100644 SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs create mode 100644 SHH.CameraSdk/Program.cs create mode 100644 SHH.CameraSdk/SHH.CameraSdk.csproj diff --git a/SHH.CameraSdk/Abstractions/Enums/DeviceBrand.cs b/SHH.CameraSdk/Abstractions/Enums/DeviceBrand.cs new file mode 100644 index 0000000..4349a21 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Enums/DeviceBrand.cs @@ -0,0 +1,62 @@ +namespace SHH.CameraSdk; + +/// +/// 视频源物理/逻辑品牌类型 +/// 职责:用于工厂模式匹配具体的 IVideoSource 实现类,并定义基础通信协议栈 +/// +public enum DeviceBrand +{ + /// + /// 未知 + /// + Unknown = 0, + + /// + /// 海康威视 (HikVision) + /// 技术路径:基于海康私有 SDK (HCNetSDK.dll / PlayCtrl.dll)。 + /// 特性:支持全功能控制(PTZ、对讲、配置、报警回传)。 + /// + HikVision, + + /// + /// 大华 (Dahua) + /// 技术路径:基于大华私有 SDK (dhnetsdk.dll / dhplay.dll)。 + /// 特性:支持全功能控制,与海康私有协议不兼容。 + /// + Dahua, + + /// + /// USB 摄像头 / 虚拟摄像头 + /// 技术路径:基于 DirectShow 或 Windows Media Foundation。 + /// 特性:通常通过 OpenCV (VideoCapture) 或 DirectShowLib 直接读取本地硬件引用。 + /// + Usb, + + /// + /// 标准 RTSP 流媒体 + /// 技术路径:基于标准 RTSP/RTP 协议 (RFC 2326)。 + /// 特性:跨品牌兼容,通常使用 FFmpeg 或 GStreamer 库取流,仅支持音视频,不支持云台控制。 + /// + RtspGeneral, + + /// + /// 三恒自研 WebSocket 流 + /// 技术路径:基于 WebSocket 传输的自定义二进制或 Base64 帧。 + /// 特性:专用于 Web 或云端推送场景的私有流媒体格式。 + /// + WebSocketShine, + + /// + /// 本地视频文件 + /// 技术路径:基于文件 IO 的离线解码。 + /// 特性:常用于算法演示、回放模拟,支持 Mp4, Avi, Mkv 等容器格式。 + /// + File, + + /// + /// 未知/通用标准 (ONVIF) + /// 技术路径:基于标准 ONVIF WebService。 + /// 特性:用于接入非主流厂商但符合 ONVIF 标准的设备,支持基础 PTZ。 + /// + OnvifGeneral +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs b/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs new file mode 100644 index 0000000..b8ecbe8 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs @@ -0,0 +1,16 @@ +namespace SHH.CameraSdk; + +/// +/// 网络传输协议类型 +/// +public enum TransportProtocol +{ + /// 可靠传输 (默认) + Tcp = 0, + + /// 快速传输 (可能丢包/花屏) + Udp = 1, + + /// 组播 (节省带宽) + Multicast = 2 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Enums/VideoSourceStatus.cs b/SHH.CameraSdk/Abstractions/Enums/VideoSourceStatus.cs new file mode 100644 index 0000000..b50e653 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Enums/VideoSourceStatus.cs @@ -0,0 +1,57 @@ +namespace SHH.CameraSdk; + +/// +/// 视频源逻辑状态枚举 +/// 描述了从配置加载到视频流稳定输出的完整生命周期 +/// +public enum VideoSourceStatus +{ + /// + /// 已断开/初始状态。 + /// 此时资源已释放,尚未执行 Login 或 Start 操作。 + /// + Disconnected, + + /// + /// 正在尝试建立网络连接。 + /// 此时正在进行 Socket 握手或探测设备 IP 是否可达。 + /// + Connecting, + + /// + /// 正在进行身份验证。 + /// 连接已建立,正在提交 UserName/Password 调用 SDK 的 Login 接口。 + /// + Authorizing, + + /// + /// 已登录/待机。 + /// 登录成功并获取到了设备元数据(Metadata),但尚未启动预览(RealPlay)。 + /// 适用于“仅管理,不看画面”的场景。 + /// + Connected, + + /// + /// 正常取流播放中 + /// + Playing, + + /// + /// 正在取流/正常运行中。 + /// 预览句柄已开启,取流回调函数正在持续接收数据帧并进行解码。 + /// + Streaming, + + /// + /// 自动重连中。 + /// 检测到网络抖动或心跳丢失,SDK 正在尝试内部恢复,此时视频流可能处于停滞状态。 + /// + Reconnecting, + + /// + /// 故障/异常状态。 + /// 发生了不可恢复的错误(如密码错误、最大连接数限制、设备强制离线)。 + /// 进入此状态通常需要人工干预或调用 Stop 后重新 Start。 + /// + Faulted +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Errors/CameraErrorCode.cs b/SHH.CameraSdk/Abstractions/Errors/CameraErrorCode.cs new file mode 100644 index 0000000..05de2a9 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Errors/CameraErrorCode.cs @@ -0,0 +1,226 @@ +using System.ComponentModel; + +namespace SHH.CameraSdk; + +/// +/// 工业级相机归一化错误码 (修正全量版) +/// 职责:跨厂家建立统一故障语义,支撑 HikErrorMapper 等驱动层的精准映射。 +/// +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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Errors/CameraException.cs b/SHH.CameraSdk/Abstractions/Errors/CameraException.cs new file mode 100644 index 0000000..318aa17 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Errors/CameraException.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; + +namespace SHH.CameraSdk; + +/// +/// 视频 SDK 统一异常类 (V3.3.1 修复版) +/// 核心职责: +/// 1. 封装标准化错误码、厂商原始错误码、设备品牌信息 +/// 2. 记录异常上下文快照,辅助故障定位与复盘 +/// 协作关系: +/// 1. 与 配合:实现厂商错误码→标准错误码的转换 +/// 2. 与 配合:提供错误码输入,驱动故障自愈决策 +/// +public class CameraException : Exception +{ + #region --- 核心异常属性 (Core Exception Properties) --- + + /// + /// 归一化后的标准错误码 + /// 业务用途:作为 RecoveryPolicy 的决策输入,屏蔽厂商差异 + /// + public CameraErrorCode ErrorCode { get; } + + /// + /// 厂商原始错误码(如海康 NET_DVR_GetLastError、大华 SDK 原生错误码) + /// 业务用途:厂商文档对照、深度问题排查 + /// + public int RawErrorCode { get; } + + /// + /// 发生异常的设备品牌 + /// 业务用途:区分不同厂商的错误码规则,辅助错误映射 + /// + public DeviceBrand Brand { get; } + + /// + /// 异常发生时的上下文快照(只读集合,防止外部篡改) + /// 存储内容:设备IP、通道号、操作参数、SDK句柄、时间戳等案发现场信息 + /// 业务用途:故障复盘时还原现场,快速定位根因 + /// + public IReadOnlyDictionary Context { get; init; } = new Dictionary(); + + #endregion + + #region --- 构造函数 (Constructors) --- + + /// + /// 初始化 CameraException 实例 + /// + /// 归一化标准错误码 + /// 异常描述信息 + /// 设备品牌 + /// 厂商原始错误码(默认 0) + /// 内部异常(默认 null) + 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(); + } + + #endregion + + #region --- 工具方法 (Utility Methods) --- + + /// + /// 链式添加上下文信息(Builder 模式) + /// 业务用途:在抛出异常前,逐步追加案发现场信息 + /// + /// 上下文键(如 "DeviceIp", "ChannelIndex") + /// 上下文值 + /// 当前异常实例(支持链式调用) + public CameraException WithContext(string key, object value) + { + // 强制转换为可写的 Dictionary,保证上下文可追加 + if (Context is Dictionary contextDict) + { + contextDict[key] = value; + } + return this; + } + + /// + /// 重写 ToString 方法,输出标准化异常日志 + /// 格式:[CameraError] Brand: {品牌} | Code: {标准码}({原始码}) | Message: {描述} | Context: {上下文} + /// + /// 格式化的异常字符串 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Errors/RecoveryAction.cs b/SHH.CameraSdk/Abstractions/Errors/RecoveryAction.cs new file mode 100644 index 0000000..94159aa --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Errors/RecoveryAction.cs @@ -0,0 +1,55 @@ +namespace SHH.CameraSdk; + +/// +/// 故障恢复决策建议枚举 +/// 核心职责:定义标准化的故障自愈动作指令,指导 执行差异化恢复逻辑 +/// 设计原则:按“无动作→自动恢复→降级→致命停止→人工介入”的优先级划分,覆盖全场景故障处理 +/// +public enum RecoveryAction +{ + #region --- 0. 基础状态 --- + + /// + /// 正常状态,无需执行任何恢复动作 + /// 适用场景:错误码为 Success、设备运行正常 + /// + None, + + #endregion + + #region --- 1. 自动恢复动作 --- + + /// + /// 自动指数退避重试 + /// 适用场景:网络抖动、超时、设备资源繁忙等**暂时性故障** + /// 执行标准:采用 2^n * 1000ms 算法计算延迟,上限 2 分钟,避免频繁重试加剧系统负载 + /// + RetryWithBackoff, + + /// + /// 降级运行 + /// 适用场景:主码流超限、高清分辨率不支持等**非致命功能降级场景** + /// 执行标准:自动切换到备用方案(如主码流→子码流、4K→1080P),保证基础功能可用 + /// + Degrade, + + #endregion + + #region --- 2. 终止与人工动作 --- + + /// + /// 致命停止,禁止继续重试 + /// 适用场景:密码错误、账号锁定、IP 拉黑等**不可自愈的认证/权限类故障** + /// 执行标准:立即停止自愈引擎,推送告警信息到运维平台,记录详细错误日志 + /// + FatalStop, + + /// + /// 需要人工介入处理 + /// 适用场景:硬件故障、磁盘满、SDK 组件缺失等**软件无法修复的底层故障** + /// 执行标准:触发告警通知,标记设备状态为 Faulted,等待运维人员排查 + /// + ManualIntervention + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Errors/RecoveryPolicy.cs b/SHH.CameraSdk/Abstractions/Errors/RecoveryPolicy.cs new file mode 100644 index 0000000..5b2f573 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Errors/RecoveryPolicy.cs @@ -0,0 +1,100 @@ +namespace SHH.CameraSdk; + +/// +/// [决策引擎] 故障自愈策略 (V3.3.1 修复版) +/// 核心职责:根据设备错误码特征,智能裁决系统应采取的恢复动作,实现故障自动化处理 +/// 关键修复(Bug R): +/// 1. 致命错误防护:对 InvalidCredentials/AccountLocked 等错误禁止重试,防止账号被锁、IP 拉黑 +/// 2. 未知错误保守策略:对 Unknown 错误采用 ManualIntervention,避免未知风险扩散 +/// 设计原则:最小化风险、最大化自愈率,区分可重试/不可重试/需人工干预的错误类型 +/// +public static class RecoveryPolicy +{ + #region --- 1. 核心决策逻辑:错误码→自愈动作映射 --- + + /// + /// 根据相机错误码判定对应的故障自愈动作 + /// + /// 设备上报的错误码 + /// 标准化的自愈动作指令 + 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. 辅助算法:指数退避延迟计算 --- + + /// + /// 获取建议的指数退避延迟时间(毫秒) + /// 算法公式:delay = min(2^n * 1000, 120000),n = 当前重试次数 + /// 限流规则:第一次 2s → 第二次 4s → ... → 第六次 64s → 上限 120s(2分钟) + /// + /// 当前重试次数(从 1 开始计数) + /// 延迟毫秒数 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/IVideoSource.cs b/SHH.CameraSdk/Abstractions/IVideoSource.cs new file mode 100644 index 0000000..39b26c8 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/IVideoSource.cs @@ -0,0 +1,91 @@ +namespace SHH.CameraSdk; + +/// +/// [核心契约] 工业级视频源接口 (V3.3.1 终极定稿) +/// 核心职责:定义所有视频源设备的标准化生命周期、状态观测与数据分发能力 +/// 关键修复: +/// 1. [Fix Bug δ] 新增 UpdateConfig 接口,支持运行时配置热更新 +/// 2. 强化资源管理契约:继承 IDisposable/IAsyncDisposable,规范非托管资源释放 +/// 适用场景:海康/大华/宇视等不同品牌相机的驱动适配、统一管理 +/// +public interface IVideoSource : IDisposable, IAsyncDisposable +{ + #region --- 1. 只读属性 (设备标识与状态观测) --- + + /// 设备唯一业务标识(全局唯一,如数据库自增ID) + long Id { get; } + + /// 设备详细逻辑状态(如 Idle/Connecting/Playing/Faulted) + VideoSourceStatus Status { get; } + + /// 用户意图标识:是否需要保持设备运行状态 + bool IsRunning { get; set; } + + /// 设备物理在线状态(基于心跳/探测的实时感知结果) + bool IsOnline { get; } + + /// 设备能力元数据(只读,如分辨率、码流类型、支持的功能集) + DeviceMetadata Metadata { get; } + + #endregion + + #region --- 2. 事件契约 (数据分发与状态通知) --- + + /// + /// 视频帧接收事件(热路径,高频触发) + /// + /// + /// 1. 载荷类型:通常为 对象 + /// 2. 内存管理:订阅者必须负责载荷对象的 Dispose 操作,否则会导致内存泄漏 + /// 3. 性能约束:事件处理逻辑需控制在 10ms 内,避免阻塞取流线程 + /// + event Action? FrameReceived; + + /// + /// 设备状态变更通知事件(结构化状态同步) + /// + /// 携带状态变更前后的详细信息,用于监控告警、日志记录 + event EventHandler StatusChanged; + + #endregion + + #region --- 3. 核心方法 (生命周期与配置管理) --- + + /// + /// 异步启动设备(完整流程:连接设备 → 登录鉴权 → 启动码流接收) + /// + /// 设备状态非法时抛出 + /// SDK 通信失败时抛出 + Task StartAsync(); + + /// + /// 异步停止设备(完整流程:停止码流 → 登出设备 → 释放连接资源) + /// + Task StopAsync(); + + /// + /// [Fix Bug δ] 运行时更新设备配置 + /// + /// 新的设备配置(如 IP、端口、用户名密码) + /// + /// 1. 生效机制:新配置不会立即生效,将在下次启动或自动重连时应用 + /// 2. 原子性保证:配置更新为原子操作,不会出现部分生效的情况 + /// 3. 适用场景:设备 IP 变更、密码修改等运维场景 + /// + void UpdateConfig(VideoSourceConfig newConfig); + + /// + /// 应用动态流配置补丁(无需重启,实时生效) + /// + /// 动态流参数(如主码流/子码流切换、分辨率调整) + /// 适用于运行时按需调整码流参数,降低带宽占用 + void ApplyOptions(DynamicStreamOptions options); + + /// + /// 强制刷新设备元数据,并返回元数据变更差异 + /// + /// 元数据变更差异对象(如分辨率变化、功能集变化) + Task RefreshMetadataAsync(); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/ChannelMetadata.cs b/SHH.CameraSdk/Abstractions/Models/ChannelMetadata.cs new file mode 100644 index 0000000..bf6e3b8 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/ChannelMetadata.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace SHH.CameraSdk; + +/// +/// 通道级能力描述(镜头身份证) +/// 核心职责:描述单个物理镜头或 NVR 通道的技术参数、功能支持特性与分辨率能力 +/// 协作场景:作为 的子级数据,支撑设备能力自发现、配置合法性校验 +/// +public class ChannelMetadata +{ + #region --- 1. 通道基础标识 (Basic Identification) --- + + /// + /// 物理通道索引(从 1 开始计数,与 DVR/NVR 物理通道号一一对应) + /// 业务用途:作为通道唯一标识,用于码流订阅、参数配置 + /// + public int ChannelIndex { get; init; } + + /// + /// 通道名称(用于 OSD 叠加显示、UI 界面展示) + /// 示例:"北大门主入口" "地下车库A区" + /// + public string Name { get; init; } = string.Empty; + + #endregion + + #region --- 2. 功能支持特性 (Capability Support) --- + + /// + /// 是否支持云台控制(PTZ:Pan/Tilt/Zoom 平移/俯仰/变焦) + /// 业务影响:决定 UI 是否显示云台控制按钮,是否允许下发 PTZ 指令 + /// + public bool SupportPtz { get; init; } + + /// + /// 是否支持音频输入(是否接入拾音器) + /// 业务影响:决定是否开启音频解码、音频流推送功能 + /// + public bool SupportAudioIn { get; init; } + + /// + /// 是否支持 AI 智能分析(人脸检测、车牌识别、行为分析等) + /// 业务影响:决定是否加载 AI 算法插件,是否接收智能事件上报 + /// + public bool SupportAiAnalysis { get; init; } + + #endregion + + #region --- 3. 分辨率能力 (Resolution Capabilities) --- + + /// + /// 通道支持的分辨率列表(只读集合,防止外部篡改) + /// 格式示例:["1920x1080", "1280x720", "3840x2160"] + /// 业务用途:前端清晰度选择下拉列表、动态配置分辨率合法性校验 + /// + public ReadOnlyCollection SupportedResolutions { get; init; } = new ReadOnlyCollection(new List()); + + #endregion + + #region --- 4. 构造函数 (Constructors) --- + + /// + /// 默认构造函数(用于序列化、初始状态初始化) + /// + public ChannelMetadata() { } + + /// + /// 简化构造函数(用于快速创建通道标识、克隆或比对场景) + /// + /// 物理通道索引 + /// 通道名称 + public ChannelMetadata(int index, string name) + { + ChannelIndex = index; + Name = name; + } + + /// + /// 完整构造函数(用于创建包含全量能力的通道元数据) + /// + /// 物理通道索引 + /// 通道名称 + /// 是否支持云台 + /// 是否支持音频输入 + /// 是否支持 AI 分析 + /// 支持的分辨率列表 + public ChannelMetadata( + int index, + string name, + bool supportPtz = false, + bool supportAudio = false, + bool supportAi = false, + IEnumerable? resolutions = null) + { + ChannelIndex = index; + Name = name; + SupportPtz = supportPtz; + SupportAudioIn = supportAudio; + SupportAiAnalysis = supportAi; + SupportedResolutions = new ReadOnlyCollection(resolutions?.ToList() ?? new List()); + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/DeviceMetadata.cs b/SHH.CameraSdk/Abstractions/Models/DeviceMetadata.cs new file mode 100644 index 0000000..863311c --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/DeviceMetadata.cs @@ -0,0 +1,176 @@ +namespace SHH.CameraSdk; + +/// +/// 增强型设备元数据中心 (V3.3.1 修复版) +/// 核心职责: +/// 1. 封装设备的硬件参数、通道能力、功能集,提供能力自发现 +/// 2. 支持元数据同步与差异对比,指导上层模块执行差异化处理 +/// 3. 存储运维指标与 SDK 原生句柄,支撑故障诊断与性能调优 +/// 设计特性:只读优先,通过版本号标记同步状态,避免并发修改冲突 +/// +public class DeviceMetadata +{ + #region --- 1. 设备级身份信息 (Identity) --- + + /// 设备型号名称(如 DS-2CD3T47G2-LIU) + public string ModelName { get; init; } = "Unknown"; + + /// 设备唯一序列号(全局唯一,用于设备溯源) + public string SerialNumber { get; init; } = string.Empty; + + /// 固件/系统版本号(用于判断 SDK 兼容性) + public string FirmwareVersion { get; init; } = string.Empty; + + /// 所属厂商/品牌(决定驱动适配逻辑) + public DeviceBrand Brand { get; init; } = DeviceBrand.Unknown; + + /// 元数据版本号(本地刷新计数,每次同步自增) + public long Version { get; private set; } + + /// 最后同步时间(标记元数据的最新有效时刻) + public DateTime LastSyncedAt { get; private set; } + + #endregion + + #region --- 2. 级联能力模型 (Cascaded Capabilities) --- + + private readonly ReadOnlyCollection _channels; + + /// 通道元数据集合(只读,防止外部篡改) + public ReadOnlyCollection Channels + { + get => _channels; + init => _channels = value ?? new ReadOnlyCollection(new List()); + } + + /// 设备总通道数量(IPC 通常为 1,NVR 为接入路数) + public int ChannelCount => Channels.Count; + + /// 索引器:通过通道号快速获取对应通道的能力描述 + /// 物理通道号 + /// 通道元数据 / 不存在则返回 null + public ChannelMetadata? this[int channelIndex] => + Channels.FirstOrDefault(c => c.ChannelIndex == channelIndex); + + #endregion + + #region --- 3. 运维指标与非托管句柄 --- + + /// 设备实时健康度字典(如 CPU 使用率、温度、内存占用等) + public Dictionary HealthMetrics { get; init; } = new(); + + /// + /// 厂商 SDK 原始句柄/结构体快照 + /// 注意事项: + /// 1. 标记 [JsonIgnore] 防止序列化非托管指针导致程序崩溃 + /// 2. 仅用于驱动层与 SDK 交互,上层业务禁止直接操作 + /// + [JsonIgnore] + public object? NativeHandle { get; init; } + + /// 厂商扩展标签(如设备位置、安装时间、责任人等自定义信息) + public Dictionary Tags { get; init; } = new(); + + #endregion + + #region --- 4. 构造与同步 (Constructor & Sync) --- + + /// + /// 默认构造函数(用于 BaseVideoSource 初始状态,无通道数据) + /// + public DeviceMetadata() : this(Enumerable.Empty()) { } + + /// + /// 完整构造函数(初始化通道元数据集合) + /// + /// 通道元数据列表 + public DeviceMetadata(IEnumerable channels) + { + // 转换为只读集合,确保通道数据不可变 + _channels = new ReadOnlyCollection(channels?.ToList() ?? new List()); + // 标记初始同步状态 + MarkSynced(); + } + + /// + /// 标记元数据同步完成 + /// 作用:更新版本号与同步时间,用于差异对比的基准判断 + /// + public void MarkSynced() + { + Version++; + LastSyncedAt = DateTime.Now; + } + + #endregion + + #region --- 5. 业务逻辑辅助方法 (Business Helpers) --- + + /// + /// 校验动态流配置的合法性(基于设备能力) + /// + /// 待校验的动态配置项 + /// 校验失败时的详细原因 + /// 合法返回 true,非法返回 false + 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; + } + + /// + /// 元数据差异比对逻辑(用于 BaseVideoSource.RefreshMetadataAsync 方法) + /// + /// 最新拉取的设备元数据 + /// 元数据差异描述符 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs b/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs new file mode 100644 index 0000000..155beec --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs @@ -0,0 +1,120 @@ +namespace SHH.CameraSdk; + +/// +/// 视频流动态配置项(运行时可调整参数容器) +/// 核心职责:承载无需重启流即可动态调整的视频参数,支持局部更新 +/// 核心特性: +/// 1. Nullable 模式:仅非空字段会触发参数更新,避免全量重置导致的性能抖动 +/// 2. 分类管理:按画面策略、帧率控制、传输输出、厂商扩展划分参数,逻辑清晰 +/// 3. 空值检查:通过 IsEmpty 判断是否存在有效配置,避免无效底层 SDK 调用 +/// +public class DynamicStreamOptions +{ + #region --- 1. 画面策略 (Resolution & Scaling) --- + + /// + /// 目标输出宽度(像素) + /// Nullable 规则:null = 保持当前配置;非 null = 触发图像缩放逻辑 + /// 注意事项:建议与 TargetHeight 成对设置,避免画面比例失衡 + /// + public int? TargetWidth { get; set; } + + /// + /// 目标输出高度(像素) + /// Nullable 规则:null = 保持当前配置;非 null = 触发图像缩放逻辑 + /// 协作关系:与 TargetWidth 配合使用,若仅设置其一,会按原始宽高比自动计算另一值 + /// + public int? TargetHeight { get; set; } + + /// + /// 图像放大开关 + /// Nullable 规则:null = 保持当前策略;true = 允许放大;false = 禁止放大 + /// 性能影响:禁止放大可节省插值计算资源,适合低性能设备 + /// + public bool? AllowEnlarge { get; set; } + + /// + /// 图像缩小开关 + /// Nullable 规则:null = 保持当前策略;true = 允许缩小;false = 禁止缩小 + /// 适用场景:禁止缩小可保留原始画质,适合需要高清分析的场景 + /// + public bool? AllowShrink { get; set; } + + #endregion + + #region --- 2. 频率控制 (Frame Rate Control) --- + + /// + /// 目标渲染/显示帧率(fps) + /// Nullable 规则:null = 不修改;0 = 跟随原始流速度;非 0 = 强制限定显示帧率 + /// 作用域:仅影响 UI 预览层,不会改变底层码流的采集帧率 + /// + public int? TargetDisplayFps { get; set; } + + /// + /// 目标 AI 分析帧率(fps) + /// Nullable 规则:null = 不修改;非 null = 限定算法处理的输入帧率 + /// 性能优化:降低此值可显著减少高分辨率下的 GPU/CPU 负荷(如 4K 从 30fps 降到 5fps) + /// + public int? TargetAnalyzeFps { get; set; } + + #endregion + + #region --- 3. 传输与输出 (Transmission & Output) --- + + /// + /// Web 推流开关 + /// Nullable 规则:null = 保持当前状态;true = 启动推流;false = 停止推流 + /// 协作组件:开启后会将处理后的视频帧推送到流媒体服务器(如 FFmpeg/RTSP 服务器) + /// + public bool? EnableStreamOutput { get; set; } + + /// + /// 渲染窗体句柄 + /// Nullable 规则:null = 保持当前窗口;非 null = 切换到新窗口渲染 + /// 适用场景:支持视频窗口拖拽、多显示器切换等交互操作 + /// + public IntPtr? RenderHandle { get; set; } + + /// + /// 码流类型切换 + /// 取值规则:0 = 主码流(高清/大带宽);1 = 子码流(标清/低延迟);2 = 第三码流 + /// Nullable 规则:null = 不切换;非 null = 执行码流切换 + /// 注意事项:切换会销毁并重建预览句柄,可能导致短暂的画面中断 + /// + public int? StreamType { get; set; } + + #endregion + + #region --- 4. 厂商扩展 (Vendor Specific) --- + + /// + /// 厂商特有参数扩展字典 + /// 用途:存放无法标准化的品牌专属功能参数 + /// 示例:海康 "FocusMode"=Auto/Manual;大华 "SmartH264"=true/false + /// 注意事项:键值对需与对应厂商 SDK 的参数名一致,否则无效 + /// + public Dictionary VendorExtensions { get; set; } = new(); + + #endregion + + #region --- 5. 配置有效性检查 --- + + /// + /// 逻辑空检查:判断当前配置包是否包含任何有效修改项 + /// 使用场景:调用 SDK 前判断,避免无意义的底层调用,提升性能 + /// + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/MetadataDiff.cs b/SHH.CameraSdk/Abstractions/Models/MetadataDiff.cs new file mode 100644 index 0000000..2fa475f --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/MetadataDiff.cs @@ -0,0 +1,53 @@ +namespace SHH.CameraSdk; + +/// +/// 元数据变更差异描述符(只读结构体) +/// 核心职责:对比设备当前运行元数据与最新拉取元数据的差异,明确变更类型及业务影响 +/// 协作场景:指导上层模块执行差异化处理(如仅刷新UI、重启流、调整功能按钮) +/// +public readonly struct MetadataDiff +{ + #region --- 差异类型属性 (Change Type Properties) --- + + /// + /// 设备基础描述信息变更(如名称、位置) + /// 业务影响:仅需刷新 UI 显示文字,无需中断当前流 + /// + public bool NameChanged { get; init; } + + /// + /// 设备能力集变更(如新增/移除对讲、截图功能) + /// 业务影响:需调整 UI 功能按钮的可用性,无需中断流 + /// + public bool CapabilityChanged { get; init; } + + /// + /// 致命/破坏性变更(如设备型号、编码格式变更) + /// 业务影响:必须销毁当前流实例并重启,否则会导致流异常或崩溃 + /// + public bool IsMajorChange { get; init; } + + /// + /// 分辨率/帧率档位列表变更 + /// 业务影响:需重新校验当前流参数是否合法,非法则自动降级到可用档位 + /// + public bool ResolutionProfilesChanged { get; init; } + + /// + /// 全局变更标识:是否存在任何类型的元数据变更 + /// 业务用途:快速判断是否需要执行后续差异化处理逻辑 + /// + public bool HasChanges => NameChanged || CapabilityChanged || IsMajorChange || ResolutionProfilesChanged; + + #endregion + + #region --- 快捷实例 (Quick Instances) --- + + /// + /// 无变更状态的快捷实例 + /// 业务用途:元数据刷新后无变化时直接返回,避免重复创建空对象 + /// + public static MetadataDiff None => new(); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/ResolutionProfile.cs b/SHH.CameraSdk/Abstractions/Models/ResolutionProfile.cs new file mode 100644 index 0000000..fcc8c35 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/ResolutionProfile.cs @@ -0,0 +1,57 @@ +namespace SHH.CameraSdk; + +/// +/// 视频分辨率档位描述符(Record 类型,不可变对象) +/// 核心职责:定义相机通道在特定编码格式下支持的分辨率、帧率上限及友好描述 +/// 协作场景: +/// 1. 前端界面:展示清晰度选择下拉列表 +/// 2. 配置校验:下发分辨率前判断是否符合硬件能力,防止超限黑屏 +/// 3. 性能评估:通过总像素量计算编码/解码的计算负载与带宽压力 +/// +/// 画面像素宽度(如 1920、3840) +/// 画面像素高度(如 1080、2160) +/// 该分辨率下硬件支持的最大输出帧率(防止超限配置) +/// 档位友好描述(如 "高清(1080P/H.265)") +public record ResolutionProfile( + int Width, + int Height, + int MaxFps, + string Description +) +{ + #region --- 计算属性 (Derived Properties) --- + + /// + /// 当前档位的总像素量 + /// 业务用途:衡量编码/解码的计算负载、网络传输的带宽压力 + /// 计算公式:Width * Height + /// + public long TotalPixels => (long)Width * Height; + + /// + /// 当前档位的屏幕宽高比 + /// 业务用途:前端渲染容器(WinForm/WPF/Web)自动调整比例,避免画面拉伸变形 + /// 保护逻辑:高度为 0 时返回 0,防止除零异常 + /// + public double AspectRatio => Height == 0 ? 0 : (double)Width / Height; + + /// + /// 判断当前档位是否属于高清范畴(行业标准:720P 及以上,即 1280x720 分辨率) + /// 业务用途:前端分类展示、带宽策略选择 + /// + public bool IsHighDefinition => Width >= 1280 && Height >= 720; + + #endregion + + #region --- 重写方法 (Overridden Methods) --- + + /// + /// 格式化分辨率档位的显示文本 + /// 输出格式:友好描述 (宽x高@最大帧率fps) + /// 示例:高清(1080P/H.265) (1920x1080@30fps) + /// + /// 格式化的显示字符串 + public override string ToString() => $"{Description} ({Width}x{Height}@{MaxFps}fps)"; + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/StatusChangedEventArgs.cs b/SHH.CameraSdk/Abstractions/Models/StatusChangedEventArgs.cs new file mode 100644 index 0000000..e9554f4 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/StatusChangedEventArgs.cs @@ -0,0 +1,80 @@ +namespace SHH.CameraSdk; + +/// +/// 视频源状态变更事件参数 +/// 核心职责:封装状态迁移的完整上下文信息,支撑三大业务场景 +/// 1. UI 层:实时反馈设备状态、显示错误提示 +/// 2. 诊断层:记录异常堆栈、SDK 错误码,辅助问题定位 +/// 3. 运维层:触发自动重连、告警推送等自动化决策 +/// +public class StatusChangedEventArgs : EventArgs +{ + #region --- 事件核心属性 (Event Core Properties) --- + + /// + /// 变更后的目标状态 + /// 业务用途: + /// 1. UI 层:控制状态图标颜色(如 Playing→绿色、Faulted→红色) + /// 2. 运维层:状态为 Reconnecting 时触发重连策略调整 + /// + public VideoSourceStatus NewStatus { get; } + + /// + /// 状态描述文本(可读) + /// 业务用途: + /// 1. UI 层:直接显示在状态栏或操作日志面板 + /// 2. 日志层:写入业务日志,便于人工排查 + /// + public string Message { get; } + + /// + /// 关联的异常对象(可选) + /// 业务用途:仅当 NewStatus = Faulted 时有效,提供异常堆栈、类型等代码级诊断信息 + /// + public Exception? Exception { get; } + + /// + /// SDK 底层原始错误码(可选) + /// 业务用途: + /// 1. 厂商适配:匹配海康 NET_DVR_GetLastError、大华 SDK 错误码等 + /// 2. 精准诊断:区分“用户锁定(153)”“密码错误(41)”“网络超时”等根因 + /// + public int? LastErrorCode { get; } + + /// + /// 变更后的新句柄(可选) + /// 业务用途:渲染器解绑/重绑场景,监听此值更新窗口句柄绑定关系 + /// + public IntPtr? NewHandle { get; init; } + + /// + /// 状态变更发生的时间戳 + /// 业务用途:日志时序排序、状态迁移耗时统计 + /// + public DateTime Timestamp { get; } = DateTime.Now; + + #endregion + + #region --- 构造函数 (Constructor) --- + + /// + /// 初始化状态变更事件参数 + /// + /// 变更后的目标状态 + /// 可读的状态描述文本 + /// 可选:关联的异常对象 + /// 可选:SDK 底层错误码 + public StatusChangedEventArgs( + VideoSourceStatus status, + string msg, + Exception? ex = null, + int? errorCode = null) + { + NewStatus = status; + Message = msg; + Exception = ex; + LastErrorCode = errorCode; + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs new file mode 100644 index 0000000..4e9eee9 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs @@ -0,0 +1,150 @@ +namespace SHH.CameraSdk; + +/// +/// 视频源基础配置对象 (V3.3.1 修复版) +/// 核心职责:定义建立相机物理连接所需的所有标准化参数与厂商扩展参数 +/// 关键修复: +/// 1. [Fix Bug U: 配置漂移] 驱动层接收配置时需创建副本,防止外部修改导致的连接异常 +/// 2. 强化配置有效性校验,提前拦截非法参数 +/// 注意事项:此类为引用类型,传递时建议使用深拷贝 +/// +public class VideoSourceConfig +{ + #region --- 1. 核心连接配置 (Core Connection Configurations) --- + + /// 业务系统唯一标识(对应数据库自增ID,全局唯一) + public long Id { get; set; } + + /// 设备显示名称(如:北大门-枪机01,用于前端展示) + public string Name { get; set; } = string.Empty; + + /// 设备品牌(决定加载对应的驱动实现类) + [JsonConverter(typeof(JsonStringEnumConverter))] + public DeviceBrand Brand { get; set; } = DeviceBrand.Unknown; + + /// 设备 IP 地址或域名(如 192.168.1.100 或 camera.example.com) + public string IpAddress { get; set; } = string.Empty; + + /// 设备端口号(厂商默认值:海康8000、大华37777、RTSP 554) + public ushort Port { get; set; } + + /// 设备登录用户名(默认值:admin) + public string Username { get; set; } = "admin"; + + /// 设备登录密码(默认空字符串,需根据实际设备配置) + public string Password { get; set; } = string.Empty; + + /// 渲染句柄(可选):用于硬解码时直接绑定显示窗口,提升渲染性能 + public IntPtr RenderHandle { get; set; } = IntPtr.Zero; + + /// 物理通道号(IPC 通常为 1;NVR 对应接入的摄像头通道索引) + public int ChannelIndex { get; set; } = 1; + + /// 默认码流类型(0 = 主码流(高清),1 = 子码流(低带宽)) + public int StreamType { get; set; } = 0; + + /// 传输协议(TCP/UDP/Multicast,默认 TCP 保证可靠性) + [JsonConverter(typeof(JsonStringEnumConverter))] + public TransportProtocol Transport { get; set; } = TransportProtocol.Tcp; + + /// 连接超时时间(毫秒,默认 5000ms = 5秒) + public int ConnectionTimeoutMs { get; set; } = 5000; + + #endregion + + #region --- 2. 厂商扩展配置 (Vendor-Specific Extensions) --- + + /// + /// 厂商扩展参数字典 + /// 用途:存储无法标准化的品牌专属参数 + /// 示例: + /// + /// { + /// "RtspPath": "/h264/ch1/main/av_stream", + /// "HikLoginMode": "ISAPI", + /// "DaHuaStreamProtocol": "HTTP" + /// } + /// + /// + public Dictionary VendorArguments { get; set; } = new(); + + #endregion + + #region --- 3. 配置工具方法 (Configuration Utility Methods) --- + + /// + /// 配置有效性校验:检查核心参数是否合法,非法则抛出异常 + /// 作用:提前拦截无效配置,避免驱动层连接时出现未知错误 + /// + /// 核心参数非法时抛出 + 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)); + } + + /// + /// 生成设备唯一连接指纹 + /// 用途:用于连接池去重、缓存键、日志标识等场景 + /// 格式:{Brand}://{Username}@{IpAddress}:{Port}/{ChannelIndex} + /// + /// 唯一连接指纹字符串 + public string GetConnectionKey() + { + // 使用 StringBuilder 提升拼接性能,避免频繁创建字符串 + return new StringBuilder() + .Append(Brand) + .Append("://") + .Append(Username) + .Append('@') + .Append(IpAddress) + .Append(':') + .Append(Port) + .Append('/') + .Append(ChannelIndex) + .ToString(); + } + + /// + /// 创建配置对象的深拷贝(防止外部修改导致配置漂移) + /// + /// 新的配置副本 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs new file mode 100644 index 0000000..2d5420a --- /dev/null +++ b/SHH.CameraSdk/Controllers/MonitorController.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace SHH.CameraSdk; + +/// +/// 视频源实时状态监控 API 控制器 +/// 核心功能:提供相机设备遥测数据查询、单设备详情查询、设备截图获取接口 +/// 适用场景:Web 监控大屏、移动端状态查询、第三方系统集成 +/// +[ApiController] +[Route("api/[controller]")] +public class MonitorController : ControllerBase +{ + #region --- 依赖注入 (Dependency Injection) --- + + /// 相机管理器实例:提供设备状态与遥测数据访问能力 + private readonly CameraManager _cameraManager; + + /// + /// 构造函数:通过依赖注入获取 CameraManager 实例 + /// + /// 相机管理器 + public MonitorController(CameraManager cameraManager) + { + _cameraManager = cameraManager; + } + + #endregion + + #region --- API 接口定义 (API Endpoints) --- + + /// + /// 获取全量相机实时遥测数据快照(支持跨域) + /// + /// + /// 返回数据包含:设备ID、名称、IP地址、运行状态、在线状态、实时FPS、累计帧数、健康度评分、最后错误信息 + /// 适用场景:监控大屏首页数据看板 + /// [cite: 191, 194] + /// + /// 200 OK + 遥测数据列表 + [HttpGet("dashboard")] + public IActionResult GetDashboard() + { + var telemetrySnapshot = _cameraManager.GetTelemetrySnapshot(); + return Ok(telemetrySnapshot); + } + + /// + /// 获取指定相机的详细运行指标 + /// + /// 相机设备唯一标识 + /// 200 OK + 设备详情 | 404 Not Found + [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); + } + + /// + /// 获取指定相机的实时截图 + /// + /// 相机设备唯一标识 + /// 200 OK + JPEG 图片流 | 504 Gateway Timeout + [HttpGet("snapshot/{id}")] + public async Task 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Features/FrameConsumer.cs b/SHH.CameraSdk/Core/Features/FrameConsumer.cs new file mode 100644 index 0000000..36509e7 --- /dev/null +++ b/SHH.CameraSdk/Core/Features/FrameConsumer.cs @@ -0,0 +1,171 @@ +using OpenCvSharp; + +namespace SHH.CameraSdk; + +/// +/// [消费者] 专用渲染线程(零延迟设计) +/// 核心策略: +/// 1. 容量为1的阻塞队列:仅保留最新一帧,杜绝帧堆积 +/// 2. 非阻塞入队+主动丢帧:渲染慢时直接丢弃新帧,确保主线程不阻塞 +/// 3. 引用计数联动:丢帧时立即释放引用,内存自动回池复用 +/// +public class FrameConsumer : IDisposable +{ + #region --- 私有资源与状态 (Private Resources & States) --- + + /// 帧缓冲队列(容量1):仅存储最新一帧,保证零延迟渲染 + /// BlockingCollection 封装线程安全操作,GetConsumingEnumerable 支持取消令牌 + private readonly BlockingCollection _frameBuffer = new(1); + + /// 取消令牌源:用于终止渲染循环 + private readonly CancellationTokenSource _cts = new(); + + /// 后台渲染任务 + private Task? _renderTask; + + /// OpenCV 窗口名称 + private readonly string _windowName; + + #endregion + + #region --- 构造与生命周期 (Constructor & Lifecycle) --- + + /// + /// 初始化帧渲染消费者 + /// + /// OpenCV 显示窗口名称 + public FrameConsumer(string windowName = "Zero Latency Preview") + { + _windowName = windowName; + } + + /// + /// 启动渲染线程 + /// + public void Start() + { + // 防止重复启动 + if (_renderTask != null) return; + + // 启动长期运行的渲染任务,提升线程调度优先级 + _renderTask = Task.Factory.StartNew(RenderLoop, TaskCreationOptions.LongRunning); + Console.WriteLine($"[Consumer] 渲染线程启动成功,窗口名称: {_windowName}"); + } + + /// + /// 停止渲染线程并清理资源 + /// + 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) --- + + /// + /// [生产端入口] 接收帧并尝试入队(非阻塞) + /// + /// 待渲染的智能帧 + public void Enqueue(SmartFrame frame) + { + // 防护:线程已停止,直接释放帧引用 + if (_cts.IsCancellationRequested) + { + frame.Dispose(); + return; + } + + // 核心零延迟策略:非阻塞尝试入队 + // 队列满 → 上一帧未渲染完成 → 丢弃当前帧,释放引用 + if (!_frameBuffer.TryAdd(frame)) + { + frame.Dispose(); + // Debug.WriteLine($"[Drop] 渲染线程[{_windowName}]处理过慢,丢弃一帧"); + } + // 入队成功 → 帧由队列托管,等待渲染线程消费 + } + + /// + /// 后台渲染循环(核心逻辑) + /// + 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) --- + + /// + /// 释放所有资源 + /// + public void Dispose() + { + Stop(); + _frameBuffer.Dispose(); + _cts.Dispose(); + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Features/SnapshotCoordinator.cs b/SHH.CameraSdk/Core/Features/SnapshotCoordinator.cs new file mode 100644 index 0000000..4ab3f05 --- /dev/null +++ b/SHH.CameraSdk/Core/Features/SnapshotCoordinator.cs @@ -0,0 +1,92 @@ +namespace SHH.CameraSdk; + +/// +/// 截图协调器(单例模式) +/// 功能:桥接 API 线程的截图请求与 SDK 线程的帧数据推送,实现异步截图功能 +/// 核心机制:基于 TaskCompletionSource 实现跨线程通信,支持超时控制与自动清理 +/// 线程安全:全量使用并发容器,无锁设计,支持多设备同时请求截图 +/// +public class SnapshotCoordinator +{ + #region --- 1. 单例实现 (Singleton Implementation) --- + + /// 全局唯一实例 + public static SnapshotCoordinator Instance { get; } = new(); + + /// 私有构造函数:禁止外部实例化,确保单例特性 + private SnapshotCoordinator() { } + + #endregion + + #region --- 2. 私有任务池 (Private Task Pool) --- + + /// 待处理截图请求任务池(线程安全) + /// Key = 设备ID,Value = 任务完成源,用于传递截图结果或超时信号 + private readonly ConcurrentDictionary> _pendingRequests = new(); + + #endregion + + #region --- 3. API 线程交互接口 (API Thread Interface) --- + + /// + /// API 线程调用:申请指定设备的截图并异步等待结果 + /// + /// 目标设备ID + /// 超时时间(默认 3000ms) + /// 截图字节数组(JPEG/PNG 格式)/ 超时返回 null + public async Task RequestSnapshotAsync(long deviceId, int timeoutMs = 3000) + { + // 1. 创建任务完成源,用于接收 SDK 线程的截图数据 + var tcs = new TaskCompletionSource(); + // 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) --- + + /// + /// SDK 线程调用:检查指定设备是否存在待处理的截图请求 + /// + /// 目标设备ID + /// 存在待处理请求返回 true,否则返回 false + public bool HasRequest(long deviceId) => _pendingRequests.ContainsKey(deviceId); + + /// + /// SDK 线程调用:提交指定设备的截图数据,完成待处理请求 + /// + /// 目标设备ID + /// 截图字节数组(JPEG/PNG 格式) + public void ProvideSnapshot(long deviceId, byte[] data) + { + // 检查是否存在待处理请求,存在则推送数据并完成任务 + if (_pendingRequests.TryGetValue(deviceId, out var tcs)) + { + tcs.TrySetResult(data); + } + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs new file mode 100644 index 0000000..3e03513 --- /dev/null +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -0,0 +1,233 @@ +namespace SHH.CameraSdk; + +/// +/// [管理层] 视频源总控管理器 (V3.3.1 修复版) +/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈 +/// 核心修复: +/// 1. [Bug γ] 二次伤害:强化销毁流程,防止 Dispose 阶段因 GC 乱序导致的非托管内存非法访问 +/// 2. [Bug A/L] 继承之前的动态感知与末日销毁协同修复,保障多线程环境下的状态一致性 +/// +public class CameraManager : IDisposable, IAsyncDisposable +{ + #region --- 1. 核心资源与状态 (Fields & States) --- + + /// 全局设备实例池(线程安全),Key = 设备唯一标识 + private readonly ConcurrentDictionary _cameraPool = new(); + + /// 后台协调器实例:负责心跳检测、断线重连、僵尸流恢复 + private readonly CameraCoordinator _coordinator = new(); + + /// 全局取消令牌源:用于销毁时瞬间关停所有异步扫描任务 + private readonly CancellationTokenSource _globalCts = new(); + + /// 销毁状态标记:防止重复销毁或销毁过程中执行操作 + private volatile bool _isDisposed; + + /// + /// [Fix Bug A: 动态失效] 协调器引擎运行状态标记 + /// 使用 volatile 关键字确保多线程环境下的内存可见性,避免指令重排导致的状态不一致 + /// + private volatile bool _isEngineStarted = false; + + #endregion + + #region --- 2. 设备管理 (Device Management) --- + + /// + /// 向管理池添加新相机设备 + /// + /// 相机设备配置信息 + 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); + } + } + + /// + /// 根据设备ID获取指定的视频源实例 + /// + /// 设备唯一标识 + /// 视频源实例 / 不存在则返回 null + public BaseVideoSource? GetDevice(long id) + => _cameraPool.TryGetValue(id, out var source) ? source : null; + + #endregion + + #region --- 3. 生命周期控制 (Engine Lifecycle) --- + + /// + /// 启动视频管理引擎,初始化SDK并启动协调器自愈循环 + /// + 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; + } + + /// + /// 获取当前所有相机的全局状态简报 + /// + /// 包含设备ID、IP、运行状态的元组集合 + 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) --- + + /// + /// 获取所有相机的健康度报告 + /// + /// 相机健康度报告集合 + public IEnumerable 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 等指标 + }); + } + + /// + /// [新增] 获取全量相机实时遥测数据快照 + /// 用于 WebAPI 实时监控大屏展示 + /// + /// 相机遥测数据快照集合 + public IEnumerable 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) --- + + /// + /// 同步销毁:内部调用异步销毁逻辑,等待销毁完成 + /// + public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); + + /// + /// [修复 Bug L & Bug γ] 异步执行全局资源清理 + /// 严格遵循销毁顺序:停止任务 → 销毁设备 → 卸载SDK,防止非托管内存泄漏 + /// + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Memory/FramePool.cs b/SHH.CameraSdk/Core/Memory/FramePool.cs new file mode 100644 index 0000000..646bd97 --- /dev/null +++ b/SHH.CameraSdk/Core/Memory/FramePool.cs @@ -0,0 +1,140 @@ +using OpenCvSharp; + +namespace SHH.CameraSdk; + +/// +/// [零延迟核心] 智能帧对象池 +/// 功能:预分配并复用 SmartFrame 实例,杜绝频繁 new Mat() 与 GC 回收,消除内存分配停顿 +/// 核心策略: +/// 1. 预热分配:启动时创建初始数量帧,避免运行时内存申请 +/// 2. 上限控制:最大池大小限制内存占用,防止内存溢出 +/// 3. 背压丢帧:池空时返回 null,强制丢帧保证实时性,不阻塞生产端 +/// +public class FramePool : IDisposable +{ + #region --- 私有资源与配置 (Private Resources & Configurations) --- + + /// 可用帧队列(线程安全):存储待借出的空闲智能帧 + private readonly ConcurrentQueue _availableFrames = new(); + + /// 所有已分配帧列表:用于统一销毁释放内存 + private readonly List _allAllocatedFrames = new(); + + /// 创建新帧锁:确保多线程下创建新帧的线程安全 + private readonly object _lock = new(); + + /// 帧宽度(与相机输出分辨率一致) + private readonly int _width; + + /// 帧高度(与相机输出分辨率一致) + private readonly int _height; + + /// 帧数据类型(如 CV_8UC3 对应 RGB 彩色图像) + private readonly MatType _type; + + /// 池最大容量:限制最大分配帧数,防止内存占用过高 + private readonly int _maxPoolSize; + + #endregion + + #region --- 构造与预热 (Constructor & Warm-Up) --- + + /// + /// 初始化智能帧对象池 + /// + /// 帧宽度 + /// 帧高度 + /// 帧数据类型 + /// 初始预热帧数(默认5) + /// 池最大容量(默认10) + 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(); + } + } + + /// + /// 创建新智能帧并加入池(内部调用,加锁保护) + /// + private void CreateNewFrame() + { + var frame = new SmartFrame(this, _width, _height, _type); + _allAllocatedFrames.Add(frame); + _availableFrames.Enqueue(frame); + } + + #endregion + + #region --- 帧借出与归还 (Frame Borrow & Return) --- + + /// + /// 从池借出一个智能帧(O(1) 时间复杂度) + /// + /// 可用智能帧 / 池空且达上限时返回 null(触发背压丢帧) + 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; + } + + /// + /// [系统内部调用] 将帧归还至池(由 SmartFrame.Dispose 自动触发) + /// + /// 待归还的智能帧 + public void Return(SmartFrame frame) + { + _availableFrames.Enqueue(frame); + } + + #endregion + + #region --- 资源释放 (Resource Disposal) --- + + /// + /// 释放帧池所有资源,销毁所有 Mat 内存 + /// + public void Dispose() + { + // 遍历所有已分配帧,释放 OpenCV Mat 底层内存 + foreach (var frame in _allAllocatedFrames) + { + frame.InternalMat.Dispose(); + } + _allAllocatedFrames.Clear(); + _availableFrames.Clear(); + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Memory/SmartFrame.cs b/SHH.CameraSdk/Core/Memory/SmartFrame.cs new file mode 100644 index 0000000..a0356f1 --- /dev/null +++ b/SHH.CameraSdk/Core/Memory/SmartFrame.cs @@ -0,0 +1,95 @@ +using OpenCvSharp; + +namespace SHH.CameraSdk; + +/// +/// [零延迟核心] 智能帧(内存复用+引用计数) +/// 功能:封装 OpenCV Mat 实现物理内存复用,通过引用计数管理生命周期,避免 GC 频繁回收导致的性能抖动 +/// 特性:引用归零自动回池,全程无内存分配/释放开销,支撑高帧率实时流处理 +/// +public class SmartFrame : IDisposable +{ + #region --- 私有资源与状态 (Private Resources & States) --- + + /// 所属帧池:用于引用归零后自动回收复用 + private readonly FramePool _pool; + + /// 引用计数器:线程安全,控制帧的生命周期 + /// 初始值 0,激活后设为 1;引用归零则自动回池 + private int _refCount = 0; + + #endregion + + #region --- 公共属性 (Public Properties) --- + + /// 帧数据物理内存载体(OpenCV Mat 对象) + /// 内存由帧池预分配,全程复用,不触发 GC + public Mat InternalMat { get; private set; } + + /// 帧激活时间戳(记录帧被取出池的时刻) + public DateTime Timestamp { get; private set; } + + #endregion + + #region --- 构造与激活 (Constructor & Activation) --- + + /// + /// 初始化智能帧(由帧池调用,外部禁止直接实例化) + /// + /// 所属帧池实例 + /// 帧宽度 + /// 帧高度 + /// 帧数据类型(如 MatType.CV_8UC3) + internal SmartFrame(FramePool pool, int width, int height, MatType type) + { + _pool = pool; + // 预分配物理内存:内存块在帧池生命周期内复用,避免频繁申请/释放 + InternalMat = new Mat(height, width, type); + } + + /// + /// [生产者调用] 从帧池取出时激活帧 + /// 功能:初始化引用计数,标记激活时间戳 + /// + public void Activate() + { + // 激活后引用计数设为 1,代表生产者(驱动/管道)持有该帧 + _refCount = 1; + // 记录帧被取出池的时间,用于后续延迟计算 + Timestamp = DateTime.Now; + } + + #endregion + + #region --- 引用计数管理 (Reference Count Management) --- + + /// + /// [消费者调用] 增加引用计数 + /// 适用场景:帧需要被多模块同时持有(如同时分发到 UI 和 AI 分析) + /// + public void AddRef() + { + // 原子递增:线程安全,避免多线程竞争导致计数错误 + Interlocked.Increment(ref _refCount); + } + + #endregion + + #region --- 释放与回池 (Disposal & Pool Return) --- + + /// + /// [消费者调用] 释放引用计数 + /// 核心逻辑:引用归零后自动将帧归还至帧池,实现内存复用 + /// + public void Dispose() + { + // 原子递减:线程安全,确保计数准确 + if (Interlocked.Decrement(ref _refCount) <= 0) + { + // 引用归零:所有消费者均已释放,将帧归还池复用 + _pool.Return(this); + } + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Pipeline/GlobalProcessingCenter.cs b/SHH.CameraSdk/Core/Pipeline/GlobalProcessingCenter.cs new file mode 100644 index 0000000..a71514c --- /dev/null +++ b/SHH.CameraSdk/Core/Pipeline/GlobalProcessingCenter.cs @@ -0,0 +1,64 @@ +using SHH.CameraSdk; + +/// +/// 全局帧处理中心(静态类) +/// 功能:接收驱动层的帧数据与决策,封装为处理任务并投递至处理管道,是驱动层与处理管道的桥梁 +/// 核心修复:初始化全链路追踪上下文并绑定到任务,避免空引用异常;管道满时记录丢弃日志并释放帧资源 +/// +public static class GlobalProcessingCenter +{ + #region --- 私有资源 (Private Resources) --- + + /// 全局帧处理管道实例 + /// 固定容量 50,采用 DropWrite 模式,生产端永不阻塞 + private static readonly ProcessingPipeline _pipeline = new ProcessingPipeline(capacity: 50); + + #endregion + + #region --- 核心任务提交 (Core Task Submission) --- + + /// + /// 提交帧数据与决策到处理中心 + /// 功能:封装帧为处理任务,初始化追踪上下文,投递至管道;投递失败时记录丢弃日志并释放资源 + /// + /// 产生帧的设备唯一标识 + /// 待处理的智能帧数据 + /// 帧处理决策(包含是否保留、分发目标等信息) + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs b/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs new file mode 100644 index 0000000..abcab37 --- /dev/null +++ b/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs @@ -0,0 +1,157 @@ +namespace SHH.CameraSdk; + +/// +/// 全局流分发器(静态类 | 线程安全) +/// 核心职责: +/// 1. 接收处理完成的帧任务,基于 AppId 路由策略实现帧的精准定向分发 +/// 2. 隔离 UI 预览、AI 分析等不同消费场景,支撑多模块并行消费 +/// 设计特性: +/// ✅ 线程安全:基于 ConcurrentDictionary 实现并发订阅/取消订阅 +/// ✅ 精准路由:按 TargetAppIds 点对点投递,避免广播风暴 +/// ✅ 异常隔离:单个订阅者异常不影响其他模块消费 +/// +public static class GlobalStreamDispatcher +{ + #region --- 1. 预设订阅通道 (Predefined Subscription Channels) --- + + /// + /// UI 预览订阅通道:供 UI 模块订阅帧数据,用于实时画面显示 + /// 回调参数:(设备唯一标识, 处理后的智能帧数据) + /// 特性:低延迟优先,支持画面渲染相关的轻量级处理 + /// + public static event Action? OnPreviewFrame; + + /// + /// AI 分析订阅通道:供 AI 模块订阅帧数据,用于行为识别/人脸检测/车牌识别等 + /// 回调参数:(设备唯一标识, 处理后的智能帧数据) + /// 特性:高吞吐优先,支持复杂算法处理,延迟容忍度较高 + /// + public static event Action? OnAnalysisFrame; + + #endregion + + #region --- 2. 动态路由表 (Dynamic Routing Table) --- + + /// + /// 动态订阅路由表:Key = 业务 AppId,Value = 帧处理多播委托 + /// 实现:ConcurrentDictionary 保证高并发场景下的读写安全 + /// 用途:支持自定义业务模块的精准订阅,扩展帧消费能力 + /// + private static readonly ConcurrentDictionary> _routingTable = new(); + + #endregion + + #region --- 3. 订阅管理接口 (Subscription Management API) --- + + /// + /// 精准订阅:为指定 AppId 注册帧处理回调 + /// 线程安全:支持多线程并发调用,委托自动合并(多播) + /// + /// 业务唯一标识(需与 FrameController.Register 中的 AppId 一致) + /// 帧处理回调函数 + /// appId 或 handler 为空时抛出 + public static void Subscribe(string appId, Action 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 + ); + } + + /// + /// 取消订阅:移除指定 AppId 的帧处理回调 + /// 线程安全:支持多线程并发调用,无订阅时静默处理 + /// + /// 业务唯一标识 + /// 需要移除的帧处理回调 + public static void Unsubscribe(string appId, Action 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) --- + + /// + /// 帧任务分发入口:基于任务的 TargetAppIds 实现精准点对点投递 + /// 核心优化:摒弃广播模式,仅投递到指定订阅者,降低系统资源消耗 + /// + /// 处理完成的帧任务(包含目标 AppId 列表、帧数据、上下文) + /// task 为空时抛出 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs b/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs new file mode 100644 index 0000000..19abac1 --- /dev/null +++ b/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs @@ -0,0 +1,148 @@ +namespace SHH.CameraSdk; + +/// +/// 帧处理管道(后台处理核心) +/// 功能:接收帧处理任务,在后台单线程执行二次处理(如打水印、裁剪),并分发至目标订阅者 +/// 核心特性: +/// 1. 有界通道+DropWrite模式:生产端永不阻塞,管道满时丢弃新任务,避免内存积压 +/// 2. 单线程处理:CPU占用恒定,避免多线程竞争导致的性能抖动 +/// 3. 引用计数管理:确保帧数据安全转移与释放,防止内存泄漏 +/// +public class ProcessingPipeline +{ + #region --- 私有资源与状态 (Private Resources & States) --- + + /// 任务队列(有界通道):存储待处理的帧任务 + private readonly Channel _queue; + + /// 取消令牌源:用于终止后台处理循环 + private readonly CancellationTokenSource _cts = new(); + + #endregion + + #region --- 构造与初始化 (Constructor & Initialization) --- + + /// + /// 初始化帧处理管道 + /// + /// 管道最大容量:超过该值时,新任务将被丢弃(DropWrite模式) + public ProcessingPipeline(int capacity) + { + // 创建有界通道,配置核心特性 + _queue = Channel.CreateBounded(new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.DropWrite, // 管道满时丢弃新写入的任务 + SingleReader = true, // 单线程读取,保证处理顺序与CPU稳定性 + SingleWriter = false // 支持多线程写入(如多相机同时提交任务) + }); + + // 启动后台处理循环(长期运行任务,标记为 LongRunning 提升调度优先级) + Task.Factory.StartNew(ProcessLoopAsync, TaskCreationOptions.LongRunning); + } + + #endregion + + #region --- 任务提交 (Task Submission) --- + + /// + /// 尝试提交帧处理任务到管道 + /// 核心逻辑:非阻塞提交,失败时回滚帧引用计数,避免内存泄漏 + /// + /// 待处理的帧任务(包含帧数据、决策、追踪上下文) + /// 提交成功返回 true,管道满导致提交失败返回 false + 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) --- + + /// + /// 后台处理循环:持续读取队列任务,执行二次处理与分发 + /// + 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) --- + + /// + /// 执行帧二次处理与分发 + /// 功能:对帧进行自定义加工(如打水印、格式转换),并通过分发器发送至目标订阅者 + /// + /// 待处理的帧任务 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs b/SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs new file mode 100644 index 0000000..ab73c62 --- /dev/null +++ b/SHH.CameraSdk/Core/Pipeline/ProcessingTask.cs @@ -0,0 +1,40 @@ +namespace SHH.CameraSdk; + +/// +/// 帧处理任务模型 +/// 功能:封装单帧数据的处理任务信息,包含帧数据、分发决策、全链路追踪上下文,是帧处理管道的核心数据载体 +/// 用途:在全局处理中心与分发器之间传递,串联帧的二次处理、分发与追踪流程 +/// +public class ProcessingTask +{ + #region --- 任务核心标识 (Task Core Identification) --- + + /// 设备唯一标识(关联产生该帧的相机设备ID) + public long DeviceId { get; set; } + + #endregion + + #region --- 帧核心数据 (Frame Core Data) --- + + /// 待处理的智能帧数据(包含原始图像数据与引用计数管理) + /// 非空约束:任务必须关联有效帧数据,不可为 null + public SmartFrame Frame { get; set; } = null!; + + #endregion + + #region --- 帧分发决策 (Frame Distribution Decision) --- + + /// 帧处理决策信息(包含是否保留帧、分发目标AppId列表等) + /// 非空约束:任务必须携带决策信息,指导后续分发逻辑 + public FrameDecision Decision { get; set; } = null!; + + #endregion + + #region --- 全链路追踪上下文 (Full-Link Tracing Context) --- + + /// 帧全链路追踪上下文(用于记录帧处理过程中的日志、耗时、状态等信息) + /// 非空约束:支持通过 AddLog 方法补充追踪日志,支撑问题排查 + public FrameContext Context { get; set; } = null!; + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs new file mode 100644 index 0000000..bc30274 --- /dev/null +++ b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs @@ -0,0 +1,231 @@ +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace SHH.CameraSdk; + +/// +/// [调度协调层] 视频自愈调度器 (V3.3.4 流量削峰版) +/// 核心职责:监控所有相机设备的运行状态,实现断线自动重连、僵死状态复位,保障视频流稳定 +/// 核心修复: +/// 1. [Bug τ] 线程池保护:引入并发节流阀,限制同时重连/探测的任务数,防止线程池饥饿 +/// +public class CameraCoordinator +{ + #region --- 私有资源与配置 (Private Resources & Configurations) --- + + /// 已注册的相机设备集合(线程安全,支持并发添加与遍历) + private readonly ConcurrentBag _cameras = new(); + + /// 全局登录单行道锁 + /// 限制同一时刻仅允许一个相机执行登录操作,避免 SDK 登录冲突 + private readonly SemaphoreSlim _sdkLoginLock = new(1, 1); + + /// 并发节流阀:限制同时进行探测/重连的任务数 + /// 最大并发数 8,避免百路相机同时重连导致 CPU 峰值过高、UI 卡顿 + private readonly SemaphoreSlim _concurrencyLimiter = new(8); + + /// 相机流存活判定阈值(秒):超过该时间无帧则判定为流中断 + private const int StreamAliveThresholdSeconds = 5; + + /// Ping 探测超时时间(毫秒) + private const int PingTimeoutMs = 800; + + /// TCP 探测超时时间(毫秒) + private const int TcpTimeoutMs = 1000; + + /// 调度循环间隔(毫秒):每 5 秒执行一次全量设备状态校验 + private const int CoordinationLoopIntervalMs = 5000; + + #endregion + + #region --- 相机注册 (Camera Registration) --- + + /// + /// 注册相机设备到调度器 + /// 功能:将相机纳入全局状态监控与自愈管理 + /// + /// 待注册的相机设备实例 + public void Register(BaseVideoSource camera) => _cameras.Add(camera); + + #endregion + + #region --- 核心调度循环 (Core Coordination Loop) --- + + /// + /// 启动调度协调循环(长期运行任务) + /// 功能:周期性校验所有相机状态,执行自愈逻辑,支持取消 + /// + /// 取消令牌:用于终止调度循环 + 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) --- + + /// + /// 安全执行相机状态调和 + /// 功能:隔离单个相机的异常,避免影响其他相机的调和逻辑 + /// + /// 待调和的相机设备 + /// 取消令牌 + private async Task SafeReconcileAsync(BaseVideoSource cam, CancellationToken token) + { + try + { + await ReconcileAsync(cam, token).ConfigureAwait(false); + } + catch + { + // 吞没单个相机的异常,确保其他相机正常调度 + } + } + + #endregion + + #region --- 状态调和逻辑 (Reconciliation Logic) --- + + /// + /// 相机状态调和(核心自愈逻辑) + /// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性 + /// + /// 待调和的相机设备 + /// 取消令牌 + 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) --- + + /// + /// 硬件连接探测:通过 Ping + TCP 双探测判定设备物理可达性 + /// + /// 待探测的相机设备 + /// 物理可达返回 true,否则返回 false + private async Task 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs new file mode 100644 index 0000000..f76c03b --- /dev/null +++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs @@ -0,0 +1,86 @@ +namespace SHH.CameraSdk; + +/// +/// 帧控制器(帧调度核心) +/// 功能:管理订阅者的帧需求,基于需求动态判定每帧的处理命运(保留/丢弃、分发目标) +/// 核心逻辑:采用“并集采样”策略,满足任意订阅者的帧率需求即保留帧,避免重复采样浪费资源 +/// +public class FrameController +{ + #region --- 私有资源与状态 (Private Resources & States) --- + + /// 订阅者帧需求集合(线程安全) + /// Key:订阅者AppId,Value:该订阅者的帧需求配置 + private readonly ConcurrentDictionary _requirements = new(); + + /// 全局决策序列号(原子递增,确保决策唯一标识) + private long _globalSequence = 0; + + #endregion + + #region --- 需求管理 (Requirement Management) --- + + /// + /// 注册/更新订阅者的帧需求 + /// 功能:新增订阅者需求或更新已有订阅者的目标帧率 + /// + /// 订阅者唯一标识(如 "RemoteClient_01"、"AI_Behavior_Engine") + /// 目标帧率(单位:fps,需大于0,否则视为无效需求) + 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) --- + + /// + /// [热路径] 判定当前物理帧是否需要保留并分发 + /// 核心逻辑:并集采样,只要任意订阅者达到采样间隔,就保留该帧并分发至对应订阅者 + /// + /// 当前系统时间戳(单位:毫秒,建议使用 Environment.TickCount64) + /// 帧决策结果(包含是否保留、分发目标等信息) + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Scheduling/FrameDecision.cs b/SHH.CameraSdk/Core/Scheduling/FrameDecision.cs new file mode 100644 index 0000000..d20f4d4 --- /dev/null +++ b/SHH.CameraSdk/Core/Scheduling/FrameDecision.cs @@ -0,0 +1,34 @@ +namespace SHH.CameraSdk; + +/// +/// 帧决策结果模型 +/// 功能:告知驱动层单帧数据的处理命运(保留/丢弃、分发目标),是帧调度的核心指令 +/// 用途:由 FrameController 生成,传递给驱动层与分发器,指导帧的后续流转 +/// +public class FrameDecision +{ + #region --- 决策核心标识 (Decision Core Identification) --- + + /// 决策序列号(全局唯一,关联帧的决策记录,用于追踪决策生命周期) + public long Sequence { get; set; } + + /// 决策生成时间戳(记录决策的创建时刻,默认当前时间) + public DateTime Timestamp { get; set; } = DateTime.Now; + + #endregion + + #region --- 帧处理决策 (Frame Processing Decision) --- + + /// 帧是否被保留(true=保留并分发,false=直接丢弃,不进行后续处理) + public bool IsCaptured { get; set; } + + #endregion + + #region --- 帧分发目标 (Frame Distribution Targets) --- + + /// 帧分发目标应用ID列表(记录该帧将服务的所有订阅者AppId) + /// 示例值:["WPF_Display_Main", "AI_Behavior_Engine"],仅当 IsCaptured 为 true 时有效 + public List TargetAppIds { get; } = new(); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs b/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs new file mode 100644 index 0000000..8f03a0e --- /dev/null +++ b/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs @@ -0,0 +1,37 @@ +namespace SHH.CameraSdk; + +/// +/// 帧需求定义模型 +/// 功能:描述某个程序/模块对视频帧的消费需求,用于帧分发调度与帧率控制 +/// 用途:配合 FrameController,实现按订阅者需求精准分配帧资源,避免资源浪费 +/// +public class FrameRequirement +{ + #region --- 订阅者核心标识 (Subscriber Core Identification) --- + + /// 订阅者唯一ID(如 "Client_A"、"AI_Service"、"WPF_Display_Main") + /// 用于区分不同的帧消费模块,作为帧分发路由的关键标识 + public string AppId { get; set; } = string.Empty; + + #endregion + + #region --- 帧需求参数 (Frame Requirement Parameters) --- + + /// 目标帧率(单位:fps,订阅者期望的每秒接收帧数,0 表示无特定需求) + /// 例如:UI 预览需 8fps,AI 分析需 2fps,按需分配以节省计算资源 + public int TargetFps { get; set; } = 0; + + /// 上次获取帧的时间点(单位:毫秒,通常为 Environment.TickCount64) + /// 用于帧率控制算法,判断是否达到订阅者的目标帧率需求 + public long LastCaptureTick { get; set; } = 0; + + #endregion + + #region --- 需求状态控制 (Requirement Status Control) --- + + /// 需求是否激活(true=正常接收帧,false=暂停接收,保留配置) + /// 支持动态启停订阅,无需删除需求配置,提升灵活性 + public bool IsActive { get; set; } = true; + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/CameraHealthReport.cs b/SHH.CameraSdk/Core/Telemetry/CameraHealthReport.cs new file mode 100644 index 0000000..89236c2 --- /dev/null +++ b/SHH.CameraSdk/Core/Telemetry/CameraHealthReport.cs @@ -0,0 +1,50 @@ +namespace SHH.CameraSdk; + +/// +/// 相机健康度报告 +/// 功能:封装相机的详细运行健康数据,包含状态、性能、故障统计等信息 +/// 用途:用于运维分析、故障排查、设备健康度评估,提供比遥测快照更细致的健康指标 +/// +public class CameraHealthReport +{ + #region --- 设备核心标识 (Device Core Identification) --- + + /// 设备唯一业务标识(对应数据库ID或配置中的设备ID) + public long DeviceId { get; set; } + + /// 设备IP地址(用于定位具体设备) + public string Ip { get; set; } = string.Empty; + + #endregion + + #region --- 设备运行状态 (Device Operation Status) --- + + /// 设备当前运行状态(字符串形式,对应 VideoSourceStatus 枚举值,如 "Streaming"/"Faulted"/"Reconnecting") + public string Status { get; set; } = string.Empty; + + /// 最后一次错误信息(无错误时建议设为空字符串,记录设备最近一次故障原因) + public string LastError { get; set; } = string.Empty; + + #endregion + + #region --- 性能与延迟指标 (Performance & Latency Metrics) --- + + /// 实时帧率(单位:fps,反映相机实际输出的有效帧率) + /// 统计逻辑:需在 RaiseFrameReceived 事件中增加计数器,按时间窗口计算实时值 + public double RealFps { get; set; } + + /// 推流延迟(单位:毫秒,记录从相机推流到接收端成功接收的总延迟) + public double LatencyMs { get; set; } + + #endregion + + #region --- 故障与恢复统计 (Fault & Recovery Statistics) --- + + /// 丢帧计数(因渲染过慢、缓冲区溢出等原因导致的丢弃帧数累计值) + public long DropFrames { get; set; } + + /// 重连次数(哨兵机制触发的自动重连累计次数,反映设备网络稳定性) + public int ReconnectCount { get; set; } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs b/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs new file mode 100644 index 0000000..0d3158f --- /dev/null +++ b/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs @@ -0,0 +1,56 @@ +namespace SHH.CameraSdk; + +/// +/// 相机实时遥测数据快照 +/// 功能:封装单台相机的实时运行状态、性能指标与健康度信息,用于监控面板展示、运维告警与数据分析 +/// 特性:数据为瞬时快照,通常定期(如1秒/次)更新,反映相机当前运行状况 +/// +public class CameraTelemetryInfo +{ + #region --- 设备核心标识 (Device Core Identification) --- + + /// 设备唯一业务标识(对应数据库ID或配置中的设备ID) + public long DeviceId { get; set; } + + /// 设备显示名称(如“北大门-枪机01”,用于UI展示) + public string Name { get; set; } = string.Empty; + + /// 设备IP地址(用于网络连通性校验与远程访问) + public string IpAddress { get; set; } = string.Empty; + + #endregion + + #region --- 设备运行状态 (Device Operation Status) --- + + /// 相机当前运行状态(字符串形式,对应 VideoSourceStatus 枚举值,如 "Playing"/"Faulted"/"Connecting") + public string Status { get; set; } = string.Empty; + + /// 设备物理连接状态(通过 Ping/TCP 探测判定,true=在线,false=离线) + public bool IsOnline { get; set; } + + /// 最后一次报错信息(无错误时为 null,用于快速定位故障原因) + public string? LastErrorMessage { get; set; } + + #endregion + + #region --- 性能指标 (Performance Metrics) --- + + /// 实时帧率(单位:fps,反映相机取流与处理的实时速度) + public double Fps { get; set; } + + /// 累计接收帧数(相机启动后接收的总帧数,用于统计数据完整性) + public long TotalFrames { get; set; } + + #endregion + + #region --- 健康度与统计 (Health & Statistics) --- + + /// 设备健康度评分(0-100分,分数越高健康状态越好) + /// 计算逻辑:结合是否断线、实时FPS是否在正常范围、是否有报错等因素综合判定 + public int HealthScore { get; set; } + + /// 遥测数据统计时间戳(记录当前快照的生成时间,默认当前时间) + public DateTime Timestamp { get; set; } = DateTime.Now; + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/FrameConsumerType.cs b/SHH.CameraSdk/Core/Telemetry/FrameConsumerType.cs new file mode 100644 index 0000000..d6026b8 --- /dev/null +++ b/SHH.CameraSdk/Core/Telemetry/FrameConsumerType.cs @@ -0,0 +1,21 @@ +namespace SHH.CameraSdk; + +/// +/// 帧消费者类型枚举 +/// 功能:定义帧数据的消费场景/模块标识,用于帧分发路由、权限控制与遥测统计 +/// 用途:配合全局流分发器(GlobalStreamDispatcher),实现帧数据的精准定向分发 +/// +public enum FrameConsumerType +{ + /// UI 预览消费:用于前端界面实时显示(如 WPF/WinForm 控件、Web 页面渲染) + UI_Preview, + + /// AI 分析消费:用于 AI 算法处理(如行为识别、人脸检测、车牌识别等耗时分析场景) + AI_Analysis, + + /// 网络流消费:用于网络推流(如 RTSP/RTMP 推流、WebSocket 实时推送等) + Network_Stream, + + /// 一次性截图消费:用于单次截图操作(如用户手动抓拍、定时快照等临时消费场景) + Snapshot_OneOff +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/FrameContext.cs b/SHH.CameraSdk/Core/Telemetry/FrameContext.cs new file mode 100644 index 0000000..7514049 --- /dev/null +++ b/SHH.CameraSdk/Core/Telemetry/FrameContext.cs @@ -0,0 +1,61 @@ +namespace SHH.CameraSdk; + +/// +/// 帧全链路追踪上下文 +/// 功能:记录单帧数据从产生到结束的完整生命周期信息,包含标识、决策结果、性能指标与日志流水 +/// 用途:用于问题排查、性能分析、帧流转追溯,支撑全链路可观测性 +/// +public class FrameContext +{ + #region --- 帧核心标识 (Frame Core Identification) --- + + /// 物理帧序号(全局唯一,关联帧的原始数据标识) + public long FrameSequence { get; set; } + + /// 帧上下文创建时间戳(默认当前时间,记录帧进入追踪链路的时刻) + public DateTime Timestamp { get; set; } = DateTime.Now; + + #endregion + + #region --- 帧决策结果 (Frame Decision Results) --- + + /// 帧是否被保留(true=保留并分发,false=被丢弃) + public bool IsCaptured { get; set; } + + /// 帧丢弃原因(仅 IsCaptured 为 false 时有效,默认空字符串) + /// 示例值:"NoSubscribers"(无订阅者)、"PipelineFull"(处理管道满)、"FpsLimit"(帧率限制) + public string DropReason { get; set; } = string.Empty; + + /// 帧分发目标应用ID列表(记录该帧最终分发给的所有订阅者标识,合并结果) + /// 示例值:["WPF_Display_Main", "AI_Behavior_Engine"] + public List TargetAppIds { get; set; } = new(); + + #endregion + + #region --- 帧性能指标 (Frame Performance Metrics) --- + + /// 颜色转码耗时(单位:毫秒) + /// 记录帧数据格式转换(如 YUV→BGR)的耗时,用于性能瓶颈定位 + public double CvtColorCostMs { get; set; } + + /// 二次处理耗时(单位:毫秒) + /// 记录帧在处理管道中的额外加工耗时(如打水印、裁剪、AI预处理等) + public double ProcessCostMs { get; set; } + + /// 帧总处理耗时(单位:毫秒) + /// 记录帧从进入追踪链路到处理完成/丢弃的总耗时,为性能优化提供数据支撑 + public double TotalCostMs { get; set; } + + #endregion + + #region --- 帧日志流水 (Frame Logs) --- + + /// 帧生命周期日志流水(按时间顺序记录关键节点操作) + public List Logs { get; } = new(); + + /// 新增帧日志(自动添加时间戳,格式:[HH:mm:ss.fff] 日志内容) + /// 日志内容(记录帧流转的关键节点,如“驱动提交帧数据”“管道处理完成”) + public void AddLog(string msg) => Logs.Add($"[{DateTime.Now:HH:mm:ss.fff}] {msg}"); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/FrameTrace.cs b/SHH.CameraSdk/Core/Telemetry/FrameTrace.cs new file mode 100644 index 0000000..3e5e6dd --- /dev/null +++ b/SHH.CameraSdk/Core/Telemetry/FrameTrace.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +namespace SHH.CameraSdk; + +/// +/// 帧追踪数据模型 +/// 功能:记录单帧数据的生命周期关键信息,用于帧流转监控、丢帧分析与性能排查 +/// 适用场景:配合全局遥测或调试工具,追溯帧的处理路径、耗时及最终状态 +/// +public class FrameTrace +{ + #region --- 帧核心标识 (Frame Core Identification) --- + + /// 帧唯一序列号(全局唯一,用于关联帧的全生命周期) + public long FrameId { get; set; } + + /// 帧产生时间戳(单位:毫秒,通常为 Environment.TickCount64 或 UTC 时间戳) + public long Timestamp { get; set; } + + #endregion + + #region --- 帧状态信息 (Frame Status Information) --- + + /// 帧是否被丢弃(true=已丢弃,false=正常处理/分发) + public bool IsDropped { get; set; } + + /// 帧丢弃原因(仅 IsDropped 为 true 时有效) + /// 示例值:"NoSubscribers"(无订阅者)、"FpsLimit"(帧率限制)、"PipelineFull"(处理管道满) + public string DropReason { get; set; } = string.Empty; + + /// 帧最终分发目标列表(记录该帧被发送到的订阅者/模块标识) + /// 示例值:["WPF_Display_Main", "AI_Behavior_Engine"] + public List Targets { get; set; } = new(); + + #endregion + + #region --- 帧性能指标 (Frame Performance Metrics) --- + + /// 帧处置总耗时(单位:毫秒) + /// 计算范围:从帧产生到最终处理完成/丢弃的总时间,用于性能瓶颈分析 + public double ProcessDurationMs { get; set; } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/GlobalTelemetry.cs b/SHH.CameraSdk/Core/Telemetry/GlobalTelemetry.cs new file mode 100644 index 0000000..0ec6c21 --- /dev/null +++ b/SHH.CameraSdk/Core/Telemetry/GlobalTelemetry.cs @@ -0,0 +1,66 @@ +namespace SHH.CameraSdk; + +/// +/// 全局遥测仓储(静态类) +/// 功能:存储并提供帧生命周期的追踪日志查询,采用环形缓冲区机制限制日志数量 +/// 用途:用于问题排查、性能分析,记录每帧的处理流程、耗时、丢弃原因等信息 +/// +public static class GlobalTelemetry +{ + #region --- 静态存储资源 (Static Storage Resources) --- + + /// + /// 环形日志缓冲区:存储帧追踪上下文(线程安全) + /// Key:帧序列号(FrameSequence),Value:帧全链路追踪上下文 + /// + private static readonly ConcurrentDictionary _logs = new(); + + /// + /// 日志序列号队列:用于维护环形缓冲区的淘汰顺序(线程安全) + /// 作用:记录帧日志的插入顺序,超过最大数量时淘汰最早的记录 + /// + private static readonly ConcurrentQueue _keys = new(); + + /// + /// 最大日志保留数量:限制环形缓冲区仅保留最近 200 条帧追踪记录 + /// 目的:防止日志过多导致内存占用飙升 + /// + private const int MaxLogCount = 200; + + #endregion + + #region --- 核心操作方法 (Core Operation Methods) --- + + /// + /// 记录帧追踪日志 + /// 功能:将帧上下文存入缓冲区,超过最大数量时自动淘汰最早的记录 + /// + /// 帧序列号(唯一标识某一帧) + /// 帧全链路追踪上下文(含处理日志、耗时、丢弃原因等) + 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 _); + } + } + + /// + /// 获取最近的帧追踪日志 + /// 功能:按时间戳降序返回缓冲区中的所有记录(最新记录在前) + /// + /// 帧追踪上下文集合(最多 MaxLogCount 条) + public static IEnumerable GetRecentLogs() + { + // 按帧上下文的时间戳降序排序,确保最新记录优先返回 + return _logs.Values.OrderByDescending(x => x.Timestamp); + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs new file mode 100644 index 0000000..67b5bd0 --- /dev/null +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -0,0 +1,512 @@ +using System.Threading.Channels; + +namespace SHH.CameraSdk; + +/// +/// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版) +/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。 +/// 修复记录: +/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。 +/// 2. [Bug π] 管道安全:Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。 +/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。 +/// +public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable +{ + #region --- 核心配置与锁机制 (Core Config & Locks) --- + + // [Fix Bug δ] 核心配置对象 + // 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例 + protected VideoSourceConfig _config; + + /// + /// 状态同步锁 + /// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致的状态读取不一致 + /// + private readonly object _stateSyncRoot = new(); + + /// + /// 生命周期互斥锁 + /// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致的状态机混乱 + /// + private readonly SemaphoreSlim _lifecycleLock = new(1, 1); + + #endregion + + #region --- 内部状态与基础设施 (Internal States & Infrastructure) --- + + // 内部状态标志位 + private volatile bool _isOnline; + private VideoSourceStatus _status = VideoSourceStatus.Disconnected; + + /// + /// 状态通知队列 (有界) + /// 特性:采用 DropOldest 策略,当消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β] + /// + private readonly Channel _statusQueue; + + // 状态分发器的取消令牌源 + private CancellationTokenSource? _distributorCts; + + // [新增修复 Bug π] 分发任务引用 + // 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费 + private Task? _distributorTask; + + // [Fix Bug V] 单调时钟 + // 作用:记录最后一次收到帧的系统 Tick,用于心跳检测,不受系统时间修改影响 + private long _lastFrameTick = 0; + + /// 获取最后帧的时间戳 (线程安全读取) + public long LastFrameTick => Interlocked.Read(ref _lastFrameTick); + + /// 视频帧回调事件 (热路径) + public event Action? 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? 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) --- + + /// + /// 构造函数:初始化基础设施 + /// + /// 视频源基础配置(含设备连接信息、通道号等) + /// 配置为空时抛出 + protected BaseVideoSource(VideoSourceConfig config) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + + // [Fix Bug U] 初始配置深拷贝 + // 防止外部引用修改导致内部状态不可控(配置防漂移) + _config = CloneConfig(config); + + // [Fix Bug β] 初始化有界通道 + // 容量 100,单读者多写者模式 + _statusQueue = Channel.CreateBounded(new BoundedChannelOptions(100) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false + }); + + _distributorCts = new CancellationTokenSource(); + + // [关键逻辑] 启动后台状态分发循环 + // 明确持有 Task 引用,以便后续进行优雅关闭等待 + _distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token)); + } + + #endregion + + #region --- 配置管理 (Config Management) --- + + /// + /// [修复 Bug δ] 更新配置实现 + /// 允许在不销毁实例的情况下更新 IP、端口等参数,新配置下次连接生效 + /// + /// 新的视频源配置 + 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(); } + } + + /// + /// 配置深拷贝辅助方法 + /// [编译修复] 严格匹配源文件中的属性复制逻辑,确保 Dictionary 等引用类型被重新创建 + /// + /// 源配置对象 + /// 深拷贝后的配置实例 + 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(source.VendorArguments) + : new Dictionary() + }; + } + + #endregion + + #region --- 生命周期控制 (Lifecycle Control) --- + + /// + /// 异步启动设备连接 + /// 包含:状态校验、生命周期锁、非托管初始化、元数据刷新 + /// + 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(); } + } + + /// + /// 异步停止设备连接 + /// 流程:标记离线→执行驱动停止逻辑→更新状态 + /// + 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(); + } + } + + /// + /// 刷新设备元数据(能力集) + /// 对比新旧元数据差异,更新设备支持的功能、通道信息等 + /// + /// 元数据差异描述符 + public async Task 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; + } + + /// + /// 应用动态参数(如码流切换、OSD设置) + /// 支持运行时调整画面分辨率、帧率、渲染句柄等 + /// + /// 动态配置项 + 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) --- + + /// + /// 检查是否有帧订阅者 + /// 用于优化性能:无订阅时可跳过解码等耗时操作 + /// + /// 有订阅者返回 true,否则返回 false + protected bool HasFrameSubscribers() => FrameReceived != null; + + /// + /// 上报驱动层异常 + /// 将底层异常转换为 Reconnecting 状态,触发协调器介入自愈 + /// + /// 相机统一异常对象 + protected void ReportError(CameraException ex) + { + if (!_isOnline) return; + + _isOnline = false; + UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK报错: {ex.Message}"); + } + + /// + /// 标记收到一帧数据(心跳保活 + FPS计算) + /// [修改] 增强了 FPS 计算逻辑,每1秒结算一次实时帧率 + /// + 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; + } + } + + /// + /// 触发帧回调事件 + /// 向所有订阅者分发帧数据(热路径,尽量减少耗时操作) + /// + /// 帧数据(通常为 OpenCvSharp.Mat 或 SmartFrame) + protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData); + + #endregion + + #region --- 状态分发 (Status Distribution) --- + + /// + /// 后台状态分发循环 + /// 负责将 Channel 中的状态变更事件调度到 StatusChanged 事件订阅者 + /// + /// 取消令牌,用于终止分发循环 + 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}"); } + } + + /// + /// 更新设备状态并写入通道 + /// 线程安全,采用 DropOldest 策略防止状态队列溢出 + /// + /// 新状态 + /// 状态描述信息 + /// 可选:状态变更关联的异常 + 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) --- + + /// + /// 驱动层启动逻辑(必须由具体驱动实现) + /// 包含设备连接、登录、取流等底层操作 + /// + /// 取消令牌 + protected abstract Task OnStartAsync(CancellationToken token); + + /// + /// 驱动层停止逻辑(必须由具体驱动实现) + /// 包含设备登出、连接断开、资源释放等底层操作 + /// + protected abstract Task OnStopAsync(); + + /// + /// 驱动层元数据获取逻辑(必须由具体驱动实现) + /// 用于获取设备型号、通道能力、固件版本等信息 + /// + /// 设备元数据实例 + protected abstract Task OnFetchMetadataAsync(); + + #endregion + + #region --- 资源清理 (Disposal) --- + + /// + /// [Fix Bug A: 死锁终结者] 同步销毁入口 + /// 原理:强制启动一个新的后台 Task 执行 DisposeAsync,并同步阻塞等待其完成 + /// 效果:彻底规避了在 UI 线程直接 wait 导致的死锁问题 + /// + public void Dispose() + { + Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult(); + } + + /// + /// 异步销毁资源 + /// 包含:停止业务、关闭管道、断开事件引用、释放非托管资源 + /// + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikErrorMapper.cs b/SHH.CameraSdk/Drivers/HikVision/HikErrorMapper.cs new file mode 100644 index 0000000..4d67748 --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/HikErrorMapper.cs @@ -0,0 +1,124 @@ +namespace SHH.CameraSdk; + +/// +/// [海康] 错误码映射器 (V3.3.1 修复版) +/// 职责:将海康原始错误码映射为系统统一标准枚举,实现跨厂家故障语义归一化。 +/// 协作:为 HikVideoSource.ReportError 提供标准化故障信息,支撑协调器自愈决策。 +/// +public static class HikErrorMapper +{ + #region --- 静态映射资源 (Static Mapping Resources) --- + + /// + /// 海康原始错误码 → 系统标准错误码 映射表(只读,初始化后不可修改) + /// + private static readonly ReadOnlyDictionary _codeMap; + + /// + /// 海康原始错误码 → 中文描述 映射表(只读,初始化后不可修改) + /// + private static readonly ReadOnlyDictionary _descMap; + + /// + /// 静态构造函数:初始化错误码映射表(程序启动时仅执行一次) + /// + static HikErrorMapper() + { + // 1. 初始化:海康原始错误码 → 系统标准错误码 映射 + var codeDict = new Dictionary + { + { 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(codeDict); + + // 2. 初始化:海康原始错误码 → 中文描述 映射 + var descDict = new Dictionary + { + { 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(descDict); + } + + #endregion + + #region --- 核心映射方法 (Core Mapping Methods) --- + + /// + /// 将海康原始错误码转换为系统统一标准错误码 + /// + /// 海康 SDK 返回的原始错误码 + /// 系统标准错误码(未匹配到时返回 CameraErrorCode.Unknown) + public static CameraErrorCode Map(uint hikErrorCode) + { + // 尝试从映射表获取,未找到则返回未知错误 + return _codeMap.TryGetValue(hikErrorCode, out var code) ? code : CameraErrorCode.Unknown; + } + + /// + /// 获取海康原始错误码的中文描述(含原始错误码) + /// + /// 海康 SDK 返回的原始错误码 + /// 中文错误描述(格式:描述 (Code:原始错误码)) + public static string GetRawDescription(uint hikErrorCode) + { + if (_descMap.TryGetValue(hikErrorCode, out var desc)) + { + return $"{desc} (Code:{hikErrorCode})"; + } + // 未匹配到的错误码,返回默认描述 + return $"未知海康错误 (Code:{hikErrorCode})"; + } + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikExtensions.cs b/SHH.CameraSdk/Drivers/HikVision/HikExtensions.cs new file mode 100644 index 0000000..f184d14 --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/HikExtensions.cs @@ -0,0 +1,37 @@ +using SHH.CameraSdk; + +/// +/// 海康 SDK 扩展方法类 +/// 功能:提供海康 API 调用结果校验的快捷扩展,简化错误处理逻辑 +/// +public static class HikExtensions +{ + #region --- 结果校验扩展 (Result Validation Extensions) --- + + /// + /// 校验海康 API 调用结果是否成功 + /// 功能:若结果为 false(调用失败),自动捕获海康错误码并抛出统一异常 + /// + /// 海康 API 调用返回的布尔结果(true=成功,false=失败) + /// 操作名称(用于异常信息描述,如“设备登录”“启动预览”) + /// 设备品牌(默认海康威视,无需手动指定) + /// API 调用失败时抛出,包含标准错误码与原始错误描述 + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs new file mode 100644 index 0000000..37cbd5d --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/HikNativeMethods.cs @@ -0,0 +1,495 @@ +namespace SHH.CameraSdk; + +/// +/// 海康 HCNetSDK.dll 原生方法封装(静态部分类) +/// 功能:包含设备登录、预览、PTZ控制、异常回调等核心 SDK 接口定义 +/// 注意:所有 API 均直接映射海康原生 DLL 函数,参数顺序与类型需严格匹配官方文档 +/// +public static partial class HikNativeMethods +{ + #region --- 基础配置 (Basic Configuration) --- + + /// + /// HCNetSDK.dll 动态库路径 + /// 说明:确保项目中该路径与实际文件位置一致,否则会导致 DllImport 调用失败 + /// + private const string DllName = "Drivers\\Hikvision\\HCNetSDK.dll"; + + #endregion + + #region --- 结构体定义 (Structures) --- + + /// + /// 设备信息结构体 (NET_DEVICEINFO_V30) + /// 功能:存储设备序列号、通道数、协议类型、能力集等核心信息 + /// 注:登录设备成功后通过 NET_DVR_Login_V30 接口返回 + /// + [StructLayout(LayoutKind.Sequential)] + public struct NET_DEVICEINFO_V30 + { + /// 设备序列号(长度48字节) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)] + public byte[] sSerialNumber; + + /// 报警输入个数 + public byte byAlarmInPortNum; + + /// 报警输出个数 + public byte byAlarmOutPortNum; + + /// 硬盘个数 + public byte byDiskNum; + + /// 设备类型:1-DVR,2-ATM DVR,3-DVS 等 + public byte byDVRType; + + /// 模拟通道个数 + public byte byChanNum; + + /// 起始通道号(目前从1开始) + public byte byStartChan; + + /// 语音通道数 + public byte byAudioChanNum; + + /// 最大数字通道个数(低8位) + public byte byIPChanNum; + + /// 零通道编码个数 + public byte byZeroChanNum; + + /// 主码流传输协议类型:0-私有协议,1-RTSP,2-同时支持两者 + public byte byMainProto; + + /// 子码流传输协议类型:0-私有协议,1-RTSP,2-同时支持两者 + public byte bySubProto; + + /// 基础能力集(位掩码),位与结果为1表示支持对应功能 + /// + /// bySupport & 0x1: 支持智能搜索
+ /// bySupport & 0x2: 支持备份
+ /// bySupport & 0x4: 支持压缩参数能力获取
+ /// bySupport & 0x8: 支持多网卡
+ /// bySupport & 0x10: 支持远程SADP
+ /// bySupport & 0x20: 支持Raid卡功能
+ /// bySupport & 0x40: 支持IPSAN目录查找
+ /// bySupport & 0x80: 支持RTP over RTSP + ///
+ public byte bySupport; + + /// 能力集扩充(位掩码),位与结果为1表示支持对应功能 + /// + /// bySupport1 & 0x1: 支持SNMP v30
+ /// bySupport1 & 0x2: 支持区分回放和下载
+ /// bySupport1 & 0x4: 支持布防优先级
+ /// bySupport1 & 0x8: 智能设备支持布防时间段扩展
+ /// bySupport1 & 0x10: 支持多磁盘数(超过33个)
+ /// bySupport1 & 0x20: 支持RTSP over HTTP
+ /// bySupport1 & 0x80: 支持车牌新报警信息(2012-9-28),且支持NET_DVR_IPPARACFG_V40结构体 + ///
+ public byte bySupport1; + + /// 能力集扩充(位掩码),位与结果为1表示支持对应功能 + /// + /// bySupport2 & 0x1: 解码器支持通过URL取流解码
+ /// bySupport2 & 0x2: 支持FTP V40
+ /// bySupport2 & 0x4: 支持ANR
+ /// bySupport2 & 0x8: 支持CCD的通道参数配置
+ /// bySupport2 & 0x10: 支持布防报警回传信息(仅抓拍机报警,新老报警结构)
+ /// bySupport2 & 0x20: 支持单独获取设备状态子项
+ /// bySupport2 & 0x40: 是码流加密设备 + ///
+ public byte bySupport2; + + /// 设备型号 + public ushort wDevType; + + /// 能力集扩充(位掩码),位与结果为1表示支持对应功能 + /// + /// bySupport3 & 0x1: 支持多码流
+ /// bySupport3 & 0x4: 支持按组配置(通道图像参数、报警输入参数等)
+ /// bySupport3 & 0x8: 支持TCP/UDP/多播预览的延时预览字段
+ /// bySupport3 & 0x10: 支持获取报警主机主要状态(V40)
+ /// bySupport3 & 0x20: 支持通过DDNS域名解析取流 + ///
+ public byte bySupport3; + + /// 多码流支持标识(按位表示) + /// 0-不支持,1-支持;bit1-码流3,bit2-码流4,bit7-主码流,bit8-子码流 + public byte byMultiStreamProto; + + /// 起始数字通道号(0表示无效) + public byte byStartDChan; + + /// 起始数字对讲通道号(0表示无效) + public byte byStartDTalkChan; + + /// 数字通道个数(高8位) + public byte byHighDChanNum; + + /// 能力集扩充(位掩码),位与结果为1表示支持对应功能 + public byte bySupport4; + + /// 支持语种能力(按位表示) + /// + /// byLanguageType = 0: 老设备
+ /// byLanguageType & 0x1: 支持中文
+ /// byLanguageType & 0x2: 支持英文 + ///
+ public byte byLanguageType; + + /// 音频输入通道数 + public byte byVoiceInChanNum; + + /// 音频输入起始通道号 + public byte byStartVoiceInChanNo; + + /// 保留字段(必须置0) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public byte[] byRes3; + + /// AES算法加密/解密能力 + public byte byMirrorCap; + + /// 起始数字通道号(扩展) + public ushort wStartIPChanNo; + + /// 保留字段(必须置0) + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)] + public byte[] byRes; + } + + /// + /// 预览参数结构体 (NET_DVR_PREVIEWINFO) + /// 功能:配置实时预览的通道、码流类型、连接方式等参数 + /// 注:用于 NET_DVR_RealPlay_V40 接口的输入参数 + /// + [StructLayoutAttribute(LayoutKind.Sequential)] + public struct NET_DVR_PREVIEWINFO + { + /// 通道号(模拟通道从1开始) + public Int32 lChannel; + + /// 码流类型:0-主码流,1-子码流,2-码流3,3-码流4,以此类推 + public uint dwStreamType; + + /// 连接方式:0-TCP,1-UDP,2-多播,3-RTP,4-RTP/RTSP,5-RTSP/HTTP + public uint dwLinkMode; + + /// 播放窗口句柄 + /// IntPtr.Zero 表示不让 SDK 直接渲染,仅获取原始流数据 + public IntPtr hPlayWnd; + + /// 取流模式:0-非阻塞,1-阻塞(阻塞模式超时5秒返回) + /// 阻塞模式不适合轮询取流操作 + public bool bBlocked; + + /// 是否启用回放录像:0-不启用,1-启用 + public bool bPassbackRecord; + + /// 预览模式:0-正常预览,1-延迟预览 + public byte byPreviewMode; + + /// 流ID(lChannel为0xffffffff时启用,长度32字节) + [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = STREAM_ID_LEN, ArraySubType = UnmanagedType.I1)] + public byte[] byStreamID; + + /// 应用层协议类型:0-私有协议,1-RTSP协议 + public byte byProtoType; + + /// 保留字段(必须置0) + public byte byRes1; + + /// 码流编解码类型:0-通用编码数据,1-热成像原始数据(含温度加密信息) + public byte byVideoCodingType; + + /// 播放库最大缓冲帧数(范围1-50,0表示默认1帧) + public uint dwDisplayBufNum; + + /// NPQ模式:0-直连,1-过流媒体 + public byte byNPQMode; + + /// 保留字段(必须置0) + [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 215, ArraySubType = UnmanagedType.I1)] + public byte[] byRes; + } + + /// + /// 时间结构体 (NET_DVR_TIME) + /// [Fix Bug P: 结构体炸弹] 修复结构体对齐问题,避免栈内存覆盖导致随机崩溃 + /// + /// + /// 原问题:ushort/byte 混合定义导致结构体总大小不足16字节,SDK写入时覆盖栈变量
+ /// 修复方案:使用 Pack=4 对齐,成员类型统一为 uint(4字节),与 C++ DWORD 匹配 + ///
+ [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) --- + + /// 流ID长度(32字节) + public const int STREAM_ID_LEN = 32; + + /// 数据类型常量:系统头数据 + public const int NET_DVR_SYSHEAD = 1; + + /// 数据类型常量:视频流数据(H.264/H.265) + public const int NET_DVR_STREAMDATA = 2; + + /// 数据类型常量:音频数据 + public const int NET_DVR_AUDIOSTREAMDATA = 3; + + /// 命令常量:获取时间配置 + public const uint NET_DVR_GET_TIMECFG = 118; + + #endregion + + #region --- PTZ 控制相关 (PTZ Control) --- + + /// 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; // 光圈缩小 + + /// PTZ命令常量:方向控制 + 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; // 自动扫描 + + /// PTZ命令常量:辅助功能 + public const uint LIGHT_PWRON = 2; // 接通灯光电源 + public const uint WIPER_PWRON = 3; // 接通雨刷开关 + + #endregion + + #region --- 异常回调相关 (Exception Callback) --- + + /// 异常类型常量 + 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; // 预览时重连成功 + + /// + /// 异常消息回调委托 + /// 功能:SDK 发生异常时触发,返回异常类型、用户ID、相关句柄等信息 + /// + /// 异常类型(对应 EXCEPTION_XXX 常量) + /// 用户ID(NET_DVR_Login_V30 返回值) + /// 异常关联句柄(预览句柄/报警句柄等) + /// 用户自定义数据指针 + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void EXCEPTION_CALLBACK(uint dwType, int lUserID, int lHandle, IntPtr pUser); + + #endregion + + #region --- 预览回调相关 (Preview Callback) --- + + /// + /// 预览数据回调委托 + /// 功能:实时预览时触发,返回原始流数据(系统头/视频流/音频流) + /// + /// 预览句柄(NET_DVR_RealPlay_V40 返回值) + /// 数据类型(对应 NET_DVR_XXX 数据类型常量) + /// 数据缓冲区指针 + /// 缓冲区大小(字节) + /// 用户自定义数据指针 + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void REALDATACALLBACK(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser); + + #endregion + + #region --- SDK 基础接口 (Basic SDK Interfaces) --- + + /// + /// 初始化 SDK + /// 功能:调用所有其他 SDK 接口的前提,必须先初始化再使用 + /// + /// 初始化成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_Init(); + + /// + /// 释放 SDK 资源 + /// 功能:程序退出前调用,释放 SDK 占用的非托管资源(网络连接、内存等) + /// + /// 释放成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_Cleanup(); + + /// + /// 获取最后一次操作的错误码 + /// 功能:API 调用失败后,通过此接口获取具体错误原因 + /// + /// 错误码(需结合海康官方文档查询含义) + [DllImport(DllName)] + public static extern uint NET_DVR_GetLastError(); + + /// + /// 设置网络连接超时时间和连接尝试次数 + /// + /// 超时时间(毫秒),推荐 3000ms + /// 连接尝试次数,推荐 1 次 + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes); + + /// + /// 设置自动重连功能 + /// + /// 重连间隔(毫秒),推荐 10000ms + /// 是否启用重连:0-禁用,1-启用 + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_SetReconnect(uint dwInterval, bool bEnableRecon); + + #endregion + + #region --- 设备登录/登出接口 (Device Login/Logout) --- + + /// + /// 用户注册设备(登录) + /// 功能:建立与设备的连接,获取用户ID(后续接口调用的核心标识) + /// + /// 设备IP地址 + /// 设备端口号(海康默认8000) + /// 登录用户名(默认 admin) + /// 登录密码 + /// 输出参数:设备信息结构体 + /// 登录成功返回用户ID(非负整数),失败返回 -1 + [DllImport(DllName)] + public static extern int NET_DVR_Login_V30(string sDVRIP, Int32 wDVRPort, string sUserName, string sPassword, ref NET_DEVICEINFO_V30 lpDeviceInfo); + + /// + /// 用户注销(登出) + /// 功能:断开与设备的连接,释放用户ID关联的资源 + /// + /// 用户ID(NET_DVR_Login_V30 返回值) + /// 登出成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_Logout(int lUserID); + + #endregion + + #region --- 预览控制接口 (Preview Control) --- + + /// + /// 实时预览(V40版本,支持回调) + /// 功能:启动设备实时取流,通过回调获取原始流数据 + /// + /// 用户ID(NET_DVR_Login_V30 返回值) + /// 预览参数结构体 + /// 流数据回调函数 + /// 用户自定义数据指针 + /// 预览成功返回预览句柄(非负整数),失败返回 -1 + [DllImport(DllName)] + public static extern int NET_DVR_RealPlay_V40(int lUserID, ref NET_DVR_PREVIEWINFO lpPreviewInfo, REALDATACALLBACK fRealDataCallBack_V30, IntPtr pUser); + + /// + /// 停止预览 + /// 功能:停止实时取流,释放预览句柄关联的资源 + /// + /// 预览句柄(NET_DVR_RealPlay_V40 返回值) + /// 停止成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_StopRealPlay(int lRealHandle); + + /// + /// 强制生成I帧 + /// 功能:主动触发设备发送I帧,优化视频流解码延时 + /// + /// 用户ID + /// 通道号 + /// 操作成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_MakeKeyFrame(int lUserID, int lChannel); + + #endregion + + #region --- PTZ 控制接口 (PTZ Control Interfaces) --- + + /// + /// 云台控制(带速度) + /// 功能:控制云台旋转、镜头缩放、光圈调节等操作 + /// + /// 用户ID + /// 通道号 + /// PTZ控制命令(对应 PTZ 命令常量) + /// 启停标识:0-开始,1-停止 + /// 控制速度(1-7,数值越大速度越快) + /// 操作成功返回 true,失败返回 false + [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) --- + + /// + /// 设置连接超时时间和重连策略(兼容旧版本) + /// + /// 重连间隔(毫秒),建议 3000 + /// 是否启用重连:1-启用,0-禁用 + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_SetReconnect(uint dwInterval, int bEnableRecon); + + /// + /// 注册异常、重连等消息的回调函数 + /// 功能:绑定异常回调委托,接收 SDK 层面的异常通知 + /// + /// 消息类型(0 表示所有消息) + /// 窗口句柄(可为 IntPtr.Zero) + /// 异常回调函数委托 + /// 用户自定义数据指针 + /// 注册成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool NET_DVR_SetExceptionCallBack_V30(uint nMessage, IntPtr hWnd, EXCEPTION_CALLBACK fExceptionCallBack, IntPtr pUser); + + #endregion + + #region --- 通用配置接口 (General Configuration Interfaces) --- + + /// + /// 获取设备配置 + /// 功能:通用接口,根据命令号获取设备特定配置(如时间配置、通道参数等) + /// + /// 用户ID + /// 配置命令号(如 NET_DVR_GET_TIMECFG) + /// 通道号(-1 表示设备级配置) + /// 输出缓冲区指针(存储配置数据) + /// 输出缓冲区大小(字节) + /// 输出参数:实际返回的数据大小(字节) + /// 获取成功返回 true,失败返回 false + [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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs b/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs new file mode 100644 index 0000000..3cde241 --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/HikPlayMethods.cs @@ -0,0 +1,347 @@ +using System; +using System.Runtime.InteropServices; +namespace SHH.CameraSdk; + +/// +/// 海康播放库 PlayCtrl.dll 的封装 +/// 完全参考官方 WinPlayCtrl.cs 定义,提供解码、播放、端口管理等核心能力 +/// +public static class HikPlayMethods +{ + #region --- 基础配置 (Basic Configuration) --- + + /// + /// PlayCtrl.dll 动态库路径 + /// 注意:请确保项目中该路径与实际文件位置一致,否则会导致 DllImport 失败 + /// + private const string DllName = @"Drivers\\Hikvision\\PlayCtrl.dll"; + + #endregion + + #region --- 常量定义 (Constants) --- + + /// 最大支持的通道数 + 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) --- + + /// + /// 帧信息结构体:存储解码后帧的宽高、帧率、序号等关键信息 + /// + [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; // 帧序号 + } + + /// + /// 帧位置结构体:存储文件流中帧的位置、时间等信息 + /// + [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; // 错误帧大小 + } + + /// + /// 帧类型结构体:存储帧数据缓冲区、大小等信息 + /// + [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) --- + + /// + /// 解码回调委托 (对应官方 DECCBFUN) + /// 功能:解码完成后触发,返回解码后的帧数据 + /// + /// 播放端口号 + /// 解码后数据缓冲区指针 + /// 缓冲区大小(字节) + /// 帧信息结构体(引用传递) + /// 保留字段1(置0) + /// 保留字段2(置0) + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void DECCBFUN( + int nPort, + IntPtr pBuf, + int nSize, + ref FRAME_INFO pFrameInfo, + int nReserved1, + int nReserved2 + ); + + /// + /// 显示回调委托 (对应官方 DISPLAYCBFUN) + /// 功能:帧数据准备显示时触发,用于自定义渲染逻辑 + /// + /// 播放端口号 + /// 显示数据缓冲区指针 + /// 缓冲区大小(字节) + /// 显示宽度(像素) + /// 显示高度(像素) + /// 时间戳 + /// 数据类型(对应 T_XXX 常量) + /// 保留字段(置0) + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void DISPLAYCBFUN( + int nPort, + IntPtr pBuf, + int nSize, + int nWidth, + int nHeight, + int nStamp, + int nType, + int nReserved + ); + + /// + /// 文件结束回调委托 + /// 功能:文件流播放完成时触发 + /// + /// 播放端口号 + /// 用户自定义数据指针 + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void FILEENDCALLBACK(int nPort, IntPtr pUser); + + #endregion + + #region --- API 导入 (Dll Imports) --- + + /// + /// 获取闲置的播放端口。 + /// [警告] 此函数非线程安全,且端口资源有限(最多500个)。 + /// 高并发场景下必须加全局锁,防止端口分配冲突。 + /// + /// 输出参数:获取到的闲置端口号(输出-1表示失败) + /// 获取成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_GetPort(ref int nPort); + + /// + /// 释放播放端口 + /// 功能:不再使用端口时调用,避免端口资源泄漏 + /// + /// 要释放的端口号 + /// 释放成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_FreePort(int nPort); + + /// + /// 打开流 + /// 功能:初始化端口的流缓冲区,准备接收流数据 + /// + /// 播放端口号 + /// 文件头数据缓冲区指针(实时流可为空) + /// 缓冲区大小(字节) + /// 流缓冲区池大小(字节) + /// 打开成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_OpenStream(int nPort, IntPtr pFileHeadBuf, uint nSize, uint nBufPoolSize); + + /// + /// 关闭流 + /// 功能:停止接收流数据,释放流缓冲区资源 + /// + /// 播放端口号 + /// 关闭成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_CloseStream(int nPort); + + /// + /// 开始播放 + /// 功能:启动解码和显示流程,绑定到指定窗口句柄 + /// + /// 播放端口号 + /// 显示窗口句柄(IntPtr.Zero 表示不绑定窗口) + /// 启动成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_Play(int nPort, IntPtr hWnd); + + /// + /// 停止播放 + /// 功能:停止解码和显示,释放播放相关资源 + /// + /// 播放端口号 + /// 停止成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_Stop(int nPort); + + /// + /// 输入流数据 + /// 功能:向播放端口推送原始流数据,供解码使用 + /// + /// 播放端口号 + /// 流数据缓冲区指针 + /// 数据大小(字节) + /// 输入成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_InputData(int nPort, IntPtr pBuf, uint nSize); + + /// + /// 设置解码回调 (Ex版本) + /// 功能:绑定解码完成后的回调函数,用于获取解码后的帧数据 + /// + /// 播放端口号 + /// 解码回调函数委托 + /// 目标缓冲区指针(可为空) + /// 目标缓冲区大小(字节) + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_SetDecCallBackEx(int nPort, DECCBFUN DecCBFun, IntPtr pDest, int nDestSize); + + /// + /// 设置流打开模式 + /// 功能:指定端口接收的流类型(实时流/文件流) + /// + /// 播放端口号 + /// 流模式(对应 STREAME_XXX 常量) + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_SetStreamOpenMode(int nPort, uint nMode); + + /// + /// 设置显示缓冲区数量 + /// 功能:调整显示缓冲帧数,平衡流畅度与延迟 + /// + /// 播放端口号 + /// 缓冲帧数(范围:MIN_DIS_FRAMES ~ MAX_DIS_FRAMES) + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_SetDisplayBuf(int nPort, uint nNum); + + /// + /// 设置叠加模式 + /// 功能:配置图像叠加方式及透明色 + /// + /// 播放端口号 + /// 是否启用叠加(1=启用,0=禁用) + /// 透明色键值 + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_SetOverlayMode(int nPort, int bOverlay, uint colorKey); + + /// + /// 获取最后一次错误码 + /// 功能:API 调用失败后,获取具体错误原因(对应 PLAYM4_XXX 错误常量) + /// + /// 播放端口号 + /// 错误码(0 表示无错误) + [DllImport(DllName)] + public static extern uint PlayM4_GetLastError(int nPort); + + /// + /// 设置视频解码模式 + /// 功能:切换硬解码/软解码模式(补充:用于硬件加速优化) + /// + /// 播放端口号 + /// 解码模式(0=软解码,1=硬解码,具体值参考官方文档) + /// 设置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_SetDecVideoMode(int nPort, int nMode); + + /// + /// 获取源缓冲区剩余空间 + /// 功能:查询端口流缓冲区的剩余可用空间(字节) + /// + /// 播放端口号 + /// 剩余空间大小(字节) + [DllImport(DllName)] + public static extern uint PlayM4_GetSourceBufferRemain(int nPort); + + /// + /// 重置源缓冲区 + /// 功能:清空端口流缓冲区中的未解码数据 + /// + /// 播放端口号 + /// 重置成功返回 true,失败返回 false + [DllImport(DllName)] + public static extern bool PlayM4_ResetSourceBuffer(int nPort); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs b/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs new file mode 100644 index 0000000..84a43ab --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs @@ -0,0 +1,125 @@ +namespace SHH.CameraSdk; + +/// +/// [驱动支持层] 海康 SDK 全局资源管理器 (V3.3.1 修复版) +/// 核心修复: +/// 1. [Bug S] 引用计数保护:增加下溢检测,防止异常销毁流程导致的计数器错乱。 +/// 2. [Bug C] 禁用内部重连:确保 SDK 不会背着上层偷偷重连,彻底消除僵尸连接。 +/// +public static class HikSdkManager +{ + #region --- 全局状态与锁 (Global States & Locks) --- + + /// + /// 全局引用计数器。 + /// 只有当计数从 0 变 1 时才进行物理初始化,从 1 变 0 时才物理卸载。 + /// + private static int _referenceCount = 0; + + /// + /// 静态同步锁。 + /// 用于保护 _referenceCount 的原子操作,防止多线程并发 Start/Stop 时导致的初始化冲突。 + /// + private static readonly object _lock = new(); + + /// + /// 播放库预热状态标记。 + /// 用于避免重复执行硬件探测(首次预热后后续直接返回)。 + /// + private static bool _isWarmedUp = false; + + #endregion + + #region --- SDK 初始化与卸载 (SDK Initialization & Uninstallation) --- + + /// + /// 全局初始化海康 SDK 环境。 + /// 此方法是幂等的,内部会自动增加引用计数,支持多线程并发调用。 + /// + /// 初始化成功返回 true;若 SDK 核心组件(HCNetSDK.dll)加载失败则返回 false。 + 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; + } + } + + /// + /// 全局卸载海康 SDK 环境。 + /// 当所有相机实例都停止并释放后(引用计数归 0),会真正释放非托管资源。 + /// + 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) --- + + /// + /// [核心策略] 强制冷启动诱发 + /// 职责:在系统真正取流前,强行触发一次 PlayCtrl.dll 的硬件探测(声卡、显卡、DirectDraw) + /// 目的:规避首次取流时的 12-18 秒延迟,提前完成硬件初始化 + /// + 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 +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs new file mode 100644 index 0000000..d93395f --- /dev/null +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -0,0 +1,386 @@ +using OpenCvSharp; + +namespace SHH.CameraSdk; + +/// +/// [海康驱动] 工业级视频源实现 V3.3.1 (极高并发修正版) +/// 核心职责:负责海康威视设备 (SDK) 的物理连接、取流、解码与资源管理。 +/// 核心修复记录: +/// 1. [Bug X] 异步竞争:引入 Epoch 世代验证,防止超时取消后的幽灵任务覆盖新连接。 +/// 2. [Bug Y] 内存踩踏:解码回调加锁,防止多核环境下共享 Mat 被并发读写引发 AV 异常。 +/// 3. [Bug α] 端口抢占:PlayM4_GetPort 全局加锁,防止高并发启动时的播放端口串位。 +/// 4. [Bug H/W/T/E] 继承之前的路由分发、幽灵句柄、零 GC、异步启动等修复。 +/// +public class HikVideoSource : BaseVideoSource +{ + #region --- 静态资源 (Global Resources) --- + + // 静态路由表 (Fix Bug H: 友军误伤) + // 作用:将海康 SDK 的全局回调(仅带 UserID)精准路由到具体的 HikVideoSource 实例 + private static readonly ConcurrentDictionary _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) --- + + /// + /// 初始化海康视频源实例 + /// + /// 视频源基础配置(含设备IP、账号、码流类型等) + public HikVideoSource(VideoSourceConfig config) : base(config) { } + + #endregion + + #region --- 核心生命周期 (Core Lifecycle) --- + + /// + /// [异步启动核心] (含 Bug E/X 修复) + /// 流程:SDK环境初始化 → 注册全局回调 → 物理登录设备 → 路由注册 → 开启网络预览 + /// + /// 取消令牌:用于终止超时或中断的启动流程 + 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); + } + + /// + /// 异步停止设备:终止取流、解码,释放所有资源 + /// + protected override async Task OnStopAsync() + { + // [Fix Bug X] 停止时递增世代:立即使所有正在进行的启动任务失效 + Interlocked.Increment(ref _connectionEpoch); + + // 在后台线程执行同步清理逻辑,避免阻塞调用线程 + await Task.Run(() => CleanupSync()); + } + + /// + /// [同步清理核心] (含 Bug Y 锁保护) + /// 职责:按“停止取流→释放解码资源→释放内存→注销登录”顺序销毁,防止非托管崩溃 + /// + 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) --- + + /// + /// 开启网络取流:配置预览参数,绑定流数据回调 + /// + /// 取流开启成功返回 true,失败返回 false + 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; + } + + /// + /// 网络流数据回调 (RealDataCallBack) + /// 职责:接收 SDK 原始流数据,系统头用于初始化播放库,流数据用于解码 + /// + 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) --- + + /// + /// 解码回调 (DecCallBack) + /// 职责:接收解码后的 YUV 数据,转码为 BGR 格式,通过帧池复用内存并分发 + /// + 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) --- + + /// + /// 全局异常回调处理 + /// 职责:将 SDK 全局异常(仅带 UserID)路由到对应的 HikVideoSource 实例 + /// + 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) --- + + /// + /// 占位实现:暂未实现设备元数据获取逻辑 + /// 注:实际场景需补充,用于获取设备型号、通道能力等信息 + /// + protected override Task OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata()); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs new file mode 100644 index 0000000..c44915a --- /dev/null +++ b/SHH.CameraSdk/Program.cs @@ -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 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] 分析一帧..."); + } + }); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/SHH.CameraSdk.csproj b/SHH.CameraSdk/SHH.CameraSdk.csproj new file mode 100644 index 0000000..acbfbe2 --- /dev/null +++ b/SHH.CameraSdk/SHH.CameraSdk.csproj @@ -0,0 +1,33 @@ + + + + Exe + net8.0 + enable + enable + AnyCPU + + + + + + + + + + + + + + + + + + + + + + + + +