WebAPI 支持摄像头启停控制、码流切换、审计日志的提供

This commit is contained in:
2025-12-26 12:15:10 +08:00
parent e9f5975a79
commit adcdc56c7a
12 changed files with 1176 additions and 357 deletions

View File

@@ -43,6 +43,9 @@ public class VideoSourceConfig
/// <summary> 默认码流类型0 = 主码流(高清)1 = 子码流(低带宽) </summary> /// <summary> 默认码流类型0 = 主码流(高清)1 = 子码流(低带宽) </summary>
public int StreamType { get; set; } = 0; public int StreamType { get; set; } = 0;
/// <summary> Rtsp 播放路径 </summary>
public string RtspPath { get; set; } = string.Empty;
/// <summary> 传输协议TCP/UDP/Multicast默认 TCP 保证可靠性) </summary> /// <summary> 传输协议TCP/UDP/Multicast默认 TCP 保证可靠性) </summary>
[JsonConverter(typeof(JsonStringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]
public TransportProtocol Transport { get; set; } = TransportProtocol.Tcp; public TransportProtocol Transport { get; set; } = TransportProtocol.Tcp;
@@ -132,6 +135,7 @@ public class VideoSourceConfig
Password = this.Password, Password = this.Password,
RenderHandle = this.RenderHandle, RenderHandle = this.RenderHandle,
ChannelIndex = this.ChannelIndex, ChannelIndex = this.ChannelIndex,
RtspPath = this.RtspPath,
StreamType = this.StreamType, StreamType = this.StreamType,
Transport = this.Transport, Transport = this.Transport,
ConnectionTimeoutMs = this.ConnectionTimeoutMs ConnectionTimeoutMs = this.ConnectionTimeoutMs

View File

@@ -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)
// ==========================================================================
/// <summary>
/// 1. 获取所有设备清单
/// </summary>
[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);
}
/// <summary>
/// 2. 新增设备 (写入配置并初始化)
/// </summary>
[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);
}
/// <summary>
/// 3. 编辑设备 (自动识别冷热更新)
/// </summary>
[HttpPut("{id}")]
public async Task<IActionResult> 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); }
}
/// <summary>
/// 4. 删除设备 (销毁连接)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> 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)
// ==========================================================================
/// <summary>
/// 5. 注册/更新进程的流需求 (A/B/C/D 场景核心)
/// </summary>
/// <remarks>
/// 示例场景:
/// - 主进程配置(B): { "appId": "Main_Config", "displayFps": 25, "analysisFps": 0 }
/// - AI进程(C): { "appId": "AI_Core", "displayFps": 0, "analysisFps": 5 }
/// </remarks>
[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)
// ==========================================================================
/// <summary>
/// 6. 绑定显示窗口 (对应 A进程-句柄场景)
/// </summary>
[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: 设备运行控制
// ==========================================================================
/// <summary>
/// 手动控制设备运行状态 (开关机)
/// </summary>
[HttpPost("{id}/power")]
public async Task<IActionResult> 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 });
}
/// <summary>
/// 热应用动态参数 (如切换码流)
/// </summary>
[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
};
}
/// <summary>
/// [辅助方法] 将全量配置 DTO 转换为更新 DTO
/// </summary>
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
};
}
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
namespace SHH.CameraSdk;
/// <summary>
/// 句柄绑定 DTO
/// 用于前端向后端传递窗口渲染句柄,实现视频流的硬件解码渲染
/// </summary>
public class BindHandleDto
{
/// <summary>
/// 窗口句柄 (IntPtr 转换为 long 类型传输,避免跨平台序列化问题)
/// </summary>
[Required(ErrorMessage = "渲染窗口句柄不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "句柄必须为有效的非负整数")]
public long Handle { get; set; }
/// <summary>
/// 用途描述 (用于审计日志,如 "Main_Preview"、"AI_Analysis_Window")
/// </summary>
[MaxLength(64, ErrorMessage = "用途描述长度不能超过 64 个字符")]
public string Purpose { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,123 @@
using System.ComponentModel.DataAnnotations;
namespace SHH.CameraSdk;
// ==============================================================================
// 1. 物理与运行配置 DTO (对应 CRUD 操作)
// 用于设备新增/全量配置查询,包含基础身份、连接信息、运行参数等全量字段
// ==============================================================================
public class CameraConfigDto
{
// --- 基础身份 (Identity) ---
/// <summary>
/// 设备唯一标识
/// </summary>
[Required(ErrorMessage = "设备ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "设备ID必须为正整数")]
public long Id { get; set; }
/// <summary>
/// 设备友好名称
/// </summary>
[MaxLength(64, ErrorMessage = "设备名称长度不能超过64个字符")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// 摄像头品牌类型 (0:HikVision, 1:Dahua, 2:RTSP...)
/// </summary>
[Range(0, 10, ErrorMessage = "品牌类型值必须在0-10范围内")]
public int Brand { get; set; }
/// <summary>
/// 设备安装位置描述
/// </summary>
[MaxLength(128, ErrorMessage = "安装位置长度不能超过128个字符")]
public string Location { get; set; } = string.Empty;
// --- 核心连接 (Connectivity) - 修改此类参数触发冷重启 ---
/// <summary>
/// 摄像头IP地址
/// </summary>
[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;
/// <summary>
/// SDK端口 (如海康默认8000)
/// </summary>
[Range(1, 65535, ErrorMessage = "端口号必须在1-65535范围内")]
public ushort Port { get; set; } = 8000;
/// <summary>
/// 登录用户名
/// </summary>
[MaxLength(32, ErrorMessage = "用户名长度不能超过32个字符")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// 登录密码
/// </summary>
[MaxLength(64, ErrorMessage = "密码长度不能超过64个字符")]
public string Password { get; set; } = string.Empty;
/// <summary>
/// 通道号 (通常为1)
/// </summary>
[Range(1, 32, ErrorMessage = "通道号必须在1-32范围内")]
public int ChannelIndex { get; set; } = 1;
/// <summary>
/// RTSP流路径 (备用或非SDK模式使用)
/// </summary>
[MaxLength(256, ErrorMessage = "RTSP地址长度不能超过256个字符")]
public string RtspPath { get; set; } = string.Empty;
// --- 主板关联信息 (Metadata) ---
/// <summary>
/// 关联主板IP地址
/// </summary>
[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;
/// <summary>
/// 关联主板端口
/// </summary>
[Range(1, 65535, ErrorMessage = "主板端口号必须在1-65535范围内")]
public int MainboardPort { get; set; } = 80;
// --- 运行时参数 (Runtime Options) - 支持热更新 ---
/// <summary>
/// 码流类型 (0:主码流, 1:子码流)
/// </summary>
[Range(0, 1, ErrorMessage = "码流类型只能是0(主码流)或1(子码流)")]
public int StreamType { get; set; } = 0;
/// <summary>
/// 是否使用灰度图 (用于AI分析场景加速)
/// </summary>
public bool UseGrayscale { get; set; } = false;
/// <summary>
/// 是否启用图像增强 (去噪/锐化等)
/// </summary>
public bool EnhanceImage { get; set; } = true;
// --- 画面变换 (Transform) - 支持热更新 ---
/// <summary>
/// 是否允许图像压缩 (降低带宽占用)
/// </summary>
public bool AllowCompress { get; set; } = true;
/// <summary>
/// 是否允许图像放大 (提升渲染质量)
/// </summary>
public bool AllowExpand { get; set; } = false;
/// <summary>
/// 目标分辨率 (格式如 1920x1080空则保持原图)
/// </summary>
[RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")]
public string TargetResolution { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,96 @@
using System.ComponentModel.DataAnnotations;
namespace SHH.CameraSdk;
/// <summary>
/// 设备配置更新传输对象
/// <para>用于接收前端的编辑请求,支持部分更新(字段为 null 表示不修改)</para>
/// <para>自动区分 <b>冷更新参数</b>(需重启连接)和 <b>热更新参数</b>(无感生效)</para>
/// </summary>
public class DeviceUpdateDto
{
// ==============================================================================
// 1. 冷更新参数 (Cold Update)
// 修改此类参数涉及物理连接变更,后端会自动执行 "Stop -> Update -> Start" 流程
// ==============================================================================
/// <summary>摄像头IP地址</summary>
[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; }
/// <summary>SDK端口 (如海康 8000)</summary>
[Range(1, 65535, ErrorMessage = "端口号必须在 1-65535 范围内")]
public ushort? Port { get; set; }
/// <summary>登录用户名</summary>
[MaxLength(32, ErrorMessage = "用户名长度不能超过 32 个字符")]
public string? Username { get; set; }
/// <summary>登录密码</summary>
[MaxLength(64, ErrorMessage = "密码长度不能超过 64 个字符")]
public string? Password { get; set; }
/// <summary>通道号 (默认 1)</summary>
[Range(1, 32, ErrorMessage = "通道号必须在 1-32 范围内")]
public int? ChannelIndex { get; set; }
/// <summary>摄像头品牌类型 (0:Hik, 1:Dahua...)</summary>
[Range(0, 10, ErrorMessage = "品牌类型值必须在 0-10 范围内")]
public int? Brand { get; set; }
/// <summary>RTSP流地址 (非SDK模式下使用)</summary>
[MaxLength(256, ErrorMessage = "RTSP地址长度不能超过 256 个字符")]
public string? RtspPath { get; set; }
/// <summary>关联的主板IP (用于联动控制)</summary>
[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; }
/// <summary>关联的主板端口</summary>
[Range(1, 65535, ErrorMessage = "主板端口号必须在 1-65535 范围内")]
public int? MainboardPort { get; set; }
// ==============================================================================
// 2. 热更新参数 (Hot Update)
// 修改此类参数仅刷新运行时状态,不中断物理连接,无感生效
// ==============================================================================
/// <summary>设备名称/别名</summary>
[MaxLength(64, ErrorMessage = "设备名称长度不能超过 64 个字符")]
public string? Name { get; set; }
/// <summary>安装位置描述</summary>
[MaxLength(128, ErrorMessage = "安装位置长度不能超过 128 个字符")]
public string? Location { get; set; }
/// <summary>码流类型 (0:主码流, 1:子码流)</summary>
[Range(0, 1, ErrorMessage = "码流类型只能是 0(主码流) 或 1(子码流)")]
public int? StreamType { get; set; }
/// <summary>渲染句柄 (IntPtr 的 Long 形式)</summary>
[Range(0, long.MaxValue, ErrorMessage = "渲染句柄必须是非负整数")]
public long? RenderHandle { get; set; }
// ==============================================================================
// 3. 图像处理参数 (Image Processing - Hot Update)
// 影响解码后的 SmartFrame 数据格式
// ==============================================================================
/// <summary>是否允许压缩 (影响传输带宽)</summary>
public bool? AllowCompress { get; set; }
/// <summary>是否允许放大 (影响渲染质量)</summary>
public bool? AllowExpand { get; set; }
/// <summary>目标分辨率 (格式如 "1920x1080",空则保持原图)</summary>
[RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")]
public string? TargetResolution { get; set; }
/// <summary>是否启用图像增强 (去噪/锐化等)</summary>
public bool? EnhanceImage { get; set; }
/// <summary>是否转为灰度图 (用于 AI 纯分析场景加速)</summary>
public bool? UseGrayscale { get; set; }
}

View File

@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
namespace SHH.CameraSdk;
// ==============================================================================
// 2. 订阅策略 DTO (对应 A/B/C/D 进程需求)
// 用于多进程帧需求的注册与更新,支持显示帧和分析帧的独立配置
// ==============================================================================
public class SubscriptionDto
{
/// <summary>
/// 进程唯一标识 (如 "AI_Process_01"、"Main_Display_02")
/// </summary>
[Required(ErrorMessage = "进程标识 AppId 不能为空")]
[MaxLength(50, ErrorMessage = "AppId 长度不能超过 50 个字符")]
public string AppId { get; set; } = string.Empty;
/// <summary>
/// 显示帧率需求 (单位: fps)
/// <para>不需要显示则设为 0控制器会自动注销该类型需求</para>
/// </summary>
[Range(0, 60, ErrorMessage = "显示帧率需在 0-60 fps 范围内")]
public int DisplayFps { get; set; }
/// <summary>
/// 分析帧率需求 (单位: fps)
/// <para>不需要分析则设为 0控制器会自动注销该类型需求</para>
/// </summary>
[Range(0, 30, ErrorMessage = "分析帧率需在 0-30 fps 范围内")]
public int AnalysisFps { get; set; }
}

View File

@@ -96,5 +96,28 @@ public class MonitorController : ControllerBase
return File(imageBytes, "image/jpeg"); return File(imageBytes, "image/jpeg");
} }
/// <summary>
/// 获取指定相机的诊断信息(含审计日志)
/// </summary>
/// <param name="id">相机设备唯一标识</param>
/// <returns>200 OK + 诊断信息 | 404 Not Found</returns>
[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 #endregion
} }

View File

@@ -72,6 +72,37 @@ public class CameraManager : IDisposable, IAsyncDisposable
public BaseVideoSource? GetDevice(long id) public BaseVideoSource? GetDevice(long id)
=> _cameraPool.TryGetValue(id, out var source) ? source : null; => _cameraPool.TryGetValue(id, out var source) ? source : null;
/// <summary>
/// 获取当前管理的所有相机设备
/// </summary>
/// <returns>设备实例集合</returns>
public IEnumerable<BaseVideoSource> GetAllDevices()
{
return _cameraPool.Values.ToList();
}
/// <summary>
/// 从管理池中移除指定设备并释放资源
/// </summary>
/// <param name="id">设备唯一标识</param>
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 #endregion
#region --- 3. (Engine Lifecycle) --- #region --- 3. (Engine Lifecycle) ---
@@ -82,13 +113,14 @@ public class CameraManager : IDisposable, IAsyncDisposable
public async Task StartAsync() public async Task StartAsync()
{ {
// 防护:已销毁则抛出异常 // 防护:已销毁则抛出异常
if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager)); if (_isDisposed) throw new System.ObjectDisposedException(nameof(CameraManager));
// 防护:避免重复启动 // 防护:避免重复启动
if (_isEngineStarted) return; if (_isEngineStarted) return;
// 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境 // 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境
HikSdkManager.Initialize(); HikSdkManager.Initialize();
// 不要运行,手动运行
//// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程 //// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程
//foreach (var source in _cameraPool.Values) //foreach (var source in _cameraPool.Values)
//{ //{
@@ -105,7 +137,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
TaskCreationOptions.LongRunning, TaskCreationOptions.LongRunning,
TaskScheduler.Default); TaskScheduler.Default);
Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); System.Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -168,14 +200,93 @@ public class CameraManager : IDisposable, IAsyncDisposable
TotalFrames = cam.TotalFrames, TotalFrames = cam.TotalFrames,
HealthScore = healthScore, HealthScore = healthScore,
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null, LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
Timestamp = DateTime.Now Timestamp = System.DateTime.Now
}; };
}).ToList(); }).ToList();
} }
#endregion #endregion
#region --- 5. (Disposal) --- #region --- 5. (Config Hot Update) ---
/// <summary>
/// 智能更新设备配置 (含冷热分离逻辑)
/// </summary>
/// <param name="deviceId">设备唯一标识</param>
/// <param name="dto">配置更新传输对象</param>
/// <exception cref="KeyNotFoundException">设备不存在时抛出</exception>
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) ---
/// <summary> /// <summary>
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成 /// 同步销毁:内部调用异步销毁逻辑,等待销毁完成

View File

@@ -38,6 +38,18 @@ public class FrameController
}); });
} }
/// <summary>
/// [新增] 注销订阅者的帧需求
/// 功能:移除指定订阅者的配置,该订阅者将不再收到任何分发帧
/// </summary>
/// <param name="appId">订阅者唯一标识</param>
public void Unregister(string appId)
{
// ConcurrentDictionary.TryRemove 是原子的、线程安全的
// out _ 表示我们要丢弃移除出的对象,因为我们只关心移除动作本身
_requirements.TryRemove(appId, out _);
}
#endregion #endregion
#region --- (Frame Decision Generation) --- #region --- (Frame Decision Generation) ---

View File

@@ -1,118 +1,162 @@
using System.Threading.Channels; namespace SHH.CameraSdk;
namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版) /// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版)
/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。 /// 核心职责:
/// 修复记录: /// <para>1. 提供线程安全的生命周期管理(启动/停止/销毁)</para>
/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。 /// <para>2. 实现状态变更的可靠分发与异常隔离</para>
/// 2. [Bug π] 管道安全Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。 /// <para>3. 支持配置热更新与动态参数应用</para>
/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。 /// <para>4. 内置 FPS/码率统计、心跳保活、审计日志能力</para>
/// 关键修复记录:
/// <para>✅ [Bug A] 死锁免疫:所有 await 均添加 ConfigureAwait(false),解除 UI 线程依赖</para>
/// <para>✅ [Bug π] 管道安全Dispose 采用优雅关闭策略,确保剩余状态消息被消费</para>
/// <para>✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑</para>
/// </summary> /// </summary>
public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
{ {
#region --- (Core Config & Locks) --- #region --- 1. (Core Config & Locks) ---
// [Fix Bug δ] 核心配置对象 /// <summary>
// 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例 /// 核心配置对象(支持热更新,去除 readonly 修饰符
/// 注意:外部修改需通过 UpdateConfig 方法,确保线程安全
/// </summary>
protected VideoSourceConfig _config; protected VideoSourceConfig _config;
/// <summary> /// <summary>
/// 状态同步锁 /// 状态同步锁
/// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致状态读取不一致 /// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致状态不一致
/// </summary> /// </summary>
private readonly object _stateSyncRoot = new(); private readonly object _stateSyncRoot = new();
/// <summary> /// <summary>
/// 生命周期互斥锁 /// 生命周期互斥锁(信号量)
/// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致状态机混乱 /// 作用:确保 StartAsync/StopAsync/UpdateConfig 串行执行,防止重入导致状态机混乱
/// 配置:初始计数 1最大计数 1 → 互斥锁
/// </summary> /// </summary>
private readonly SemaphoreSlim _lifecycleLock = new(1, 1); private readonly SemaphoreSlim _lifecycleLock = new(1, 1);
#endregion #endregion
#region --- (Internal States & Infrastructure) --- #region --- 2. (Internal States & Infrastructure) ---
// 内部状态标志位 /// <summary> 设备在线状态标志volatile 确保多线程可见性) </summary>
private volatile bool _isOnline; private volatile bool _isOnline;
/// <summary> 视频源核心状态(受 _stateSyncRoot 保护) </summary>
private VideoSourceStatus _status = VideoSourceStatus.Disconnected; private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
/// <summary> /// <summary>
/// 状态通知队列 (有界) /// 状态通知有界通道
/// 特性:采用 DropOldest 策略,消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β] /// 特性DropOldest 策略,消费者过载时丢弃旧状态,防止内存溢出
/// 配置:容量 100 | 单读者多写者
/// </summary> /// </summary>
private readonly Channel<StatusChangedEventArgs> _statusQueue; private readonly Channel<StatusChangedEventArgs> _statusQueue;
// 状态分发器的取消令牌源 /// <summary> 状态分发器的取消令牌源 </summary>
private CancellationTokenSource? _distributorCts; private CancellationTokenSource? _distributorCts;
// [新增修复 Bug π] 分发任务引用 /// <summary> 状态分发任务引用(用于 Dispose 时优雅等待) </summary>
// 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费
private Task? _distributorTask; private Task? _distributorTask;
// [Fix Bug V] 单调时钟 /// <summary> 最后一帧接收的系统 Tick单调时钟不受系统时间修改影响 </summary>
// 作用:记录最后一次收到帧的系统 Tick用于心跳检测不受系统时间修改影响
private long _lastFrameTick = 0; private long _lastFrameTick = 0;
/// <summary> 获取最后帧的时间戳 (线程安全读取) </summary> /// <summary> 视频帧回调事件(热路径,低延迟分发) </summary>
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
/// <summary> 视频帧回调事件 (热路径) </summary>
public event Action<object>? FrameReceived; public event Action<object>? FrameReceived;
#endregion #endregion
#region --- (Public Properties) --- #region --- 3. (Public Properties) ---
/// <summary> 视频源唯一标识 </summary>
public long Id => _config.Id; public long Id => _config.Id;
/// <summary> 只读配置副本(外部仅能通过 UpdateConfig 修改) </summary>
public VideoSourceConfig Config => _config; public VideoSourceConfig Config => _config;
public VideoSourceStatus Status { get { lock (_stateSyncRoot) return _status; } } /// <summary> 视频源当前状态(线程安全读取) </summary>
public VideoSourceStatus Status
{
get
{
lock (_stateSyncRoot)
{
return _status;
}
}
}
/// <summary> 运行状态标记 </summary>
public bool IsRunning { get; set; } public bool IsRunning { get; set; }
/// <summary> 设备在线状态 </summary>
public bool IsOnline => _isOnline; public bool IsOnline => _isOnline;
/// <summary> 设备元数据(能力集、通道信息等) </summary>
public DeviceMetadata Metadata { get; protected set; } = new(); public DeviceMetadata Metadata { get; protected set; } = new();
/// <summary> 状态变更事件(对外暴露状态通知) </summary>
public event EventHandler<StatusChangedEventArgs>? StatusChanged; public event EventHandler<StatusChangedEventArgs>? StatusChanged;
/// <summary> 最后一帧接收的 Tick 时间戳(线程安全读取) </summary>
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
#endregion #endregion
#region --- (Telemetry Properties) --- #region --- 4. (Telemetry Properties) ---
// [新增] 遥测统计专用字段 /// <summary> 生命周期内接收的总帧数 </summary>
private long _totalFramesReceived = 0; // 生命周期内总帧数 private long _totalFramesReceived = 0;
private int _tempFrameCounter = 0; // 用于计算FPS的临时计数器
private long _lastFpsCalcTick = 0; // 上次计算FPS的时间点
private double _currentFps = 0.0; // 当前实时FPS
// [新增] 公开的遥测属性 (线程安全读取) /// <summary> FPS 计算临时计数器 </summary>
public double RealFps => _currentFps; private int _tempFrameCounter = 0;
/// <summary> 上次 FPS 计算的 Tick 时间 </summary>
private long _lastFpsCalcTick = 0;
/// <summary> 实时 FPS每秒更新一次 </summary>
public double RealFps { get; private set; } = 0.0;
/// <summary> 实时码率 (Mbps) </summary>
protected double _currentBitrate = 0;
/// <summary> 码率计算临时字节计数器 </summary>
private long _tempByteCounter = 0;
/// <summary> 生命周期总帧数(线程安全读取) </summary>
public long TotalFrames => Interlocked.Read(ref _totalFramesReceived); public long TotalFrames => Interlocked.Read(ref _totalFramesReceived);
#endregion #endregion
#region --- (Constructor) --- #region --- 5. (Audit Log System) ---
/// <summary> 审计日志列表(线程安全访问) </summary>
private readonly List<string> _auditLogs = new();
/// <summary> 最大日志条数(滚动清除,防止内存溢出) </summary>
private const int MaxAuditLogCount = 100;
#endregion
#region --- 6. (Constructor) ---
/// <summary> /// <summary>
/// 构造函数:初始化基础设施 /// 初始化视频源基础设施
/// </summary> /// </summary>
/// <param name="config">视频源基础配置(含设备连接信息、通道号等)</param> /// <param name="config">视频源基础配置</param>
/// <exception cref="ArgumentNullException">配置为空时抛出</exception> /// <exception cref="ArgumentNullException">配置为空时抛出</exception>
protected BaseVideoSource(VideoSourceConfig config) 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); _config = CloneConfig(config);
// [Fix Bug β] 初始化有界通道 // 初始化有界状态通道
// 容量 100单读者多写者模式
_statusQueue = Channel.CreateBounded<StatusChangedEventArgs>(new BoundedChannelOptions(100) _statusQueue = Channel.CreateBounded<StatusChangedEventArgs>(new BoundedChannelOptions(100)
{ {
FullMode = BoundedChannelFullMode.DropOldest, FullMode = BoundedChannelFullMode.DropOldest,
@@ -120,44 +164,46 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
SingleWriter = false SingleWriter = false
}); });
// 初始化状态分发器
_distributorCts = new CancellationTokenSource(); _distributorCts = new CancellationTokenSource();
// [关键逻辑] 启动后台状态分发循环
// 明确持有 Task 引用,以便后续进行优雅关闭等待
_distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token)); _distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token));
} }
#endregion #endregion
#region --- (Config Management) --- #region --- 7. (Config Management) ---
/// <summary> /// <summary>
/// [修复 Bug δ] 更新配置实现 /// 热更新视频源配置(线程安全)
/// 允许在不销毁实例的情况下更新 IP、端口等参数新配置下次连接生效 /// 新配置将在下次启动/重连时生效
/// </summary> /// </summary>
/// <param name="newConfig">新的视频源配置</param> /// <param name="newConfig">新的视频源配置</param>
public void UpdateConfig(VideoSourceConfig newConfig) public void UpdateConfig(VideoSourceConfig newConfig)
{ {
if (newConfig == null) return; if (newConfig == null) return;
// 1. 获取生命周期锁 // 加生命周期锁:防止与启动/停止操作并发
// 虽然只是内存操作,但为了防止与 Start/Stop 并发导致读取到脏配置,仍需加锁
_lifecycleLock.Wait(); _lifecycleLock.Wait();
try try
{ {
// 2. 执行深拷贝 // 深拷贝新配置,隔离外部引用
_config = CloneConfig(newConfig); _config = newConfig.DeepCopy();
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置已更新 ({_config.IpAddress}),下次连接生效。");
// 写入审计日志
AddAuditLog($"配置已更新 [IP:{_config.IpAddress}],生效时机:{(_isOnline ? "" : "")}");
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置落地完成");
}
finally
{
_lifecycleLock.Release();
} }
finally { _lifecycleLock.Release(); }
} }
/// <summary> /// <summary>
/// 配置深拷贝辅助方法 /// 配置深拷贝辅助方法(确保引用类型独立)
/// [编译修复] 严格匹配源文件中的属性复制逻辑,确保 Dictionary 等引用类型被重新创建
/// </summary> /// </summary>
/// <param name="source">源配置对象</param> /// <param name="source">源配置</param>
/// <returns>深拷贝后的配置实例</returns> /// <returns>深拷贝后的配置</returns>
private VideoSourceConfig CloneConfig(VideoSourceConfig source) private VideoSourceConfig CloneConfig(VideoSourceConfig source)
{ {
return new VideoSourceConfig return new VideoSourceConfig
@@ -172,7 +218,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
StreamType = source.StreamType, StreamType = source.StreamType,
Transport = source.Transport, Transport = source.Transport,
ConnectionTimeoutMs = source.ConnectionTimeoutMs, ConnectionTimeoutMs = source.ConnectionTimeoutMs,
// 必须深拷贝字典,防止外部修改影响内部 // Dictionary 深拷贝:防止外部修改影响内部
VendorArguments = source.VendorArguments != null VendorArguments = source.VendorArguments != null
? new Dictionary<string, string>(source.VendorArguments) ? new Dictionary<string, string>(source.VendorArguments)
: new Dictionary<string, string>() : new Dictionary<string, string>()
@@ -181,316 +227,391 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
#endregion #endregion
#region --- (Lifecycle Control) --- #region --- 8. (Lifecycle Control) ---
/// <summary> /// <summary>
/// 异步启动设备连接 /// 异步启动设备连接
/// 包含:状态校验、生命周期锁、非托管初始化、元数据刷新 /// 包含:状态校验、非托管初始化、元数据刷新
/// </summary> /// </summary>
public async Task StartAsync() public async Task StartAsync()
{ {
// [修复 Bug A] 必须加 ConfigureAwait(false) // 死锁免疫:不捕获当前同步上下文
// 确保后续代码在线程池线程执行,防止 UI 线程死锁
await _lifecycleLock.WaitAsync().ConfigureAwait(false); await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try try
{ {
// 1. 强制等待上一个生命周期动作完全结束 // 等待上一个生命周期任务完成
// 防止快速点击 Start/Stop 导致的逻辑重叠
await _pendingLifecycleTask.ConfigureAwait(false); await _pendingLifecycleTask.ConfigureAwait(false);
// 2. 状态幂等性检查 // 幂等性检查:已在线则直接返回
if (_isOnline) return; if (_isOnline) return;
// 3. 更新状态为连接中 // 更新状态为连接中
UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand}..."); UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand} 设备...");
// 4. 执行具体的驱动启动逻辑 (带超时控制) // 驱动启动逻辑(带 15 秒超时)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await OnStartAsync(cts.Token).ConfigureAwait(false); await OnStartAsync(cts.Token).ConfigureAwait(false);
// 5. 标记运行状态 // 标记运行状态
_isOnline = true; _isOnline = true;
IsRunning = true; IsRunning = true;
// [Fix Bug D/J] 重置心跳 // 初始化心跳:给予 2 秒宽限期,防止刚启动被判定为僵死
// 给予初始宽限期,防止刚启动就被判定为僵尸流
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000); Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000);
// 6. 更新状态为播放中并刷新元数据 // 更新状态为播放中并刷新元数据
UpdateStatus(VideoSourceStatus.Playing, "流传输运行"); UpdateStatus(VideoSourceStatus.Playing, "流传输正常运行");
await RefreshMetadataAsync().ConfigureAwait(false); await RefreshMetadataAsync().ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
// 7. 异常处理:回滚状态 // 异常回滚:标记离线并更新状态
_isOnline = false; _isOnline = false;
UpdateStatus(VideoSourceStatus.Disconnected, $"启动失败: {ex.Message}"); UpdateStatus(VideoSourceStatus.Disconnected, $"启动失败: {ex.Message}");
throw; throw; // 向上抛出异常,由上层处理
}
finally { _lifecycleLock.Release(); }
}
/// <summary>
/// 异步停止设备连接
/// 流程:标记离线→执行驱动停止逻辑→更新状态
/// </summary>
public async Task StopAsync()
{
// [修复 Bug A] ConfigureAwait(false) 护体
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 1. 标记离线,阻断后续的数据处理
_isOnline = false;
// 2. 执行具体的驱动停止逻辑
await OnStopAsync().ConfigureAwait(false);
} }
finally finally
{ {
// 3. 更新状态并释放锁
UpdateStatus(VideoSourceStatus.Disconnected, "连接已断开");
_lifecycleLock.Release(); _lifecycleLock.Release();
} }
} }
/// <summary> /// <summary>
/// 刷新设备元数据(能力集) /// 异步停止设备连接
/// 对比新旧元数据差异,更新设备支持的功能、通道信息等 /// </summary>
public async Task StopAsync()
{
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 标记离线,阻断后续数据处理
_isOnline = false;
// 执行驱动层停止逻辑
await OnStopAsync().ConfigureAwait(false);
}
finally
{
// 更新状态并释放锁
UpdateStatus(VideoSourceStatus.Disconnected, "连接已手动断开");
_lifecycleLock.Release();
}
}
/// <summary>
/// 刷新设备元数据,对比差异并更新
/// </summary> /// </summary>
/// <returns>元数据差异描述符</returns> /// <returns>元数据差异描述符</returns>
public async Task<MetadataDiff> RefreshMetadataAsync() public async Task<MetadataDiff> RefreshMetadataAsync()
{ {
// 离线状态不刷新元数据
if (!_isOnline) return MetadataDiff.None; if (!_isOnline) return MetadataDiff.None;
try try
{ {
// 1. 调用驱动层获取最新元数据 // 获取驱动层最新元数据
var latestMetadata = await OnFetchMetadataAsync().ConfigureAwait(false); var latestMetadata = await OnFetchMetadataAsync().ConfigureAwait(false);
// 2. 比对差异并更新
if (latestMetadata != null && latestMetadata.ChannelCount > 0) if (latestMetadata != null && latestMetadata.ChannelCount > 0)
{ {
// 对比新旧元数据差异
var diff = Metadata.CompareWith(latestMetadata); var diff = Metadata.CompareWith(latestMetadata);
// 更新元数据并标记同步时间
Metadata = latestMetadata; Metadata = latestMetadata;
Metadata.MarkSynced(); // 标记同步时间 Metadata.MarkSynced();
return diff; 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; return MetadataDiff.None;
} }
/// <summary> /// <summary>
/// 应用动态参数(如码流切换、OSD设置 /// 应用动态参数(运行时热更新
/// 支持运行时调整画面分辨率、帧率、渲染句柄等 /// 支持:分辨率切换、码流类型切换、渲染句柄变更
/// </summary> /// </summary>
/// <param name="options">动态配置项</param> /// <param name="options">动态配置项</param>
public void ApplyOptions(DynamicStreamOptions options) public void ApplyOptions(DynamicStreamOptions options)
{ {
if (options == null || !_isOnline) return; // 离线或参数为空时,忽略请求
if (options == null || !_isOnline)
{
AddAuditLog("动态参数应用失败:设备离线或参数为空");
return;
}
try try
{ {
// 1. 校验参数合法性 // 1. 基于设备能力校验参数合法性
if (Metadata.ValidateOptions(options, out string error)) if (Metadata.ValidateOptions(options, out string errorMsg))
{ {
// 2. 调用驱动层应用参数 // 2. 执行驱动层参数应用逻辑
OnApplyOptions(options); OnApplyOptions(options);
UpdateStatus(_status, "动态参数已应用"); UpdateStatus(_status, "动态参数已成功应用");
// 3. 记录参数变更日志
var changeLog = new List<string>();
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}");
}
} }
// 虚方法:供子类重写具体的参数应用逻辑 /// <summary>
/// 驱动层参数应用逻辑(子类重写)
/// </summary>
/// <param name="options">动态配置项</param>
protected virtual void OnApplyOptions(DynamicStreamOptions options) { } protected virtual void OnApplyOptions(DynamicStreamOptions options) { }
#endregion #endregion
#region --- (Frame Processing Helpers) --- #region --- 9. (Frame Processing & Status Management) ---
/// <summary> /// <summary>
/// 检查是否帧订阅者 /// 检查是否存在帧订阅者(性能优化)
/// 用于优化性能:无订阅时可跳过解码等耗时操作 /// 无订阅时可跳过解码/预处理等耗时操作
/// </summary> /// </summary>
/// <returns>有订阅者返回 true,否则返回 false</returns> /// <returns>有订阅者返回 true</returns>
protected bool HasFrameSubscribers() => FrameReceived != null; protected bool HasFrameSubscribers() => FrameReceived != null;
/// <summary> /// <summary>
/// 上报驱动层异常 /// 上报驱动层异常,触发重连自愈逻辑
/// 将底层异常转换为 Reconnecting 状态,触发协调器介入自愈
/// </summary> /// </summary>
/// <param name="ex">相机统一异常对象</param> /// <param name="ex">相机统一异常</param>
protected void ReportError(CameraException ex) protected void ReportError(CameraException ex)
{ {
if (!_isOnline) return; if (!_isOnline) return;
// 标记离线并更新状态为重连中
_isOnline = false; _isOnline = false;
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK报错: {ex.Message}"); UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK异常: {ex.Message}", ex);
} }
/// <summary> /// <summary>
/// 标记收到一帧数据(心跳保活 + FPS计 /// 标记帧接收事件(心跳保活 + FPS/码率统计)
/// [修改] 增强了 FPS 计算逻辑每1秒结算一次实时帧率
/// </summary> /// </summary>
protected void MarkFrameReceived() /// <param name="dataSize">当前帧字节大小</param>
protected void MarkFrameReceived(uint dataSize = 0)
{ {
long now = Environment.TickCount64; var now = Environment.TickCount64;
// 1. 更新心跳时间 (原有逻辑) // 1. 更新心跳时间戳(原子操作)
Interlocked.Exchange(ref _lastFrameTick, now); Interlocked.Exchange(ref _lastFrameTick, now);
// 2. 加总帧数 (原子操作) // 2. 加总帧数原子操作
Interlocked.Increment(ref _totalFramesReceived); Interlocked.Increment(ref _totalFramesReceived);
// 3. 计算实时帧率 (FPS) // 3. 累加临时计数器(用于 FPS/码率计算)
// 注意:这里不需要加锁,因为通常回调是单线程串行的
// 即便有多线程微小竞争对于FPS统计来说误差可忽略优先保证性能
_tempFrameCounter++; _tempFrameCounter++;
long timeDiff = now - _lastFpsCalcTick; _tempByteCounter += dataSize;
// 每 1000ms (1秒) 结算一次 FPS // 4. 每秒结算一次统计指标
if (timeDiff >= 1000) var timeDiff = now - _lastFpsCalcTick;
if (timeDiff >= 1000 && _lastFpsCalcTick > 0)
{ {
if (_lastFpsCalcTick > 0) // 忽略第一次冷启动的数据 var duration = timeDiff / 1000.0;
{
// 计算公式: 帧数 / (时间间隔秒)
_currentFps = Math.Round(_tempFrameCounter / (timeDiff / 1000.0), 1);
}
// 计算实时 FPS (保留 1 位小数)
RealFps = Math.Round(_tempFrameCounter / duration, 1);
// 计算实时码率 (Mbps) = (字节数 * 8) / 1024 / 1024 / 秒
_currentBitrate = Math.Round((_tempByteCounter * 8.0) / 1024 / 1024 / duration, 2);
// 重置临时计数器
_lastFpsCalcTick = now; _lastFpsCalcTick = now;
_tempFrameCounter = 0; _tempFrameCounter = 0;
_tempByteCounter = 0;
}
else if (_lastFpsCalcTick == 0)
{
// 初始化 FPS 计算起始时间
_lastFpsCalcTick = now;
} }
} }
/// <summary> /// <summary>
/// 触发帧回调事件 /// 触发帧回调事件(热路径优化)
/// 向所有订阅者分发帧数据(热路径,尽量减少耗时操作)
/// </summary> /// </summary>
/// <param name="frameData">帧数据(通常为 OpenCvSharp.Mat 或 SmartFrame</param> /// <param name="frameData">帧数据(如 Mat/SmartFrame</param>
protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData); protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData);
#endregion
#region --- (Status Distribution) ---
/// <summary> /// <summary>
/// 后台状态分发循环 /// 后台状态分发循环(单线程消费状态队列)
/// 负责将 Channel 中的状态变更事件调度到 StatusChanged 事件订阅者
/// </summary> /// </summary>
/// <param name="token">取消令牌,用于终止分发循环</param> /// <param name="token">取消令牌</param>
private async Task StatusDistributorLoopAsync(CancellationToken token) private async Task StatusDistributorLoopAsync(CancellationToken token)
{ {
try try
{ {
// [修复 Bug π] 关键修复点 // 关键修复:使用 CancellationToken.None 等待读取
// 使用 CancellationToken.None 作为 WaitToReadAsync 的参数 // 确保取消时仍能消费完队列剩余消息
// 含义:即使 token 被取消,只要 Channel 里还有数据,就继续读取,直到 Channel 被 Complete 且为空
while (await _statusQueue.Reader.WaitToReadAsync(CancellationToken.None).ConfigureAwait(false)) while (await _statusQueue.Reader.WaitToReadAsync(CancellationToken.None).ConfigureAwait(false))
{ {
// 批量读取队列中的所有消息
while (_statusQueue.Reader.TryRead(out var args)) while (_statusQueue.Reader.TryRead(out var args))
{ {
// [Fix Bug M] 玻璃心防护:捕获用户层回调异常,防止崩溃 // 异常隔离:捕获订阅者回调异常,防止分发器崩溃
try try
{ {
StatusChanged?.Invoke(this, args); StatusChanged?.Invoke(this, args);
} }
catch (Exception ex) 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}");
}
} }
/// <summary> /// <summary>
/// 更新设备状态并写入通道 /// 更新设备状态并写入分发队列
/// 线程安全,采用 DropOldest 策略防止状态队列溢出
/// </summary> /// </summary>
/// <param name="status">新状态</param> /// <param name="status">新状态</param>
/// <param name="msg">状态描述信息</param> /// <param name="msg">状态描述</param>
/// <param name="ex">可选:状态变更关联异常</param> /// <param name="ex">关联异常(可选)</param>
protected void UpdateStatus(VideoSourceStatus status, string msg, CameraException? ex = null) protected void UpdateStatus(VideoSourceStatus status, string msg, CameraException? ex = null)
{ {
lock (_stateSyncRoot) lock (_stateSyncRoot)
{ {
// 更新内部状态
_status = status; _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 #endregion
#region --- (Abstract Methods) --- #region --- 10. (Audit Log Helpers) ---
/// <summary> /// <summary>
/// 驱动层启动逻辑(必须由具体驱动实现 /// 添加审计日志(线程安全
/// 包含设备连接、登录、取流等底层操作 /// </summary>
/// <param name="message">日志内容</param>
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);
}
}
}
/// <summary>
/// 获取审计日志副本
/// </summary>
/// <returns>日志列表副本</returns>
public List<string> GetAuditLogs()
{
lock (_auditLogs)
{
return new List<string>(_auditLogs);
}
}
#endregion
#region --- 11. (Abstract Methods) ---
/// <summary>
/// 驱动层启动逻辑(子类必须实现)
/// 包含:设备登录、码流订阅、取流线程启动等
/// </summary> /// </summary>
/// <param name="token">取消令牌</param> /// <param name="token">取消令牌</param>
protected abstract Task OnStartAsync(CancellationToken token); protected abstract Task OnStartAsync(CancellationToken token);
/// <summary> /// <summary>
/// 驱动层停止逻辑(必须由具体驱动实现) /// 驱动层停止逻辑(子类必须实现)
/// 包含设备登出、连接断开、资源释放等底层操作 /// 包含:码流停止、设备登出、资源释放等
/// </summary> /// </summary>
protected abstract Task OnStopAsync(); protected abstract Task OnStopAsync();
/// <summary> /// <summary>
/// 驱动层元数据获取逻辑(必须由具体驱动实现) /// 驱动层元数据获取逻辑(子类必须实现)
/// 用于获取设备型号、通道能力、固件版本等信息
/// </summary> /// </summary>
/// <returns>设备元数据实例</returns> /// <returns>设备元数据</returns>
protected abstract Task<DeviceMetadata> OnFetchMetadataAsync(); protected abstract Task<DeviceMetadata> OnFetchMetadataAsync();
#endregion #endregion
#region --- (Disposal) --- #region --- 12. (Resource Disposal) ---
/// <summary> /// <summary>
/// [Fix Bug A: 死锁终结者] 同步销毁入口 /// 同步销毁入口(死锁免疫)
/// 原理:强制启动一个新的后台 Task 执行 DisposeAsync并同步阻塞等待其完成
/// 效果:彻底规避了在 UI 线程直接 wait 导致的死锁问题
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
// 异步销毁在后台执行,避免阻塞 UI 线程
Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult(); Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
} }
/// <summary> /// <summary>
/// 异步销毁资源 /// 异步销毁资源(优雅关闭)
/// 包含:停止业务、关闭管道、断开事件引用、释放非托管资源
/// </summary> /// </summary>
/// <returns>ValueTask</returns>
public virtual async ValueTask DisposeAsync() public virtual async ValueTask DisposeAsync()
{ {
// 1. 停止业务逻辑 // 1. 停止业务逻辑
await StopAsync().ConfigureAwait(false); await StopAsync().ConfigureAwait(false);
// 2. [Fix Bug π] 优雅关闭状态管道 // 2. 优雅关闭状态分发器
_statusQueue.Writer.TryComplete(); // 标记不再接受新数据 _statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息
_distributorCts?.Cancel(); // 通知消费者准备退出 _distributorCts?.Cancel(); // 触发分发器取消
// 3. 等待分发器处理完剩余消息(最多等待 500ms
if (_distributorTask != null) if (_distributorTask != null)
{ {
// 3. 等待分发器处理完剩余消息
// 给予 500ms 的宽限期,防止无限等待
await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false); await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false);
} }
// 4. [Fix Bug ε] 强力切断事件引用 // 4. 切断事件引用,防止内存泄漏
// 防止 UI 控件忘记取消订阅导致的内存泄漏
FrameReceived = null; FrameReceived = null;
StatusChanged = null; StatusChanged = null;
@@ -498,15 +619,19 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
_lifecycleLock.Dispose(); _lifecycleLock.Dispose();
_distributorCts?.Dispose(); _distributorCts?.Dispose();
// 6. 抑制垃圾回收器的终结器
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
#endregion #endregion
#region --- (Internal Fields) --- #region --- 13. ---
// 用于跟踪上一个未完成的生命周期任务 /// <summary> 跟踪上一个未完成的生命周期任务 </summary>
private Task _pendingLifecycleTask = Task.CompletedTask; private Task _pendingLifecycleTask = Task.CompletedTask;
/// <summary> 帧控制器(用于帧分发策略管理) </summary>
public FrameController Controller { get; protected set; }
#endregion #endregion
} }

View File

@@ -3,110 +3,89 @@
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// [海康驱动] 工业级视频源实现 V3.3.1 (极高并发修正版) /// [海康驱动] 工业级视频源实现 V3.4.0 (运维增强版)
/// 核心职责:负责海康威视设备 (SDK) 的物理连接、取流、解码与资源管理。 /// 修复记录:
/// 核心修复记录: /// 1. [Fix Bug Z] 控制器遮蔽:移除子类 Controller 定义,复用基类实例,修复 FPS 控制失效问题。
/// 1. [Bug X] 异步竞争:引入 Epoch 世代验证,防止超时取消后的幽灵任务覆盖新连接 /// 2. [Feat A] 热更新支持:实现 OnApplyOptions支持码流/句柄不亦断线热切换
/// 2. [Bug Y] 内存踩踏:解码回调加锁,防止多核环境下共享 Mat 被并发读写引发 AV 异常 /// 3. [Feat B] 审计集成:全面接入 AddAuditLog对接 Web 运维仪表盘
/// 3. [Bug α] 端口抢占PlayM4_GetPort 全局加锁,防止高并发启动时的播放端口串位。
/// 4. [Bug H/W/T/E] 继承之前的路由分发、幽灵句柄、零 GC、异步启动等修复。
/// </summary> /// </summary>
public class HikVideoSource : BaseVideoSource public class HikVideoSource : BaseVideoSource
{ {
#region --- (Global Resources) --- #region --- (Global Resources) ---
// 静态路由表 (Fix Bug H: 友军误伤) // 静态路由表
// 作用:将海康 SDK 的全局回调(仅带 UserID精准路由到具体的 HikVideoSource 实例
private static readonly ConcurrentDictionary<int, HikVideoSource> _instances = new(); private static readonly ConcurrentDictionary<int, HikVideoSource> _instances = new();
// 全局异常回调
// 全局异常回调委托(防止 GC 回收)
private static readonly HikNativeMethods.EXCEPTION_CALLBACK _globalExceptionCallback = StaticOnSdkException; private static readonly HikNativeMethods.EXCEPTION_CALLBACK _globalExceptionCallback = StaticOnSdkException;
// 端口抢占锁
// [Fix Bug α: 端口抢占]
// 背景:海康播放库 PlayCtrl.dll 的 PlayM4_GetPort 函数内部使用了非线程安全的全局计数器。
// 作用:使用全局静态锁强制串行化端口申请操作,防止高并发启动时分配到相同的 Port。
private static readonly object _globalPortLock = new(); private static readonly object _globalPortLock = new();
#endregion #endregion
#region --- (Instance Members) --- #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 int _userId = -1; // SDK 登录句柄
private readonly object _bufferLock = new(); // 解码缓冲区锁 (Fix Bug Y: 防止多线程并发读写内存) 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; private volatile int _connectionEpoch = 0;
// 回调委托引用:必须持有以防止 P/Invoke 过程中被 GC 回收,导致回调崩溃 // 回调委托引用 (防止GC)
private HikNativeMethods.REALDATACALLBACK? _realDataCallBack; private HikNativeMethods.REALDATACALLBACK? _realDataCallBack;
private HikPlayMethods.DECCBFUN? _decCallBack; private HikPlayMethods.DECCBFUN? _decCallBack;
// 内存复用对象 (Fix Bug T):复用非托管内存块,减少 LOH (大对象堆) 分配压力 // 内存复用对象
private Mat? _sharedYuvMat; private Mat? _sharedYuvMat;
private Mat? _sharedBgrMat; private Mat? _sharedBgrMat; // (如有需要可复用当前逻辑直接用FramePool)
// 帧对象池:实现零 GC 分配,避免频繁创建/销毁 Mat 导致的性能抖动
private FramePool? _framePool; 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 #endregion
#region --- (Constructor) --- #region --- (Constructor) ---
/// <summary> public HikVideoSource(VideoSourceConfig config) : base(config)
/// 初始化海康视频源实例 {
/// </summary> // 构造函数保持简洁
/// <param name="config">视频源基础配置含设备IP、账号、码流类型等</param> }
public HikVideoSource(VideoSourceConfig config) : base(config) { }
#endregion #endregion
#region --- (Core Lifecycle) --- #region --- (Core Lifecycle) ---
/// <summary>
/// [异步启动核心] (含 Bug E/X 修复)
/// 流程SDK环境初始化 → 注册全局回调 → 物理登录设备 → 路由注册 → 开启网络预览
/// </summary>
/// <param name="token">取消令牌:用于终止超时或中断的启动流程</param>
protected override async Task OnStartAsync(CancellationToken token) protected override async Task OnStartAsync(CancellationToken token)
{ {
// [Fix Bug X] 记录当前启动世代,标记一次新的启动请求
int currentEpoch = Interlocked.Increment(ref _connectionEpoch); int currentEpoch = Interlocked.Increment(ref _connectionEpoch);
// [Fix Bug E] 切换到后台线程执行避免阻塞UI/上下文线程(登录为同步阻塞操作)
await Task.Run(() => await Task.Run(() =>
{ {
// [Fix Bug X] 世代验证:若已存在新的启动请求,直接放弃当前任务
if (currentEpoch != _connectionEpoch) return; if (currentEpoch != _connectionEpoch) return;
// 初始化海康 SDK 环境(引用计数管理,确保资源不重复加载)
if (!HikSdkManager.Initialize()) if (!HikSdkManager.Initialize())
throw new CameraException(CameraErrorCode.SdkNotInitialized, "SDK初始化失败", DeviceBrand.HikVision); throw new CameraException(CameraErrorCode.SdkNotInitialized, "SDK初始化失败", DeviceBrand.HikVision);
try try
{ {
// 注册全局异常回调捕获断线、重连等SDK层面异常
HikNativeMethods.NET_DVR_SetExceptionCallBack_V30(0, IntPtr.Zero, _globalExceptionCallback, IntPtr.Zero); HikNativeMethods.NET_DVR_SetExceptionCallBack_V30(0, IntPtr.Zero, _globalExceptionCallback, IntPtr.Zero);
// 执行设备物理登录(阻塞调用,网络异常时可能耗时数秒) // [审计] 记录登录动作
AddAuditLog($"正在执行物理登录... ({_config.IpAddress})");
var devInfo = new HikNativeMethods.NET_DEVICEINFO_V30(); var devInfo = new HikNativeMethods.NET_DEVICEINFO_V30();
int newUserId = HikNativeMethods.NET_DVR_Login_V30( int newUserId = HikNativeMethods.NET_DVR_Login_V30(
_config.IpAddress, _config.Port, _config.Username, _config.Password, ref devInfo); _config.IpAddress, _config.Port, _config.Username, _config.Password, ref devInfo);
// [Fix Bug X] 登录后再次验证世代:避免超时后产生的幽灵句柄
if (currentEpoch != _connectionEpoch) if (currentEpoch != _connectionEpoch)
{ {
if (newUserId >= 0) HikNativeMethods.NET_DVR_Logout(newUserId); // 释放僵尸句柄 if (newUserId >= 0) HikNativeMethods.NET_DVR_Logout(newUserId);
throw new OperationCanceledException("启动任务已过期(被新的请求抢占)"); throw new OperationCanceledException("启动任务已过期");
} }
_userId = newUserId; _userId = newUserId;
@@ -116,53 +95,47 @@ public class HikVideoSource : BaseVideoSource
throw new CameraException(HikErrorMapper.Map(err), $"登录失败: {err}", DeviceBrand.HikVision, (int)err); throw new CameraException(HikErrorMapper.Map(err), $"登录失败: {err}", DeviceBrand.HikVision, (int)err);
} }
// [Bug H] 路由注册:将 UserID 与当前实例绑定,支持全局回调路由
_instances.TryAdd(_userId, this); _instances.TryAdd(_userId, this);
AddAuditLog($"物理登录成功 (UserID: {_userId})");
// 开启网络预览(取流):失败则抛出异常,触发资源清理 // 开启取流
if (!StartRealPlay()) if (!StartRealPlay())
{ {
uint err = HikNativeMethods.NET_DVR_GetLastError(); uint err = HikNativeMethods.NET_DVR_GetLastError();
throw new CameraException(HikErrorMapper.Map(err), $"预览失败: {err}", DeviceBrand.HikVision, (int)err); 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(); CleanupSync();
throw; throw;
} }
}, token); }, token);
} }
/// <summary>
/// 异步停止设备:终止取流、解码,释放所有资源
/// </summary>
protected override async Task OnStopAsync() protected override async Task OnStopAsync()
{ {
// [Fix Bug X] 停止时递增世代:立即使所有正在进行的启动任务失效
Interlocked.Increment(ref _connectionEpoch); Interlocked.Increment(ref _connectionEpoch);
AddAuditLog("正在执行停止流程...");
// 在后台线程执行同步清理逻辑,避免阻塞调用线程
await Task.Run(() => CleanupSync()); await Task.Run(() => CleanupSync());
AddAuditLog("设备已停止");
} }
/// <summary>
/// [同步清理核心] (含 Bug Y 锁保护)
/// 职责:按“停止取流→释放解码资源→释放内存→注销登录”顺序销毁,防止非托管崩溃
/// </summary>
private void CleanupSync() private void CleanupSync()
{ {
lock (_initLock) lock (_initLock)
{ {
// 1. 停止网络取流:释放预览句柄 // 1. 停止预览
if (_realPlayHandle >= 0) if (_realPlayHandle >= 0)
{ {
HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle); HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle);
_realPlayHandle = -1; _realPlayHandle = -1;
} }
// 2. 停止解码并释放播放端口:避免端口资源泄漏 // 2. 停止解码
if (_playPort >= 0) if (_playPort >= 0)
{ {
HikPlayMethods.PlayM4_Stop(_playPort); HikPlayMethods.PlayM4_Stop(_playPort);
@@ -171,14 +144,13 @@ public class HikVideoSource : BaseVideoSource
_playPort = -1; _playPort = -1;
} }
// [Fix Bug Y] 内存释放保护:确保解码回调未在使用内存
lock (_bufferLock) lock (_bufferLock)
{ {
_sharedYuvMat?.Dispose(); _sharedYuvMat = null; _sharedYuvMat?.Dispose(); _sharedYuvMat = null;
_sharedBgrMat?.Dispose(); _sharedBgrMat = null; _sharedBgrMat?.Dispose(); _sharedBgrMat = null;
} }
// 3. 注销登录:先移除路由映射,再释放登录句柄 // 3. 注销登录
if (_userId >= 0) if (_userId >= 0)
{ {
_instances.TryRemove(_userId, out _); _instances.TryRemove(_userId, out _);
@@ -186,76 +158,115 @@ public class HikVideoSource : BaseVideoSource
_userId = -1; _userId = -1;
} }
// 4. 释放帧对象池:清理复用内存
_framePool?.Dispose(); _framePool?.Dispose();
_framePool = null; _framePool = null;
_isPoolReady = false; _isPoolReady = false;
} }
// 5. 减少SDK全局引用计数确保最后一个实例销毁时卸载SDK
HikSdkManager.Uninitialize(); HikSdkManager.Uninitialize();
} }
#endregion #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) --- #region --- (Network Streaming) ---
/// <summary>
/// 开启网络取流:配置预览参数,绑定流数据回调
/// </summary>
/// <returns>取流开启成功返回 true失败返回 false</returns>
private bool StartRealPlay() private bool StartRealPlay()
{ {
var previewInfo = new HikNativeMethods.NET_DVR_PREVIEWINFO var previewInfo = new HikNativeMethods.NET_DVR_PREVIEWINFO
{ {
hPlayWnd = IntPtr.Zero, // 句柄为空SDK不直接渲染通过回调获取原始流数据 hPlayWnd = IntPtr.Zero,
lChannel = _config.ChannelIndex, // 设备通道号(从配置读取) lChannel = _config.ChannelIndex,
dwStreamType = (uint)_config.StreamType, // 码流类型(主码流/子码流,从配置读取) dwStreamType = (uint)_config.StreamType,
bBlocked = false // 非阻塞取流:避免长时间阻塞线程 bBlocked = false
}; };
// 绑定网络流回调接收SDK推送的原始流数据
_realDataCallBack = new HikNativeMethods.REALDATACALLBACK(SafeOnRealDataReceived); _realDataCallBack = new HikNativeMethods.REALDATACALLBACK(SafeOnRealDataReceived);
_realPlayHandle = HikNativeMethods.NET_DVR_RealPlay_V40(_userId, ref previewInfo, _realDataCallBack, IntPtr.Zero); _realPlayHandle = HikNativeMethods.NET_DVR_RealPlay_V40(_userId, ref previewInfo, _realDataCallBack, IntPtr.Zero);
return _realPlayHandle >= 0; return _realPlayHandle >= 0;
} }
/// <summary>
/// 网络流数据回调 (RealDataCallBack)
/// 职责:接收 SDK 原始流数据,系统头用于初始化播放库,流数据用于解码
/// </summary>
private void SafeOnRealDataReceived(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser) private void SafeOnRealDataReceived(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser)
{ {
try try
{ {
// 预览句柄无效时直接返回,避免无效处理 // [优化] 维持心跳,防止被哨兵误杀
MarkFrameReceived(dwBufSize);
if (_realPlayHandle == -1) return; if (_realPlayHandle == -1) return;
// 处理系统头:初始化播放库(仅首次接收系统头时执行) // 处理系统头
if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1) if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1)
{ {
lock (_initLock) lock (_initLock)
{ {
// 双重校验:防止多线程下重复初始化
if (_realPlayHandle == -1 || _playPort != -1) return; if (_realPlayHandle == -1 || _playPort != -1) return;
// [Fix Bug α: 端口抢占] 全局锁保护端口申请,避免并发冲突
DateTime timeStart = DateTime.Now;
bool getPortSuccess; bool getPortSuccess;
lock (_globalPortLock) lock (_globalPortLock)
{ {
getPortSuccess = HikPlayMethods.PlayM4_GetPort(ref _playPort); getPortSuccess = HikPlayMethods.PlayM4_GetPort(ref _playPort);
} }
var useTime = Math.Round((DateTime.Now - timeStart).TotalSeconds, 1);
if (!getPortSuccess) return; 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)) if (!HikPlayMethods.PlayM4_OpenStream(_playPort, pBuffer, dwBufSize, 2 * 1024 * 1024))
{ {
HikPlayMethods.PlayM4_FreePort(_playPort); HikPlayMethods.PlayM4_FreePort(_playPort);
@@ -268,36 +279,32 @@ public class HikVideoSource : BaseVideoSource
HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero); HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero);
} }
} }
// 处理流数据:将原始流数据传入播放库解码 // 处理流数据
else if (dwDataType == HikNativeMethods.NET_DVR_STREAMDATA && _playPort != -1) else if (dwDataType == HikNativeMethods.NET_DVR_STREAMDATA && _playPort != -1)
{ {
HikPlayMethods.PlayM4_InputData(_playPort, pBuffer, dwBufSize); HikPlayMethods.PlayM4_InputData(_playPort, pBuffer, dwBufSize);
} }
} }
catch { /* 吞没回调异常防止回调崩溃导致整个SDK进程退出 */ } catch { }
} }
#endregion #endregion
#region --- (Decoding & Frame Distribution) --- #region --- (Decoding) ---
/// <summary>
/// 解码回调 (DecCallBack)
/// 职责:接收解码后的 YUV 数据,转码为 BGR 格式,通过帧池复用内存并分发
/// </summary>
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2) private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
{ {
// 汇报心跳:更新帧接收时间,防止哨兵判定设备僵死 // 1. [核心流控] 询问基类控制器:这帧要不要?
MarkFrameReceived(); // 之前失效是因为操作的是子类被遮蔽的 Controller现在复用基类 Controller逻辑就通了。
// 1. 帧分发决策:根据订阅者需求判断是否需要保留当前帧(耗时<0.01ms
var decision = Controller.MakeDecision(Environment.TickCount64); var decision = Controller.MakeDecision(Environment.TickCount64);
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
if (!decision.IsCaptured) return; if (!decision.IsCaptured) return;
int width = pFrameInfo.nWidth; int width = pFrameInfo.nWidth;
int height = pFrameInfo.nHeight; int height = pFrameInfo.nHeight;
// 2. 初始化帧池:首次解码时创建,按实际分辨率分配内存 // 2. 初始化帧池
if (!_isPoolReady) if (!_isPoolReady)
{ {
lock (_initLock) lock (_initLock)
@@ -305,7 +312,6 @@ public class HikVideoSource : BaseVideoSource
if (!_isPoolReady) if (!_isPoolReady)
{ {
_framePool?.Dispose(); _framePool?.Dispose();
// 帧池配置CV_8UC3=BGR格式初始3帧最大5帧平衡内存与性能
_framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5); _framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5);
_isPoolReady = true; _isPoolReady = true;
} }
@@ -314,73 +320,54 @@ public class HikVideoSource : BaseVideoSource
if (_framePool == null) return; if (_framePool == null) return;
// 3. 从帧池获取内存零GC分配池满时返回null直接丢帧避免积压 // 3. 转换与分发
SmartFrame smartFrame = _framePool.Get(); SmartFrame smartFrame = _framePool.Get();
try try
{ {
if (smartFrame == null) return; // 池满,丢弃当前 if (smartFrame == null) return; // 池满
try using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
{ {
// 4. YUVBGR:直接写入帧池内存,无中间对象分配 Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
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. 提交到全局处理中心:后续由管道处理二次加工与分发 // 4. [分发] 将决策结果传递给处理中心
// decision.TargetAppIds 包含了 "谁需要这一帧" 的信息
GlobalProcessingCenter.Submit(this.Id, smartFrame, decision); GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
} }
catch (Exception ex)
{
smartFrame.Dispose();
// 这里为了性能不频繁写日志,仅在调试时开启
// Debug.WriteLine(ex.Message);
}
finally finally
{ {
// 释放驱动层引用:驱动职责结束,引用计数-1由消费者/管道管理后续生命周期)
smartFrame.Dispose(); smartFrame.Dispose();
} }
} }
#endregion #endregion
#region --- (Exception Handling) --- #region --- ---
/// <summary>
/// 全局异常回调处理
/// 职责:将 SDK 全局异常(仅带 UserID路由到对应的 HikVideoSource 实例
/// </summary>
private static void StaticOnSdkException(uint dwType, int lUserID, int lHandle, IntPtr pUser) private static void StaticOnSdkException(uint dwType, int lUserID, int lHandle, IntPtr pUser)
{ {
try try
{ {
// 通过 UserID 查找实例,触发实例内异常处理逻辑
if (_instances.TryGetValue(lUserID, out var instance)) if (_instances.TryGetValue(lUserID, out var instance))
{ {
instance.AddAuditLog($"SDK报警异常: 0x{dwType:X}"); // 写入审计
instance.ReportError(new CameraException( instance.ReportError(new CameraException(
CameraErrorCode.NetworkUnreachable, CameraErrorCode.NetworkUnreachable,
$"SDK全局报警异常: 0x{dwType:X}", $"SDK全局异常: 0x{dwType:X}",
DeviceBrand.HikVision)); DeviceBrand.HikVision));
} }
} }
catch { /* 吞没异常:避免全局回调崩溃 */ } catch { }
} }
#endregion #endregion
#region --- (Metadata Fetching) ---
/// <summary>
/// 占位实现:暂未实现设备元数据获取逻辑
/// 注:实际场景需补充,用于获取设备型号、通道能力等信息
/// </summary>
protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata()); protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
#endregion
} }

View File

@@ -110,11 +110,12 @@ namespace SHH.CameraSdk
{ {
Id = 101, Id = 101,
Brand = DeviceBrand.HikVision, Brand = DeviceBrand.HikVision,
// IpAddress = "172.16.41.206", IpAddress = "172.16.41.206",
IpAddress = "192.168.5.9", //IpAddress = "192.168.5.9",
Port = 8000, Port = 8000,
Username = "admin", Username = "admin",
Password = "RRYFOA", Password = "abcd1234",
//Password = "RRYFOA",
StreamType = 0 // 主码流 StreamType = 0 // 主码流
}; };
manager.AddDevice(config); manager.AddDevice(config);
@@ -152,25 +153,25 @@ namespace SHH.CameraSdk
// 投递到渲染线程 (FrameConsumer) // 投递到渲染线程 (FrameConsumer)
renderer.Enqueue(frame); renderer.Enqueue(frame);
Console.WriteLine("Frame Enqueued"); //Console.WriteLine("Frame Enqueued");
}); });
// [消费者 B] - 绝对只会收到 8fps // [消费者 B] - 绝对只会收到 8fps
GlobalStreamDispatcher.Subscribe("Process_B_Local", (deviceId, frame) => GlobalStreamDispatcher.Subscribe("Process_B_Local", (deviceId, frame) =>
{ {
if (deviceId == 101) //if (deviceId == 101)
{ //{
Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)"); // Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)");
} //}
}); });
// [消费者 AI] // [消费者 AI]
GlobalStreamDispatcher.Subscribe("AI_Engine_Core", (deviceId, frame) => GlobalStreamDispatcher.Subscribe("AI_Engine_Core", (deviceId, frame) =>
{ {
if (deviceId == 101) //if (deviceId == 101)
{ //{
Console.WriteLine($" >>> [AI] 分析一帧..."); // Console.WriteLine($" >>> [AI] 分析一帧...");
} //}
}); });
} }
} }