WebAPI 支持摄像头启停控制、码流切换、审计日志的提供
This commit is contained in:
@@ -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
|
||||||
|
|||||||
283
SHH.CameraSdk/Controllers/CamerasController.cs
Normal file
283
SHH.CameraSdk/Controllers/CamerasController.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs
Normal file
23
SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs
Normal 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;
|
||||||
|
}
|
||||||
123
SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs
Normal file
123
SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs
Normal 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;
|
||||||
|
}
|
||||||
96
SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs
Normal file
96
SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs
Normal 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; }
|
||||||
|
}
|
||||||
31
SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
Normal file
31
SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成
|
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成
|
||||||
|
|||||||
@@ -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) ---
|
||||||
|
|||||||
@@ -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] {error}"); }
|
else
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[OptionRejected] 设备 {Id}: {errorMsg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddAuditLog($"动态参数应用失败: {ex.Message}");
|
||||||
|
Debug.WriteLine($"[ApplyOptionsError] {ex.Message}");
|
||||||
}
|
}
|
||||||
catch (Exception ex) { 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
|
||||||
}
|
}
|
||||||
@@ -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 _userId = -1; // SDK 登录句柄
|
||||||
private int _realPlayHandle = -1; // 预览句柄 (网络层,-1 表示未开启预览)
|
private int _realPlayHandle = -1; // 预览句柄
|
||||||
private int _playPort = -1; // 播放端口 (解码层,-1 表示未分配端口)
|
private int _playPort = -1; // 播放端口
|
||||||
|
|
||||||
private readonly object _initLock = new(); // 初始化/清理互斥锁:保护启动/停止流程的原子性
|
private readonly object _initLock = new();
|
||||||
private readonly object _bufferLock = new(); // 解码缓冲区锁 (Fix Bug Y: 防止多线程并发读写内存)
|
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;
|
||||||
|
|
||||||
// 帧需求控制器:管理不同订阅者(UI/AI)的帧率需求,实现按需分发
|
// 【关键修复 Bug Z】: 删除了这里原本的 "public FrameController Controller..."
|
||||||
public FrameController Controller { get; } = new();
|
// 直接使用 BaseVideoSource.Controller
|
||||||
|
|
||||||
#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
|
|
||||||
{
|
|
||||||
// 4. YUV转BGR:直接写入帧池内存,无中间对象分配
|
|
||||||
using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
|
using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
|
||||||
{
|
{
|
||||||
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
|
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 对外分发帧数据:通过基类事件通知订阅者(零拷贝)
|
// 4. [分发] 将决策结果传递给处理中心
|
||||||
RaiseFrameReceived(smartFrame);
|
// decision.TargetAppIds 包含了 "谁需要这一帧" 的信息
|
||||||
|
GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 异常时释放帧:避免内存泄漏
|
|
||||||
smartFrame.Dispose();
|
smartFrame.Dispose();
|
||||||
Debug.WriteLine($"[DecodingError] {ex.Message}");
|
// 这里为了性能不频繁写日志,仅在调试时开启
|
||||||
}
|
// Debug.WriteLine(ex.Message);
|
||||||
|
|
||||||
// 6. 提交到全局处理中心:后续由管道处理二次加工与分发
|
|
||||||
GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
|
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
@@ -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] 分析一帧...");
|
||||||
}
|
//}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user