diff --git a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs index 4e9eee9..04bee6c 100644 --- a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs +++ b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs @@ -43,6 +43,9 @@ public class VideoSourceConfig /// 默认码流类型(0 = 主码流(高清),1 = 子码流(低带宽)) public int StreamType { get; set; } = 0; + /// Rtsp 播放路径 + public string RtspPath { get; set; } = string.Empty; + /// 传输协议(TCP/UDP/Multicast,默认 TCP 保证可靠性) [JsonConverter(typeof(JsonStringEnumConverter))] public TransportProtocol Transport { get; set; } = TransportProtocol.Tcp; @@ -132,6 +135,7 @@ public class VideoSourceConfig Password = this.Password, RenderHandle = this.RenderHandle, ChannelIndex = this.ChannelIndex, + RtspPath = this.RtspPath, StreamType = this.StreamType, Transport = this.Transport, ConnectionTimeoutMs = this.ConnectionTimeoutMs diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs new file mode 100644 index 0000000..cac6691 --- /dev/null +++ b/SHH.CameraSdk/Controllers/CamerasController.cs @@ -0,0 +1,283 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SHH.CameraSdk; + +[ApiController] +[Route("api/[controller]")] +public class CamerasController : ControllerBase +{ + private readonly CameraManager _manager; + + public CamerasController(CameraManager manager) + { + _manager = manager; + } + + // ========================================================================== + // 区域 A: 设备全生命周期管理 (CRUD) + // ========================================================================== + + /// + /// 1. 获取所有设备清单 + /// + [HttpGet] + public IActionResult GetAll() + { + var devices = _manager.GetAllDevices().Select(d => new + { + d.Id, + d.Config.IpAddress, + d.Config.Name, + Status = d.Status.ToString(), + d.RealFps, + d.TotalFrames + }); + return Ok(devices); + } + + /// + /// 2. 新增设备 (写入配置并初始化) + /// + [HttpPost] + public IActionResult Add([FromBody] CameraConfigDto dto) + { + if (_manager.GetDevice(dto.Id) != null) + return Conflict($"设备ID {dto.Id} 已存在"); + + // DTO 转 Config (实际项目中建议用 AutoMapper) + var config = MapToConfig(dto); + + _manager.AddDevice(config); // 添加到内存池 + // 注意:此时 IsRunning 默认为 false,等待手动 Start 或 API 控制 + + return CreatedAtAction(nameof(GetAll), new { id = dto.Id }, dto); + } + + /// + /// 3. 编辑设备 (自动识别冷热更新) + /// + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] CameraConfigDto dto) + { + try + { + if (id != dto.Id) return BadRequest("ID 不匹配"); + + // 调用 Manager 的智能更新逻辑 (之前实现的 UpdateDeviceConfigAsync) + await _manager.UpdateDeviceConfigAsync(id, MapToUpdateDto(dto)); + + return Ok(new { Success = true, Message = "配置已更新" }); + } + catch (KeyNotFoundException) { return NotFound(); } + catch (System.Exception ex) { return StatusCode(500, ex.Message); } + } + + /// + /// 4. 删除设备 (销毁连接) + /// + [HttpDelete("{id}")] + public async Task Remove(long id) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + await device.StopAsync(); // 停流 + _manager.RemoveDevice(id); // 从池中移除 + + return Ok($"设备 {id} 已移除"); + } + + // ========================================================================== + // 区域 B: 多进程流控订阅 (Subscription Strategy) + // ========================================================================== + + /// + /// 5. 注册/更新进程的流需求 (A/B/C/D 场景核心) + /// + /// + /// 示例场景: + /// - 主进程配置(B): { "appId": "Main_Config", "displayFps": 25, "analysisFps": 0 } + /// - AI进程(C): { "appId": "AI_Core", "displayFps": 0, "analysisFps": 5 } + /// + [HttpPost("{id}/subscriptions")] + public IActionResult UpdateSubscription(long id, [FromBody] SubscriptionDto sub) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + // 逻辑转换:将 "显示帧" 和 "分析帧" 映射到底层控制器的注册表 + + // 1. 处理显示需求 + string displayKey = $"{sub.AppId}_Display"; + if (sub.DisplayFps > 0) + { + // 告诉控制器:这个 App 需要 X 帧用于显示 + device.Controller.Register(displayKey, sub.DisplayFps); + } + else + { + // 如果不需要,移除注册 + device.Controller.Unregister(displayKey); + } + + // 2. 处理分析需求 + string analysisKey = $"{sub.AppId}_Analysis"; + if (sub.AnalysisFps > 0) + { + // 告诉控制器:这个 App 需要 Y 帧用于分析 + device.Controller.Register(analysisKey, sub.AnalysisFps); + } + else + { + device.Controller.Unregister(analysisKey); + } + + // 运维审计 + device.AddAuditLog($"更新订阅策略 [{sub.AppId}]: Display={sub.DisplayFps}, Analysis={sub.AnalysisFps}"); + + return Ok(new { Message = "订阅策略已更新", DeviceId = id }); + } + + // ========================================================================== + // 区域 C: 句柄动态绑定 (Handle Binding) + // ========================================================================== + + /// + /// 6. 绑定显示窗口 (对应 A进程-句柄场景) + /// + [HttpPost("{id}/bind-handle")] + public IActionResult BindHandle(long id, [FromBody] BindHandleDto dto) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + // 构造动态选项,应用句柄 + var options = new DynamicStreamOptions + { + RenderHandle = (System.IntPtr)dto.Handle + }; + + device.ApplyOptions(options); // 触发驱动层的 OnApplyOptions + + device.AddAuditLog($"绑定新句柄: {dto.Handle} ({dto.Purpose})"); + + return Ok(new { Success = true }); + } + + // ========================================================================== + // 区域 D: 设备运行控制 + // ========================================================================== + + /// + /// 手动控制设备运行状态 (开关机) + /// + [HttpPost("{id}/power")] + public async Task TogglePower(long id, [FromQuery] bool enabled) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + // 1. 更新运行意图 + device.IsRunning = enabled; + + // 2. 审计与执行 + if (enabled) + { + device.AddAuditLog("用户指令:手动开启设备"); + // 异步启动,Coordinator 也会在下个周期辅助检查 + _ = device.StartAsync(); + } + else + { + device.AddAuditLog("用户指令:手动关闭设备"); + await device.StopAsync(); + } + + return Ok(new { DeviceId = id, IsRunning = enabled }); + } + + /// + /// 热应用动态参数 (如切换码流) + /// + [HttpPost("{id}/options")] + public IActionResult ApplyOptions(long id, [FromBody] DynamicStreamOptions options) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + // 1. 如果涉及码流切换,先同步更新配置对象 + if (options.StreamType.HasValue) + { + var newConfig = device.Config.DeepCopy(); + newConfig.StreamType = options.StreamType.Value; + device.UpdateConfig(newConfig); + } + + // 2. 应用到驱动层(触发热切换逻辑) + device.ApplyOptions(options); + + return Ok(new { Message = "动态参数已发送", DeviceId = id }); + } + + // ========================================================================== + // 辅助方法 (Mapping) + // ========================================================================== + private VideoSourceConfig MapToConfig(CameraConfigDto dto) + { + return new VideoSourceConfig + { + Id = dto.Id, + Name = dto.Name, + Brand = (DeviceBrand)dto.Brand, + IpAddress = dto.IpAddress, + Port = dto.Port, + Username = dto.Username, + Password = dto.Password, + ChannelIndex = dto.ChannelIndex, + StreamType = dto.StreamType, + RtspPath = dto.RtspPath + }; + } + + /// + /// [辅助方法] 将全量配置 DTO 转换为更新 DTO + /// + private DeviceUpdateDto MapToUpdateDto(CameraConfigDto dto) + { + return new DeviceUpdateDto + { + // ========================================== + // 1. 冷更新参数 (基础连接信息) + // ========================================== + IpAddress = dto.IpAddress, + Port = dto.Port, + Username = dto.Username, + Password = dto.Password, + ChannelIndex = dto.ChannelIndex, + Brand = dto.Brand, + RtspPath = dto.RtspPath, + MainboardIp = dto.MainboardIp, + MainboardPort = dto.MainboardPort, + + // ========================================== + // 2. 热更新参数 (运行时属性) + // ========================================== + Name = dto.Name, + Location = dto.Location, + StreamType = dto.StreamType, + + // 注意:通常句柄是通过 bind-handle 接口单独绑定的, + // 但如果 ConfigDto 里包含了上次保存的句柄,也可以映射 + // RenderHandle = dto.RenderHandle, + + // ========================================== + // 3. 图像处理参数 + // ========================================== + AllowCompress = dto.AllowCompress, + AllowExpand = dto.AllowExpand, + TargetResolution = dto.TargetResolution, + EnhanceImage = dto.EnhanceImage, + UseGrayscale = dto.UseGrayscale + }; + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs b/SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs new file mode 100644 index 0000000..0e6cf84 --- /dev/null +++ b/SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace SHH.CameraSdk; + +/// +/// 句柄绑定 DTO +/// 用于前端向后端传递窗口渲染句柄,实现视频流的硬件解码渲染 +/// +public class BindHandleDto +{ + /// + /// 窗口句柄 (IntPtr 转换为 long 类型传输,避免跨平台序列化问题) + /// + [Required(ErrorMessage = "渲染窗口句柄不能为空")] + [Range(1, long.MaxValue, ErrorMessage = "句柄必须为有效的非负整数")] + public long Handle { get; set; } + + /// + /// 用途描述 (用于审计日志,如 "Main_Preview"、"AI_Analysis_Window") + /// + [MaxLength(64, ErrorMessage = "用途描述长度不能超过 64 个字符")] + public string Purpose { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs b/SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs new file mode 100644 index 0000000..f661119 --- /dev/null +++ b/SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs @@ -0,0 +1,123 @@ +using System.ComponentModel.DataAnnotations; + +namespace SHH.CameraSdk; + +// ============================================================================== +// 1. 物理与运行配置 DTO (对应 CRUD 操作) +// 用于设备新增/全量配置查询,包含基础身份、连接信息、运行参数等全量字段 +// ============================================================================== +public class CameraConfigDto +{ + // --- 基础身份 (Identity) --- + /// + /// 设备唯一标识 + /// + [Required(ErrorMessage = "设备ID不能为空")] + [Range(1, long.MaxValue, ErrorMessage = "设备ID必须为正整数")] + public long Id { get; set; } + + /// + /// 设备友好名称 + /// + [MaxLength(64, ErrorMessage = "设备名称长度不能超过64个字符")] + public string Name { get; set; } = string.Empty; + + /// + /// 摄像头品牌类型 (0:HikVision, 1:Dahua, 2:RTSP...) + /// + [Range(0, 10, ErrorMessage = "品牌类型值必须在0-10范围内")] + public int Brand { get; set; } + + /// + /// 设备安装位置描述 + /// + [MaxLength(128, ErrorMessage = "安装位置长度不能超过128个字符")] + public string Location { get; set; } = string.Empty; + + // --- 核心连接 (Connectivity) - 修改此类参数触发冷重启 --- + /// + /// 摄像头IP地址 + /// + [Required(ErrorMessage = "IP地址不能为空")] + [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$", + ErrorMessage = "请输入合法的IPv4地址")] + public string IpAddress { get; set; } = string.Empty; + + /// + /// SDK端口 (如海康默认8000) + /// + [Range(1, 65535, ErrorMessage = "端口号必须在1-65535范围内")] + public ushort Port { get; set; } = 8000; + + /// + /// 登录用户名 + /// + [MaxLength(32, ErrorMessage = "用户名长度不能超过32个字符")] + public string Username { get; set; } = string.Empty; + + /// + /// 登录密码 + /// + [MaxLength(64, ErrorMessage = "密码长度不能超过64个字符")] + public string Password { get; set; } = string.Empty; + + /// + /// 通道号 (通常为1) + /// + [Range(1, 32, ErrorMessage = "通道号必须在1-32范围内")] + public int ChannelIndex { get; set; } = 1; + + /// + /// RTSP流路径 (备用或非SDK模式使用) + /// + [MaxLength(256, ErrorMessage = "RTSP地址长度不能超过256个字符")] + public string RtspPath { get; set; } = string.Empty; + + // --- 主板关联信息 (Metadata) --- + /// + /// 关联主板IP地址 + /// + [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)?$", + ErrorMessage = "请输入合法的IPv4地址")] + public string MainboardIp { get; set; } = string.Empty; + + /// + /// 关联主板端口 + /// + [Range(1, 65535, ErrorMessage = "主板端口号必须在1-65535范围内")] + public int MainboardPort { get; set; } = 80; + + // --- 运行时参数 (Runtime Options) - 支持热更新 --- + /// + /// 码流类型 (0:主码流, 1:子码流) + /// + [Range(0, 1, ErrorMessage = "码流类型只能是0(主码流)或1(子码流)")] + public int StreamType { get; set; } = 0; + + /// + /// 是否使用灰度图 (用于AI分析场景加速) + /// + public bool UseGrayscale { get; set; } = false; + + /// + /// 是否启用图像增强 (去噪/锐化等) + /// + public bool EnhanceImage { get; set; } = true; + + // --- 画面变换 (Transform) - 支持热更新 --- + /// + /// 是否允许图像压缩 (降低带宽占用) + /// + public bool AllowCompress { get; set; } = true; + + /// + /// 是否允许图像放大 (提升渲染质量) + /// + public bool AllowExpand { get; set; } = false; + + /// + /// 目标分辨率 (格式如 1920x1080,空则保持原图) + /// + [RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")] + public string TargetResolution { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs b/SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs new file mode 100644 index 0000000..fb23efc --- /dev/null +++ b/SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs @@ -0,0 +1,96 @@ +using System.ComponentModel.DataAnnotations; + +namespace SHH.CameraSdk; + +/// +/// 设备配置更新传输对象 +/// 用于接收前端的编辑请求,支持部分更新(字段为 null 表示不修改) +/// 自动区分 冷更新参数(需重启连接)和 热更新参数(无感生效) +/// +public class DeviceUpdateDto +{ + // ============================================================================== + // 1. 冷更新参数 (Cold Update) + // 修改此类参数涉及物理连接变更,后端会自动执行 "Stop -> Update -> Start" 流程 + // ============================================================================== + + /// 摄像头IP地址 + [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$", + ErrorMessage = "请输入合法的IPv4地址")] + public string? IpAddress { get; set; } + + /// SDK端口 (如海康 8000) + [Range(1, 65535, ErrorMessage = "端口号必须在 1-65535 范围内")] + public ushort? Port { get; set; } + + /// 登录用户名 + [MaxLength(32, ErrorMessage = "用户名长度不能超过 32 个字符")] + public string? Username { get; set; } + + /// 登录密码 + [MaxLength(64, ErrorMessage = "密码长度不能超过 64 个字符")] + public string? Password { get; set; } + + /// 通道号 (默认 1) + [Range(1, 32, ErrorMessage = "通道号必须在 1-32 范围内")] + public int? ChannelIndex { get; set; } + + /// 摄像头品牌类型 (0:Hik, 1:Dahua...) + [Range(0, 10, ErrorMessage = "品牌类型值必须在 0-10 范围内")] + public int? Brand { get; set; } + + /// RTSP流地址 (非SDK模式下使用) + [MaxLength(256, ErrorMessage = "RTSP地址长度不能超过 256 个字符")] + public string? RtspPath { get; set; } + + /// 关联的主板IP (用于联动控制) + [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)?$", + ErrorMessage = "请输入合法的IPv4地址")] + public string? MainboardIp { get; set; } + + /// 关联的主板端口 + [Range(1, 65535, ErrorMessage = "主板端口号必须在 1-65535 范围内")] + public int? MainboardPort { get; set; } + + // ============================================================================== + // 2. 热更新参数 (Hot Update) + // 修改此类参数仅刷新运行时状态,不中断物理连接,无感生效 + // ============================================================================== + + /// 设备名称/别名 + [MaxLength(64, ErrorMessage = "设备名称长度不能超过 64 个字符")] + public string? Name { get; set; } + + /// 安装位置描述 + [MaxLength(128, ErrorMessage = "安装位置长度不能超过 128 个字符")] + public string? Location { get; set; } + + /// 码流类型 (0:主码流, 1:子码流) + [Range(0, 1, ErrorMessage = "码流类型只能是 0(主码流) 或 1(子码流)")] + public int? StreamType { get; set; } + + /// 渲染句柄 (IntPtr 的 Long 形式) + [Range(0, long.MaxValue, ErrorMessage = "渲染句柄必须是非负整数")] + public long? RenderHandle { get; set; } + + // ============================================================================== + // 3. 图像处理参数 (Image Processing - Hot Update) + // 影响解码后的 SmartFrame 数据格式 + // ============================================================================== + + /// 是否允许压缩 (影响传输带宽) + public bool? AllowCompress { get; set; } + + /// 是否允许放大 (影响渲染质量) + public bool? AllowExpand { get; set; } + + /// 目标分辨率 (格式如 "1920x1080",空则保持原图) + [RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")] + public string? TargetResolution { get; set; } + + /// 是否启用图像增强 (去噪/锐化等) + public bool? EnhanceImage { get; set; } + + /// 是否转为灰度图 (用于 AI 纯分析场景加速) + public bool? UseGrayscale { get; set; } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs b/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs new file mode 100644 index 0000000..2460736 --- /dev/null +++ b/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace SHH.CameraSdk; + +// ============================================================================== +// 2. 订阅策略 DTO (对应 A/B/C/D 进程需求) +// 用于多进程帧需求的注册与更新,支持显示帧和分析帧的独立配置 +// ============================================================================== +public class SubscriptionDto +{ + /// + /// 进程唯一标识 (如 "AI_Process_01"、"Main_Display_02") + /// + [Required(ErrorMessage = "进程标识 AppId 不能为空")] + [MaxLength(50, ErrorMessage = "AppId 长度不能超过 50 个字符")] + public string AppId { get; set; } = string.Empty; + + /// + /// 显示帧率需求 (单位: fps) + /// 不需要显示则设为 0,控制器会自动注销该类型需求 + /// + [Range(0, 60, ErrorMessage = "显示帧率需在 0-60 fps 范围内")] + public int DisplayFps { get; set; } + + /// + /// 分析帧率需求 (单位: fps) + /// 不需要分析则设为 0,控制器会自动注销该类型需求 + /// + [Range(0, 30, ErrorMessage = "分析帧率需在 0-30 fps 范围内")] + public int AnalysisFps { get; set; } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs index 2d5420a..a14b243 100644 --- a/SHH.CameraSdk/Controllers/MonitorController.cs +++ b/SHH.CameraSdk/Controllers/MonitorController.cs @@ -96,5 +96,28 @@ public class MonitorController : ControllerBase return File(imageBytes, "image/jpeg"); } + /// + /// 获取指定相机的诊断信息(含审计日志) + /// + /// 相机设备唯一标识 + /// 200 OK + 诊断信息 | 404 Not Found + [HttpGet("diagnose/{id}")] + public IActionResult GetDeviceDiagnostic(long id) + { + var device = _cameraManager.GetDevice(id); + if (device == null) return NotFound(); + + return Ok(new + { + Id = device.Id, + Status = device.Status.ToString(), + RealFps = device.RealFps, + TotalFrames = device.TotalFrames, + // 关键:将 BaseVideoSource 中的日志列表返回给前端 + // 注意:属性名 AuditLogs 会被序列化为 auditLogs (首字母小写),符合前端预期 + AuditLogs = device.GetAuditLogs() + }); + } + #endregion } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index c2cfd3d..dd82281 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -72,6 +72,37 @@ public class CameraManager : IDisposable, IAsyncDisposable public BaseVideoSource? GetDevice(long id) => _cameraPool.TryGetValue(id, out var source) ? source : null; + /// + /// 获取当前管理的所有相机设备 + /// + /// 设备实例集合 + public IEnumerable GetAllDevices() + { + return _cameraPool.Values.ToList(); + } + + /// + /// 从管理池中移除指定设备并释放资源 + /// + /// 设备唯一标识 + public void RemoveDevice(long id) + { + if (_cameraPool.TryRemove(id, out var device)) + { + // 记录日志 + System.Console.WriteLine($"[Manager] 正在移除设备 {id}..."); + + // 1. 停止物理连接 (异步转同步等待,防止资源未释放) + // 在实际高并发场景建议改为 RemoveDeviceAsync + device.StopAsync().GetAwaiter().GetResult(); + + // 2. 释放资源 (销毁非托管句柄) + device.Dispose(); + + System.Console.WriteLine($"[Manager] 设备 {id} 已彻底移除"); + } + } + #endregion #region --- 3. 生命周期控制 (Engine Lifecycle) --- @@ -82,13 +113,14 @@ public class CameraManager : IDisposable, IAsyncDisposable public async Task StartAsync() { // 防护:已销毁则抛出异常 - if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager)); + if (_isDisposed) throw new System.ObjectDisposedException(nameof(CameraManager)); // 防护:避免重复启动 if (_isEngineStarted) return; // 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境 HikSdkManager.Initialize(); + // 不要运行,手动运行 //// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程 //foreach (var source in _cameraPool.Values) //{ @@ -105,7 +137,7 @@ public class CameraManager : IDisposable, IAsyncDisposable TaskCreationOptions.LongRunning, TaskScheduler.Default); - Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); + System.Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); await Task.CompletedTask; } @@ -168,14 +200,93 @@ public class CameraManager : IDisposable, IAsyncDisposable TotalFrames = cam.TotalFrames, HealthScore = healthScore, LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null, - Timestamp = DateTime.Now + Timestamp = System.DateTime.Now }; }).ToList(); } #endregion - #region --- 5. 资源清理 (Disposal) --- + #region --- 5. 配置热更新 (Config Hot Update) --- + + /// + /// 智能更新设备配置 (含冷热分离逻辑) + /// + /// 设备唯一标识 + /// 配置更新传输对象 + /// 设备不存在时抛出 + public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto) + { + if (!_cameraPool.TryGetValue(deviceId, out var device)) + throw new KeyNotFoundException($"设备 {deviceId} 不存在"); + + // 1. 审计 + device.AddAuditLog("收到配置更新请求"); + + // 2. 创建副本进行对比 + var oldConfig = device.Config; + var newConfig = oldConfig.DeepCopy(); + + // 3. 映射 DTO 值 (仅当不为空时修改) + if (dto.IpAddress != null) newConfig.IpAddress = dto.IpAddress; + if (dto.Port != null) newConfig.Port = dto.Port.Value; + if (dto.Username != null) newConfig.Username = dto.Username; + if (dto.Password != null) newConfig.Password = dto.Password; + if (dto.ChannelIndex != null) newConfig.ChannelIndex = dto.ChannelIndex.Value; + if (dto.StreamType != null) newConfig.StreamType = dto.StreamType.Value; + if (dto.Name != null) newConfig.Name = dto.Name; + if (dto.RenderHandle != null) newConfig.RenderHandle = (System.IntPtr)dto.RenderHandle.Value; + + // 4. 判定冷热更新 + // 核心参数变更 -> 冷重启 + bool needColdRestart = + newConfig.IpAddress != oldConfig.IpAddress || + newConfig.Port != oldConfig.Port || + newConfig.Username != oldConfig.Username || + newConfig.Password != oldConfig.Password || + newConfig.ChannelIndex != oldConfig.ChannelIndex || + newConfig.Brand != oldConfig.Brand; + + if (needColdRestart) + { + device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)"); + + // 记录之前的运行状态 + bool wasRunning = device.IsRunning; + + // A. 彻底停止 + if (device.IsOnline) await device.StopAsync(); + + // B. 写入新配置 + device.UpdateConfig(newConfig); + + // C. 如果之前是运行意图,则自动重启连接 + if (wasRunning) await device.StartAsync(); + } + else + { + device.AddAuditLog($"检测到运行时参数变更,执行热更新 (HotSwap)"); + + // A. 更新配置数据 + device.UpdateConfig(newConfig); + + // B. 在线应用策略 (无需断线) + if (device.IsOnline) + { + var options = new DynamicStreamOptions + { + StreamType = dto.StreamType, + RenderHandle = dto.RenderHandle.HasValue ? (System.IntPtr)dto.RenderHandle : null + }; + // 触发驱动层的 OnApplyOptions + device.ApplyOptions(options); + } + } + } + + #endregion + + #region --- 6. 资源清理 (Disposal) --- /// /// 同步销毁:内部调用异步销毁逻辑,等待销毁完成 diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs index f76c03b..9304c42 100644 --- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs +++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs @@ -38,6 +38,18 @@ public class FrameController }); } + /// + /// [新增] 注销订阅者的帧需求 + /// 功能:移除指定订阅者的配置,该订阅者将不再收到任何分发帧 + /// + /// 订阅者唯一标识 + public void Unregister(string appId) + { + // ConcurrentDictionary.TryRemove 是原子的、线程安全的 + // out _ 表示我们要丢弃移除出的对象,因为我们只关心移除动作本身 + _requirements.TryRemove(appId, out _); + } + #endregion #region --- 帧决策生成 (Frame Decision Generation) --- diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index 67b5bd0..c372cb9 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -1,118 +1,162 @@ -using System.Threading.Channels; - -namespace SHH.CameraSdk; +namespace SHH.CameraSdk; /// /// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版) -/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。 -/// 修复记录: -/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。 -/// 2. [Bug π] 管道安全:Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。 -/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。 +/// 核心职责: +/// 1. 提供线程安全的生命周期管理(启动/停止/销毁) +/// 2. 实现状态变更的可靠分发与异常隔离 +/// 3. 支持配置热更新与动态参数应用 +/// 4. 内置 FPS/码率统计、心跳保活、审计日志能力 +/// 关键修复记录: +/// ✅ [Bug A] 死锁免疫:所有 await 均添加 ConfigureAwait(false),解除 UI 线程依赖 +/// ✅ [Bug π] 管道安全:Dispose 采用优雅关闭策略,确保剩余状态消息被消费 +/// ✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑 /// public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable { - #region --- 核心配置与锁机制 (Core Config & Locks) --- + #region --- 1. 核心配置与锁机制 (Core Config & Locks) --- - // [Fix Bug δ] 核心配置对象 - // 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例 + /// + /// 核心配置对象(支持热更新,去除 readonly 修饰符) + /// 注意:外部修改需通过 UpdateConfig 方法,确保线程安全 + /// protected VideoSourceConfig _config; - /// - /// 状态同步锁 - /// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致的状态读取不一致 + /// + /// 状态同步锁 + /// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致状态不一致 /// private readonly object _stateSyncRoot = new(); - /// - /// 生命周期互斥锁 - /// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致的状态机混乱 + /// + /// 生命周期互斥锁(信号量) + /// 作用:确保 StartAsync/StopAsync/UpdateConfig 串行执行,防止重入导致状态机混乱 + /// 配置:初始计数 1,最大计数 1 → 互斥锁 /// private readonly SemaphoreSlim _lifecycleLock = new(1, 1); #endregion - #region --- 内部状态与基础设施 (Internal States & Infrastructure) --- + #region --- 2. 内部状态与基础设施 (Internal States & Infrastructure) --- - // 内部状态标志位 + /// 设备在线状态标志(volatile 确保多线程可见性) private volatile bool _isOnline; + + /// 视频源核心状态(受 _stateSyncRoot 保护) private VideoSourceStatus _status = VideoSourceStatus.Disconnected; - /// - /// 状态通知队列 (有界) - /// 特性:采用 DropOldest 策略,当消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β] + /// + /// 状态通知有界通道 + /// 特性:DropOldest 策略,消费者过载时丢弃旧状态,防止内存溢出 + /// 配置:容量 100 | 单读者多写者 /// private readonly Channel _statusQueue; - // 状态分发器的取消令牌源 + /// 状态分发器的取消令牌源 private CancellationTokenSource? _distributorCts; - // [新增修复 Bug π] 分发任务引用 - // 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费 + /// 状态分发任务引用(用于 Dispose 时优雅等待) private Task? _distributorTask; - // [Fix Bug V] 单调时钟 - // 作用:记录最后一次收到帧的系统 Tick,用于心跳检测,不受系统时间修改影响 + /// 最后一帧接收的系统 Tick(单调时钟,不受系统时间修改影响) private long _lastFrameTick = 0; - /// 获取最后帧的时间戳 (线程安全读取) - public long LastFrameTick => Interlocked.Read(ref _lastFrameTick); - - /// 视频帧回调事件 (热路径) + /// 视频帧回调事件(热路径,低延迟分发) public event Action? FrameReceived; #endregion - #region --- 公开属性 (Public Properties) --- + #region --- 3. 公开属性 (Public Properties) --- + /// 视频源唯一标识 public long Id => _config.Id; + /// 只读配置副本(外部仅能通过 UpdateConfig 修改) public VideoSourceConfig Config => _config; - public VideoSourceStatus Status { get { lock (_stateSyncRoot) return _status; } } + /// 视频源当前状态(线程安全读取) + 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; + /// 最后一帧接收的 Tick 时间戳(线程安全读取) + public long LastFrameTick => Interlocked.Read(ref _lastFrameTick); + #endregion - #region --- 遥测统计属性 (Telemetry Properties) --- + #region --- 4. 遥测统计属性 (Telemetry Properties) --- - // [新增] 遥测统计专用字段 - private long _totalFramesReceived = 0; // 生命周期内总帧数 - private int _tempFrameCounter = 0; // 用于计算FPS的临时计数器 - private long _lastFpsCalcTick = 0; // 上次计算FPS的时间点 - private double _currentFps = 0.0; // 当前实时FPS + /// 生命周期内接收的总帧数 + private long _totalFramesReceived = 0; - // [新增] 公开的遥测属性 (线程安全读取) - public double RealFps => _currentFps; + /// FPS 计算临时计数器 + private int _tempFrameCounter = 0; + /// 上次 FPS 计算的 Tick 时间 + private long _lastFpsCalcTick = 0; + + /// 实时 FPS(每秒更新一次) + public double RealFps { get; private set; } = 0.0; + + /// 实时码率 (Mbps) + protected double _currentBitrate = 0; + + /// 码率计算临时字节计数器 + private long _tempByteCounter = 0; + + /// 生命周期总帧数(线程安全读取) public long TotalFrames => Interlocked.Read(ref _totalFramesReceived); #endregion - #region --- 构造函数 (Constructor) --- + #region --- 5. 审计日志系统 (Audit Log System) --- + + /// 审计日志列表(线程安全访问) + private readonly List _auditLogs = new(); + + /// 最大日志条数(滚动清除,防止内存溢出) + private const int MaxAuditLogCount = 100; + + #endregion + + #region --- 6. 构造函数 (Constructor) --- /// - /// 构造函数:初始化基础设施 + /// 初始化视频源基础设施 /// - /// 视频源基础配置(含设备连接信息、通道号等) + /// 视频源基础配置 /// 配置为空时抛出 protected BaseVideoSource(VideoSourceConfig config) { - if (config == null) throw new ArgumentNullException(nameof(config)); + // 入参校验 + _config = config ?? throw new ArgumentNullException(nameof(config), "视频源配置不能为空"); - // [Fix Bug U] 初始配置深拷贝 - // 防止外部引用修改导致内部状态不可控(配置防漂移) + // 初始化帧控制器 + Controller = new FrameController(); + + // 配置深拷贝(防漂移:内部配置与外部引用隔离) _config = CloneConfig(config); - // [Fix Bug β] 初始化有界通道 - // 容量 100,单读者多写者模式 + // 初始化有界状态通道 _statusQueue = Channel.CreateBounded(new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.DropOldest, @@ -120,44 +164,46 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable SingleWriter = false }); + // 初始化状态分发器 _distributorCts = new CancellationTokenSource(); - - // [关键逻辑] 启动后台状态分发循环 - // 明确持有 Task 引用,以便后续进行优雅关闭等待 _distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token)); } #endregion - #region --- 配置管理 (Config Management) --- + #region --- 7. 配置管理 (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}),下次连接生效。"); + // 深拷贝新配置,隔离外部引用 + _config = newConfig.DeepCopy(); + + // 写入审计日志 + AddAuditLog($"配置已更新 [IP:{_config.IpAddress}],生效时机:{(_isOnline ? "下次重连" : "下次启动")}"); + Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置落地完成"); + } + finally + { + _lifecycleLock.Release(); } - finally { _lifecycleLock.Release(); } } /// - /// 配置深拷贝辅助方法 - /// [编译修复] 严格匹配源文件中的属性复制逻辑,确保 Dictionary 等引用类型被重新创建 + /// 配置深拷贝辅助方法(确保引用类型独立) /// - /// 源配置对象 - /// 深拷贝后的配置实例 + /// 源配置 + /// 深拷贝后的新配置 private VideoSourceConfig CloneConfig(VideoSourceConfig source) { return new VideoSourceConfig @@ -172,7 +218,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable StreamType = source.StreamType, Transport = source.Transport, ConnectionTimeoutMs = source.ConnectionTimeoutMs, - // 必须深拷贝字典,防止外部修改影响内部 + // Dictionary 深拷贝:防止外部修改影响内部 VendorArguments = source.VendorArguments != null ? new Dictionary(source.VendorArguments) : new Dictionary() @@ -181,316 +227,391 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable #endregion - #region --- 生命周期控制 (Lifecycle Control) --- + #region --- 8. 生命周期控制 (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}..."); + // 更新状态为连接中 + UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand} 设备..."); - // 4. 执行具体的驱动启动逻辑 (带超时控制) + // 驱动层启动逻辑(带 15 秒超时) using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); await OnStartAsync(cts.Token).ConfigureAwait(false); - // 5. 标记运行状态 + // 标记运行状态 _isOnline = true; IsRunning = true; - // [Fix Bug D/J] 重置心跳 - // 给予初始宽限期,防止刚启动就被判定为僵尸流 + // 初始化心跳:给予 2 秒宽限期,防止刚启动被判定为僵死 Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000); - // 6. 更新状态为播放中并刷新元数据 - UpdateStatus(VideoSourceStatus.Playing, "流传输运行中"); + // 更新状态为播放中,并刷新元数据 + 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); + throw; // 向上抛出异常,由上层处理 } finally { - // 3. 更新状态并释放锁 - UpdateStatus(VideoSourceStatus.Disconnected, "连接已断开"); _lifecycleLock.Release(); } } /// - /// 刷新设备元数据(能力集) - /// 对比新旧元数据差异,更新设备支持的功能、通道信息等 + /// 异步停止设备连接 + /// + public async Task StopAsync() + { + await _lifecycleLock.WaitAsync().ConfigureAwait(false); + try + { + // 标记离线,阻断后续数据处理 + _isOnline = false; + + // 执行驱动层停止逻辑 + await OnStopAsync().ConfigureAwait(false); + } + finally + { + // 更新状态并释放锁 + 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(); // 标记同步时间 + Metadata.MarkSynced(); return diff; } } - catch (Exception ex) { Console.WriteLine($"[MetadataWarning] {Id}: {ex.Message}"); } + catch (Exception ex) + { + AddAuditLog($"元数据刷新失败: {ex.Message}"); + Debug.WriteLine($"[MetadataWarning] 设备 {Id}: {ex.Message}"); + } return MetadataDiff.None; } /// - /// 应用动态参数(如码流切换、OSD设置) - /// 支持运行时调整画面分辨率、帧率、渲染句柄等 + /// 应用动态流参数(运行时热更新) + /// 支持:分辨率切换、码流类型切换、渲染句柄变更等 /// /// 动态配置项 public void ApplyOptions(DynamicStreamOptions options) { - if (options == null || !_isOnline) return; + // 离线或参数为空时,忽略请求 + if (options == null || !_isOnline) + { + AddAuditLog("动态参数应用失败:设备离线或参数为空"); + return; + } try { - // 1. 校验参数合法性 - if (Metadata.ValidateOptions(options, out string error)) + // 1. 基于设备能力校验参数合法性 + if (Metadata.ValidateOptions(options, out string errorMsg)) { - // 2. 调用驱动层应用参数 + // 2. 执行驱动层参数应用逻辑 OnApplyOptions(options); - UpdateStatus(_status, "动态参数已应用"); + UpdateStatus(_status, "动态参数已成功应用"); + + // 3. 记录参数变更日志 + var changeLog = new List(); + if (options.StreamType.HasValue) changeLog.Add($"码流类型={options.StreamType}"); + if (options.RenderHandle.HasValue) changeLog.Add($"渲染句柄已更新"); + if (options.TargetAnalyzeFps.HasValue) changeLog.Add($"分析帧率={options.TargetAnalyzeFps}fps"); + + AddAuditLog($"动态参数应用: {string.Join(" | ", changeLog)}"); + } + else + { + Debug.WriteLine($"[OptionRejected] 设备 {Id}: {errorMsg}"); } - else { Debug.WriteLine($"[OptionRejected] {error}"); } } - catch (Exception ex) { Debug.WriteLine($"[ApplyOptionsError] {ex.Message}"); } + catch (Exception ex) + { + AddAuditLog($"动态参数应用失败: {ex.Message}"); + Debug.WriteLine($"[ApplyOptionsError] {ex.Message}"); + } } - // 虚方法:供子类重写具体的参数应用逻辑 + /// + /// 驱动层参数应用逻辑(子类重写) + /// + /// 动态配置项 protected virtual void OnApplyOptions(DynamicStreamOptions options) { } #endregion - #region --- 帧处理辅助 (Frame Processing Helpers) --- + #region --- 9. 帧处理与状态管理 (Frame Processing & Status Management) --- /// - /// 检查是否有帧订阅者 - /// 用于优化性能:无订阅时可跳过解码等耗时操作 + /// 检查是否存在帧订阅者(性能优化) + /// 无订阅时可跳过解码/预处理等耗时操作 /// - /// 有订阅者返回 true,否则返回 false + /// 有订阅者返回 true protected bool HasFrameSubscribers() => FrameReceived != null; /// - /// 上报驱动层异常 - /// 将底层异常转换为 Reconnecting 状态,触发协调器介入自愈 + /// 上报驱动层异常,触发重连自愈逻辑 /// - /// 相机统一异常对象 + /// 相机统一异常 protected void ReportError(CameraException ex) { if (!_isOnline) return; + // 标记离线并更新状态为重连中 _isOnline = false; - UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK报错: {ex.Message}"); + UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK异常: {ex.Message}", ex); } /// - /// 标记收到一帧数据(心跳保活 + FPS计算) - /// [修改] 增强了 FPS 计算逻辑,每1秒结算一次实时帧率 + /// 标记帧接收事件(心跳保活 + FPS/码率统计) /// - protected void MarkFrameReceived() + /// 当前帧字节大小 + protected void MarkFrameReceived(uint dataSize = 0) { - long now = Environment.TickCount64; + var now = Environment.TickCount64; - // 1. 更新心跳时间 (原有逻辑) + // 1. 更新心跳时间戳(原子操作) Interlocked.Exchange(ref _lastFrameTick, now); - // 2. 增加总帧数 (原子操作) + // 2. 累加总帧数(原子操作) Interlocked.Increment(ref _totalFramesReceived); - // 3. 计算实时帧率 (FPS) - // 注意:这里不需要加锁,因为通常回调是单线程串行的 - // 即便有多线程微小竞争,对于FPS统计来说误差可忽略,优先保证性能 + // 3. 累加临时计数器(用于 FPS/码率计算) _tempFrameCounter++; - long timeDiff = now - _lastFpsCalcTick; + _tempByteCounter += dataSize; - // 每 1000ms (1秒) 结算一次 FPS - if (timeDiff >= 1000) + // 4. 每秒结算一次统计指标 + var timeDiff = now - _lastFpsCalcTick; + if (timeDiff >= 1000 && _lastFpsCalcTick > 0) { - if (_lastFpsCalcTick > 0) // 忽略第一次冷启动的数据 - { - // 计算公式: 帧数 / (时间间隔秒) - _currentFps = Math.Round(_tempFrameCounter / (timeDiff / 1000.0), 1); - } + var duration = timeDiff / 1000.0; + // 计算实时 FPS (保留 1 位小数) + RealFps = Math.Round(_tempFrameCounter / duration, 1); + + // 计算实时码率 (Mbps) = (字节数 * 8) / 1024 / 1024 / 秒 + _currentBitrate = Math.Round((_tempByteCounter * 8.0) / 1024 / 1024 / duration, 2); + + // 重置临时计数器 _lastFpsCalcTick = now; _tempFrameCounter = 0; + _tempByteCounter = 0; + } + else if (_lastFpsCalcTick == 0) + { + // 初始化 FPS 计算起始时间 + _lastFpsCalcTick = now; } } /// - /// 触发帧回调事件 - /// 向所有订阅者分发帧数据(热路径,尽量减少耗时操作) + /// 触发帧回调事件(热路径优化) /// - /// 帧数据(通常为 OpenCvSharp.Mat 或 SmartFrame) + /// 帧数据(如 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 且为空 + // 关键修复:使用 CancellationToken.None 等待读取 + // 确保取消时仍能消费完队列剩余消息 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}"); + Debug.WriteLine($"[UIEventError] 设备 {Id} 状态回调异常: {ex.Message}"); } - // 退出条件:仅当明确取消 且 队列已空 时才退出 - if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0) return; + // 退出条件:取消令牌已触发 且 队列为空 + if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0) + { + return; + } } // 双重检查退出条件 - 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}"); } + 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)); + + // 写入状态队列(满时自动丢弃旧数据) + _ = _statusQueue.Writer.TryWrite(new StatusChangedEventArgs( + status, + msg, + ex, + ex?.RawErrorCode) + { + NewHandle = ex?.Context.TryGetValue("NativeHandle", out var handle) == true ? (IntPtr)handle : null + }); } } #endregion - #region --- 抽象方法 (Abstract Methods) --- + #region --- 10. 审计日志辅助 (Audit Log Helpers) --- /// - /// 驱动层启动逻辑(必须由具体驱动实现) - /// 包含设备连接、登录、取流等底层操作 + /// 添加审计日志(线程安全) + /// + /// 日志内容 + public void AddAuditLog(string message) + { + lock (_auditLogs) + { + var logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; + _auditLogs.Add(logEntry); + + // 滚动清除旧日志 + if (_auditLogs.Count > MaxAuditLogCount) + { + _auditLogs.RemoveAt(0); + } + } + } + + /// + /// 获取审计日志副本 + /// + /// 日志列表副本 + public List GetAuditLogs() + { + lock (_auditLogs) + { + return new List(_auditLogs); + } + } + + #endregion + + #region --- 11. 抽象方法 (Abstract Methods) --- + + /// + /// 驱动层启动逻辑(子类必须实现) + /// 包含:设备登录、码流订阅、取流线程启动等 /// /// 取消令牌 protected abstract Task OnStartAsync(CancellationToken token); /// - /// 驱动层停止逻辑(必须由具体驱动实现) - /// 包含设备登出、连接断开、资源释放等底层操作 + /// 驱动层停止逻辑(子类必须实现) + /// 包含:码流停止、设备登出、资源释放等 /// protected abstract Task OnStopAsync(); /// - /// 驱动层元数据获取逻辑(必须由具体驱动实现) - /// 用于获取设备型号、通道能力、固件版本等信息 + /// 驱动层元数据获取逻辑(子类必须实现) /// - /// 设备元数据实例 + /// 设备元数据 protected abstract Task OnFetchMetadataAsync(); #endregion - #region --- 资源清理 (Disposal) --- + #region --- 12. 资源清理 (Resource Disposal) --- /// - /// [Fix Bug A: 死锁终结者] 同步销毁入口 - /// 原理:强制启动一个新的后台 Task 执行 DisposeAsync,并同步阻塞等待其完成 - /// 效果:彻底规避了在 UI 线程直接 wait 导致的死锁问题 + /// 同步销毁入口(死锁免疫) /// public void Dispose() { + // 异步销毁在后台执行,避免阻塞 UI 线程 Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult(); } /// - /// 异步销毁资源 - /// 包含:停止业务、关闭管道、断开事件引用、释放非托管资源 + /// 异步销毁资源(优雅关闭) /// + /// ValueTask public virtual async ValueTask DisposeAsync() { // 1. 停止业务逻辑 await StopAsync().ConfigureAwait(false); - // 2. [Fix Bug π] 优雅关闭状态管道 - _statusQueue.Writer.TryComplete(); // 标记不再接受新数据 - _distributorCts?.Cancel(); // 通知消费者准备退出 + // 2. 优雅关闭状态分发器 + _statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息 + _distributorCts?.Cancel(); // 触发分发器取消 + // 3. 等待分发器处理完剩余消息(最多等待 500ms) if (_distributorTask != null) { - // 3. 等待分发器处理完剩余消息 - // 给予 500ms 的宽限期,防止无限等待 await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false); } - // 4. [Fix Bug ε] 强力切断事件引用 - // 防止 UI 控件忘记取消订阅导致的内存泄漏 + // 4. 切断事件引用,防止内存泄漏 FrameReceived = null; StatusChanged = null; @@ -498,15 +619,19 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable _lifecycleLock.Dispose(); _distributorCts?.Dispose(); + // 6. 抑制垃圾回收器的终结器 GC.SuppressFinalize(this); } #endregion - #region --- 内部字段 (Internal Fields) --- + #region --- 13. 内部字段与补全属性 --- - // 用于跟踪上一个未完成的生命周期任务 + /// 跟踪上一个未完成的生命周期任务 private Task _pendingLifecycleTask = Task.CompletedTask; + /// 帧控制器(用于帧分发策略管理) + public FrameController Controller { get; protected set; } + #endregion } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index d93395f..a53552c 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -3,110 +3,89 @@ 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、异步启动等修复。 +/// [海康驱动] 工业级视频源实现 V3.4.0 (运维增强版) +/// 修复记录: +/// 1. [Fix Bug Z] 控制器遮蔽:移除子类 Controller 定义,复用基类实例,修复 FPS 控制失效问题。 +/// 2. [Feat A] 热更新支持:实现 OnApplyOptions,支持码流/句柄不亦断线热切换。 +/// 3. [Feat B] 审计集成:全面接入 AddAuditLog,对接 Web 运维仪表盘。 /// 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: 防止多线程并发读写内存) + private int _userId = -1; // SDK 登录句柄 + private int _realPlayHandle = -1; // 预览句柄 + private int _playPort = -1; // 播放端口 + + private readonly object _initLock = new(); + private readonly object _bufferLock = new(); - // [Fix Bug X: 异步状态竞争] - // 作用:连接世代计数器,每次 StartAsync 调用时自增 - // 原理:异步任务执行过程中验证是否为最新请求,避免幽灵任务覆盖状态 private volatile int _connectionEpoch = 0; - // 回调委托引用:必须持有以防止 P/Invoke 过程中被 GC 回收,导致回调崩溃 + // 回调委托引用 (防止GC) private HikNativeMethods.REALDATACALLBACK? _realDataCallBack; private HikPlayMethods.DECCBFUN? _decCallBack; - // 内存复用对象 (Fix Bug T):复用非托管内存块,减少 LOH (大对象堆) 分配压力 + // 内存复用对象 private Mat? _sharedYuvMat; - private Mat? _sharedBgrMat; + private Mat? _sharedBgrMat; // (如有需要可复用,当前逻辑直接用FramePool) - // 帧对象池:实现零 GC 分配,避免频繁创建/销毁 Mat 导致的性能抖动 private FramePool? _framePool; - private bool _isPoolReady = false; // 帧池初始化状态标记 + private bool _isPoolReady = false; + + // 【关键修复 Bug Z】: 删除了这里原本的 "public FrameController Controller..." + // 直接使用 BaseVideoSource.Controller - // 帧需求控制器:管理不同订阅者(UI/AI)的帧率需求,实现按需分发 - public FrameController Controller { get; } = new(); - #endregion #region --- 构造函数 (Constructor) --- - - /// - /// 初始化海康视频源实例 - /// - /// 视频源基础配置(含设备IP、账号、码流类型等) - public HikVideoSource(VideoSourceConfig config) : base(config) { } - + + 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); - // 执行设备物理登录(阻塞调用,网络异常时可能耗时数秒) + // [审计] 记录登录动作 + AddAuditLog($"正在执行物理登录... ({_config.IpAddress})"); + 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("启动任务已过期(被新的请求抢占)"); + if (newUserId >= 0) HikNativeMethods.NET_DVR_Logout(newUserId); + throw new OperationCanceledException("启动任务已过期"); } _userId = newUserId; @@ -116,53 +95,47 @@ public class HikVideoSource : BaseVideoSource throw new CameraException(HikErrorMapper.Map(err), $"登录失败: {err}", DeviceBrand.HikVision, (int)err); } - // [Bug H] 路由注册:将 UserID 与当前实例绑定,支持全局回调路由 _instances.TryAdd(_userId, this); + AddAuditLog($"物理登录成功 (UserID: {_userId})"); - // 开启网络预览(取流):失败则抛出异常,触发资源清理 + // 开启取流 if (!StartRealPlay()) { uint err = HikNativeMethods.NET_DVR_GetLastError(); throw new CameraException(HikErrorMapper.Map(err), $"预览失败: {err}", DeviceBrand.HikVision, (int)err); } + + AddAuditLog($"网络取流成功 (StreamType: {_config.StreamType})"); } - catch + catch (Exception ex) { - // [Fix Bug W] 异常清理:启动失败时执行完整资源释放,防止句柄泄漏 + AddAuditLog($"启动异常: {ex.Message}"); CleanupSync(); throw; } }, token); } - /// - /// 异步停止设备:终止取流、解码,释放所有资源 - /// protected override async Task OnStopAsync() { - // [Fix Bug X] 停止时递增世代:立即使所有正在进行的启动任务失效 Interlocked.Increment(ref _connectionEpoch); - - // 在后台线程执行同步清理逻辑,避免阻塞调用线程 + AddAuditLog("正在执行停止流程..."); await Task.Run(() => CleanupSync()); + AddAuditLog("设备已停止"); } - /// - /// [同步清理核心] (含 Bug Y 锁保护) - /// 职责:按“停止取流→释放解码资源→释放内存→注销登录”顺序销毁,防止非托管崩溃 - /// private void CleanupSync() { lock (_initLock) { - // 1. 停止网络取流:释放预览句柄 + // 1. 停止预览 if (_realPlayHandle >= 0) { HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle); _realPlayHandle = -1; } - // 2. 停止解码并释放播放端口:避免端口资源泄漏 + // 2. 停止解码 if (_playPort >= 0) { HikPlayMethods.PlayM4_Stop(_playPort); @@ -171,14 +144,13 @@ public class HikVideoSource : BaseVideoSource _playPort = -1; } - // [Fix Bug Y] 内存释放保护:确保解码回调未在使用内存 lock (_bufferLock) { _sharedYuvMat?.Dispose(); _sharedYuvMat = null; _sharedBgrMat?.Dispose(); _sharedBgrMat = null; } - // 3. 注销登录:先移除路由映射,再释放登录句柄 + // 3. 注销登录 if (_userId >= 0) { _instances.TryRemove(_userId, out _); @@ -186,76 +158,115 @@ public class HikVideoSource : BaseVideoSource _userId = -1; } - // 4. 释放帧对象池:清理复用内存 _framePool?.Dispose(); _framePool = null; _isPoolReady = false; } - - // 5. 减少SDK全局引用计数:确保最后一个实例销毁时卸载SDK HikSdkManager.Uninitialize(); } #endregion + #region --- [新功能] 动态参数热应用 (Hot Swap) --- + + // 【关键修复 Feat A】实现基类的抽象方法,处理码流切换 + protected override void OnApplyOptions(DynamicStreamOptions options) + { + // 1. 码流热切换逻辑 + if (options.StreamType.HasValue) + { + int targetStream = options.StreamType.Value; + AddAuditLog($"收到码流切换请求: {targetStream},开始执行热切换..."); + + lock (_initLock) + { + // A. 停止预览 (Keep Login) + if (_realPlayHandle >= 0) + { + HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle); + _realPlayHandle = -1; + } + + // B. 清理播放库 (防止旧流数据残留) + if (_playPort >= 0) + { + HikPlayMethods.PlayM4_Stop(_playPort); + HikPlayMethods.PlayM4_CloseStream(_playPort); + HikPlayMethods.PlayM4_FreePort(_playPort); + _playPort = -1; + } + + // C. 更新内部配置状态 + _config.StreamType = targetStream; + + // D. 重新开启预览 + if (StartRealPlay()) + { + AddAuditLog($"码流热切换成功 (当前: {(_config.StreamType == 0 ? "主" : "子")}码流)"); + } + else + { + uint err = HikNativeMethods.NET_DVR_GetLastError(); + AddAuditLog($"码流切换失败: {err}"); + } + } + } + + // 2. 句柄动态更新逻辑 (如有需要) + if (options.RenderHandle.HasValue) + { + // 如果是硬解码模式,可以在这里调用 PlayM4_Play(port, newHandle) + AddAuditLog($"收到新句柄绑定请求: {options.RenderHandle}"); + } + } + + #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 // 非阻塞取流:避免长时间阻塞线程 + hPlayWnd = IntPtr.Zero, + 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 { - // 预览句柄无效时直接返回,避免无效处理 + // [优化] 维持心跳,防止被哨兵误杀 + MarkFrameReceived(dwBufSize); + 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_SetDisplayBuf(_playPort, 1); // 极速模式 + HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); - // 初始化播放库:设置流模式→打开流→绑定解码回调→开始解码 - HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); // 0=实时流模式 if (!HikPlayMethods.PlayM4_OpenStream(_playPort, pBuffer, dwBufSize, 2 * 1024 * 1024)) { HikPlayMethods.PlayM4_FreePort(_playPort); @@ -268,36 +279,32 @@ public class HikVideoSource : BaseVideoSource HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero); } } - // 处理流数据:将原始流数据传入播放库解码 + // 处理流数据 else if (dwDataType == HikNativeMethods.NET_DVR_STREAMDATA && _playPort != -1) { HikPlayMethods.PlayM4_InputData(_playPort, pBuffer, dwBufSize); } } - catch { /* 吞没回调异常:防止回调崩溃导致整个SDK进程退出 */ } + catch { } } #endregion - #region --- 解码与帧分发 (Decoding & Frame Distribution) --- + #region --- 解码与帧分发 (Decoding) --- - /// - /// 解码回调 (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) + // 1. [核心流控] 询问基类控制器:这帧要不要? + // 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。 var decision = Controller.MakeDecision(Environment.TickCount64); + + // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return; int width = pFrameInfo.nWidth; int height = pFrameInfo.nHeight; - // 2. 初始化帧池:首次解码时创建,按实际分辨率分配内存 + // 2. 初始化帧池 if (!_isPoolReady) { lock (_initLock) @@ -305,7 +312,6 @@ public class HikVideoSource : BaseVideoSource if (!_isPoolReady) { _framePool?.Dispose(); - // 帧池配置:CV_8UC3=BGR格式,初始3帧,最大5帧(平衡内存与性能) _framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5); _isPoolReady = true; } @@ -314,73 +320,54 @@ public class HikVideoSource : BaseVideoSource if (_framePool == null) return; - // 3. 从帧池获取内存:零GC分配,池满时返回null(直接丢帧,避免积压) + // 3. 转换与分发 SmartFrame smartFrame = _framePool.Get(); try { - if (smartFrame == null) return; // 帧池满,丢弃当前帧 + if (smartFrame == null) return; // 池满丢帧 - try + using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf)) { - // 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}"); + Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12); } - // 6. 提交到全局处理中心:后续由管道处理二次加工与分发 + // 4. [分发] 将决策结果传递给处理中心 + // decision.TargetAppIds 包含了 "谁需要这一帧" 的信息 GlobalProcessingCenter.Submit(this.Id, smartFrame, decision); } + catch (Exception ex) + { + smartFrame.Dispose(); + // 这里为了性能不频繁写日志,仅在调试时开启 + // Debug.WriteLine(ex.Message); + } finally { - // 释放驱动层引用:驱动职责结束,引用计数-1(由消费者/管道管理后续生命周期) smartFrame.Dispose(); } } #endregion - #region --- 异常处理 (Exception Handling) --- + #region --- 异常处理 --- - /// - /// 全局异常回调处理 - /// 职责:将 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.AddAuditLog($"SDK报警异常: 0x{dwType:X}"); // 写入审计 instance.ReportError(new CameraException( CameraErrorCode.NetworkUnreachable, - $"SDK全局报警异常: 0x{dwType:X}", + $"SDK全局异常: 0x{dwType:X}", DeviceBrand.HikVision)); } } - catch { /* 吞没异常:避免全局回调崩溃 */ } + 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 index 5b74353..cc11a0c 100644 --- a/SHH.CameraSdk/Program.cs +++ b/SHH.CameraSdk/Program.cs @@ -110,11 +110,12 @@ namespace SHH.CameraSdk { Id = 101, Brand = DeviceBrand.HikVision, - // IpAddress = "172.16.41.206", - IpAddress = "192.168.5.9", + IpAddress = "172.16.41.206", + //IpAddress = "192.168.5.9", Port = 8000, Username = "admin", - Password = "RRYFOA", + Password = "abcd1234", + //Password = "RRYFOA", StreamType = 0 // 主码流 }; manager.AddDevice(config); @@ -152,25 +153,25 @@ namespace SHH.CameraSdk // 投递到渲染线程 (FrameConsumer) renderer.Enqueue(frame); - Console.WriteLine("Frame Enqueued"); + //Console.WriteLine("Frame Enqueued"); }); // [消费者 B] - 绝对只会收到 8fps GlobalStreamDispatcher.Subscribe("Process_B_Local", (deviceId, frame) => { - if (deviceId == 101) - { - Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)"); - } + //if (deviceId == 101) + //{ + // Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)"); + //} }); // [消费者 AI] GlobalStreamDispatcher.Subscribe("AI_Engine_Core", (deviceId, frame) => { - if (deviceId == 101) - { - Console.WriteLine($" >>> [AI] 分析一帧..."); - } + //if (deviceId == 101) + //{ + // Console.WriteLine($" >>> [AI] 分析一帧..."); + //} }); } }