diff --git a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs
index 4e9eee9..04bee6c 100644
--- a/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs
+++ b/SHH.CameraSdk/Abstractions/Models/VideoSourceConfig.cs
@@ -43,6 +43,9 @@ public class VideoSourceConfig
/// 默认码流类型(0 = 主码流(高清),1 = 子码流(低带宽))
public int StreamType { get; set; } = 0;
+ /// Rtsp 播放路径
+ public string RtspPath { get; set; } = string.Empty;
+
/// 传输协议(TCP/UDP/Multicast,默认 TCP 保证可靠性)
[JsonConverter(typeof(JsonStringEnumConverter))]
public TransportProtocol Transport { get; set; } = TransportProtocol.Tcp;
@@ -132,6 +135,7 @@ public class VideoSourceConfig
Password = this.Password,
RenderHandle = this.RenderHandle,
ChannelIndex = this.ChannelIndex,
+ RtspPath = this.RtspPath,
StreamType = this.StreamType,
Transport = this.Transport,
ConnectionTimeoutMs = this.ConnectionTimeoutMs
diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs
new file mode 100644
index 0000000..cac6691
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/CamerasController.cs
@@ -0,0 +1,283 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace SHH.CameraSdk;
+
+[ApiController]
+[Route("api/[controller]")]
+public class CamerasController : ControllerBase
+{
+ private readonly CameraManager _manager;
+
+ public CamerasController(CameraManager manager)
+ {
+ _manager = manager;
+ }
+
+ // ==========================================================================
+ // 区域 A: 设备全生命周期管理 (CRUD)
+ // ==========================================================================
+
+ ///
+ /// 1. 获取所有设备清单
+ ///
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ var devices = _manager.GetAllDevices().Select(d => new
+ {
+ d.Id,
+ d.Config.IpAddress,
+ d.Config.Name,
+ Status = d.Status.ToString(),
+ d.RealFps,
+ d.TotalFrames
+ });
+ return Ok(devices);
+ }
+
+ ///
+ /// 2. 新增设备 (写入配置并初始化)
+ ///
+ [HttpPost]
+ public IActionResult Add([FromBody] CameraConfigDto dto)
+ {
+ if (_manager.GetDevice(dto.Id) != null)
+ return Conflict($"设备ID {dto.Id} 已存在");
+
+ // DTO 转 Config (实际项目中建议用 AutoMapper)
+ var config = MapToConfig(dto);
+
+ _manager.AddDevice(config); // 添加到内存池
+ // 注意:此时 IsRunning 默认为 false,等待手动 Start 或 API 控制
+
+ return CreatedAtAction(nameof(GetAll), new { id = dto.Id }, dto);
+ }
+
+ ///
+ /// 3. 编辑设备 (自动识别冷热更新)
+ ///
+ [HttpPut("{id}")]
+ public async Task Update(long id, [FromBody] CameraConfigDto dto)
+ {
+ try
+ {
+ if (id != dto.Id) return BadRequest("ID 不匹配");
+
+ // 调用 Manager 的智能更新逻辑 (之前实现的 UpdateDeviceConfigAsync)
+ await _manager.UpdateDeviceConfigAsync(id, MapToUpdateDto(dto));
+
+ return Ok(new { Success = true, Message = "配置已更新" });
+ }
+ catch (KeyNotFoundException) { return NotFound(); }
+ catch (System.Exception ex) { return StatusCode(500, ex.Message); }
+ }
+
+ ///
+ /// 4. 删除设备 (销毁连接)
+ ///
+ [HttpDelete("{id}")]
+ public async Task Remove(long id)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ await device.StopAsync(); // 停流
+ _manager.RemoveDevice(id); // 从池中移除
+
+ return Ok($"设备 {id} 已移除");
+ }
+
+ // ==========================================================================
+ // 区域 B: 多进程流控订阅 (Subscription Strategy)
+ // ==========================================================================
+
+ ///
+ /// 5. 注册/更新进程的流需求 (A/B/C/D 场景核心)
+ ///
+ ///
+ /// 示例场景:
+ /// - 主进程配置(B): { "appId": "Main_Config", "displayFps": 25, "analysisFps": 0 }
+ /// - AI进程(C): { "appId": "AI_Core", "displayFps": 0, "analysisFps": 5 }
+ ///
+ [HttpPost("{id}/subscriptions")]
+ public IActionResult UpdateSubscription(long id, [FromBody] SubscriptionDto sub)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ // 逻辑转换:将 "显示帧" 和 "分析帧" 映射到底层控制器的注册表
+
+ // 1. 处理显示需求
+ string displayKey = $"{sub.AppId}_Display";
+ if (sub.DisplayFps > 0)
+ {
+ // 告诉控制器:这个 App 需要 X 帧用于显示
+ device.Controller.Register(displayKey, sub.DisplayFps);
+ }
+ else
+ {
+ // 如果不需要,移除注册
+ device.Controller.Unregister(displayKey);
+ }
+
+ // 2. 处理分析需求
+ string analysisKey = $"{sub.AppId}_Analysis";
+ if (sub.AnalysisFps > 0)
+ {
+ // 告诉控制器:这个 App 需要 Y 帧用于分析
+ device.Controller.Register(analysisKey, sub.AnalysisFps);
+ }
+ else
+ {
+ device.Controller.Unregister(analysisKey);
+ }
+
+ // 运维审计
+ device.AddAuditLog($"更新订阅策略 [{sub.AppId}]: Display={sub.DisplayFps}, Analysis={sub.AnalysisFps}");
+
+ return Ok(new { Message = "订阅策略已更新", DeviceId = id });
+ }
+
+ // ==========================================================================
+ // 区域 C: 句柄动态绑定 (Handle Binding)
+ // ==========================================================================
+
+ ///
+ /// 6. 绑定显示窗口 (对应 A进程-句柄场景)
+ ///
+ [HttpPost("{id}/bind-handle")]
+ public IActionResult BindHandle(long id, [FromBody] BindHandleDto dto)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ // 构造动态选项,应用句柄
+ var options = new DynamicStreamOptions
+ {
+ RenderHandle = (System.IntPtr)dto.Handle
+ };
+
+ device.ApplyOptions(options); // 触发驱动层的 OnApplyOptions
+
+ device.AddAuditLog($"绑定新句柄: {dto.Handle} ({dto.Purpose})");
+
+ return Ok(new { Success = true });
+ }
+
+ // ==========================================================================
+ // 区域 D: 设备运行控制
+ // ==========================================================================
+
+ ///
+ /// 手动控制设备运行状态 (开关机)
+ ///
+ [HttpPost("{id}/power")]
+ public async Task TogglePower(long id, [FromQuery] bool enabled)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ // 1. 更新运行意图
+ device.IsRunning = enabled;
+
+ // 2. 审计与执行
+ if (enabled)
+ {
+ device.AddAuditLog("用户指令:手动开启设备");
+ // 异步启动,Coordinator 也会在下个周期辅助检查
+ _ = device.StartAsync();
+ }
+ else
+ {
+ device.AddAuditLog("用户指令:手动关闭设备");
+ await device.StopAsync();
+ }
+
+ return Ok(new { DeviceId = id, IsRunning = enabled });
+ }
+
+ ///
+ /// 热应用动态参数 (如切换码流)
+ ///
+ [HttpPost("{id}/options")]
+ public IActionResult ApplyOptions(long id, [FromBody] DynamicStreamOptions options)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ // 1. 如果涉及码流切换,先同步更新配置对象
+ if (options.StreamType.HasValue)
+ {
+ var newConfig = device.Config.DeepCopy();
+ newConfig.StreamType = options.StreamType.Value;
+ device.UpdateConfig(newConfig);
+ }
+
+ // 2. 应用到驱动层(触发热切换逻辑)
+ device.ApplyOptions(options);
+
+ return Ok(new { Message = "动态参数已发送", DeviceId = id });
+ }
+
+ // ==========================================================================
+ // 辅助方法 (Mapping)
+ // ==========================================================================
+ private VideoSourceConfig MapToConfig(CameraConfigDto dto)
+ {
+ return new VideoSourceConfig
+ {
+ Id = dto.Id,
+ Name = dto.Name,
+ Brand = (DeviceBrand)dto.Brand,
+ IpAddress = dto.IpAddress,
+ Port = dto.Port,
+ Username = dto.Username,
+ Password = dto.Password,
+ ChannelIndex = dto.ChannelIndex,
+ StreamType = dto.StreamType,
+ RtspPath = dto.RtspPath
+ };
+ }
+
+ ///
+ /// [辅助方法] 将全量配置 DTO 转换为更新 DTO
+ ///
+ private DeviceUpdateDto MapToUpdateDto(CameraConfigDto dto)
+ {
+ return new DeviceUpdateDto
+ {
+ // ==========================================
+ // 1. 冷更新参数 (基础连接信息)
+ // ==========================================
+ IpAddress = dto.IpAddress,
+ Port = dto.Port,
+ Username = dto.Username,
+ Password = dto.Password,
+ ChannelIndex = dto.ChannelIndex,
+ Brand = dto.Brand,
+ RtspPath = dto.RtspPath,
+ MainboardIp = dto.MainboardIp,
+ MainboardPort = dto.MainboardPort,
+
+ // ==========================================
+ // 2. 热更新参数 (运行时属性)
+ // ==========================================
+ Name = dto.Name,
+ Location = dto.Location,
+ StreamType = dto.StreamType,
+
+ // 注意:通常句柄是通过 bind-handle 接口单独绑定的,
+ // 但如果 ConfigDto 里包含了上次保存的句柄,也可以映射
+ // RenderHandle = dto.RenderHandle,
+
+ // ==========================================
+ // 3. 图像处理参数
+ // ==========================================
+ AllowCompress = dto.AllowCompress,
+ AllowExpand = dto.AllowExpand,
+ TargetResolution = dto.TargetResolution,
+ EnhanceImage = dto.EnhanceImage,
+ UseGrayscale = dto.UseGrayscale
+ };
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs b/SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs
new file mode 100644
index 0000000..0e6cf84
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/Dto/BindHandleDto.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SHH.CameraSdk;
+
+///
+/// 句柄绑定 DTO
+/// 用于前端向后端传递窗口渲染句柄,实现视频流的硬件解码渲染
+///
+public class BindHandleDto
+{
+ ///
+ /// 窗口句柄 (IntPtr 转换为 long 类型传输,避免跨平台序列化问题)
+ ///
+ [Required(ErrorMessage = "渲染窗口句柄不能为空")]
+ [Range(1, long.MaxValue, ErrorMessage = "句柄必须为有效的非负整数")]
+ public long Handle { get; set; }
+
+ ///
+ /// 用途描述 (用于审计日志,如 "Main_Preview"、"AI_Analysis_Window")
+ ///
+ [MaxLength(64, ErrorMessage = "用途描述长度不能超过 64 个字符")]
+ public string Purpose { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs b/SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs
new file mode 100644
index 0000000..f661119
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/Dto/CameraConfigDto.cs
@@ -0,0 +1,123 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SHH.CameraSdk;
+
+// ==============================================================================
+// 1. 物理与运行配置 DTO (对应 CRUD 操作)
+// 用于设备新增/全量配置查询,包含基础身份、连接信息、运行参数等全量字段
+// ==============================================================================
+public class CameraConfigDto
+{
+ // --- 基础身份 (Identity) ---
+ ///
+ /// 设备唯一标识
+ ///
+ [Required(ErrorMessage = "设备ID不能为空")]
+ [Range(1, long.MaxValue, ErrorMessage = "设备ID必须为正整数")]
+ public long Id { get; set; }
+
+ ///
+ /// 设备友好名称
+ ///
+ [MaxLength(64, ErrorMessage = "设备名称长度不能超过64个字符")]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 摄像头品牌类型 (0:HikVision, 1:Dahua, 2:RTSP...)
+ ///
+ [Range(0, 10, ErrorMessage = "品牌类型值必须在0-10范围内")]
+ public int Brand { get; set; }
+
+ ///
+ /// 设备安装位置描述
+ ///
+ [MaxLength(128, ErrorMessage = "安装位置长度不能超过128个字符")]
+ public string Location { get; set; } = string.Empty;
+
+ // --- 核心连接 (Connectivity) - 修改此类参数触发冷重启 ---
+ ///
+ /// 摄像头IP地址
+ ///
+ [Required(ErrorMessage = "IP地址不能为空")]
+ [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$",
+ ErrorMessage = "请输入合法的IPv4地址")]
+ public string IpAddress { get; set; } = string.Empty;
+
+ ///
+ /// SDK端口 (如海康默认8000)
+ ///
+ [Range(1, 65535, ErrorMessage = "端口号必须在1-65535范围内")]
+ public ushort Port { get; set; } = 8000;
+
+ ///
+ /// 登录用户名
+ ///
+ [MaxLength(32, ErrorMessage = "用户名长度不能超过32个字符")]
+ public string Username { get; set; } = string.Empty;
+
+ ///
+ /// 登录密码
+ ///
+ [MaxLength(64, ErrorMessage = "密码长度不能超过64个字符")]
+ public string Password { get; set; } = string.Empty;
+
+ ///
+ /// 通道号 (通常为1)
+ ///
+ [Range(1, 32, ErrorMessage = "通道号必须在1-32范围内")]
+ public int ChannelIndex { get; set; } = 1;
+
+ ///
+ /// RTSP流路径 (备用或非SDK模式使用)
+ ///
+ [MaxLength(256, ErrorMessage = "RTSP地址长度不能超过256个字符")]
+ public string RtspPath { get; set; } = string.Empty;
+
+ // --- 主板关联信息 (Metadata) ---
+ ///
+ /// 关联主板IP地址
+ ///
+ [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)?$",
+ ErrorMessage = "请输入合法的IPv4地址")]
+ public string MainboardIp { get; set; } = string.Empty;
+
+ ///
+ /// 关联主板端口
+ ///
+ [Range(1, 65535, ErrorMessage = "主板端口号必须在1-65535范围内")]
+ public int MainboardPort { get; set; } = 80;
+
+ // --- 运行时参数 (Runtime Options) - 支持热更新 ---
+ ///
+ /// 码流类型 (0:主码流, 1:子码流)
+ ///
+ [Range(0, 1, ErrorMessage = "码流类型只能是0(主码流)或1(子码流)")]
+ public int StreamType { get; set; } = 0;
+
+ ///
+ /// 是否使用灰度图 (用于AI分析场景加速)
+ ///
+ public bool UseGrayscale { get; set; } = false;
+
+ ///
+ /// 是否启用图像增强 (去噪/锐化等)
+ ///
+ public bool EnhanceImage { get; set; } = true;
+
+ // --- 画面变换 (Transform) - 支持热更新 ---
+ ///
+ /// 是否允许图像压缩 (降低带宽占用)
+ ///
+ public bool AllowCompress { get; set; } = true;
+
+ ///
+ /// 是否允许图像放大 (提升渲染质量)
+ ///
+ public bool AllowExpand { get; set; } = false;
+
+ ///
+ /// 目标分辨率 (格式如 1920x1080,空则保持原图)
+ ///
+ [RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")]
+ public string TargetResolution { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs b/SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs
new file mode 100644
index 0000000..fb23efc
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/Dto/DeviceUpdateDto.cs
@@ -0,0 +1,96 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SHH.CameraSdk;
+
+///
+/// 设备配置更新传输对象
+/// 用于接收前端的编辑请求,支持部分更新(字段为 null 表示不修改)
+/// 自动区分 冷更新参数(需重启连接)和 热更新参数(无感生效)
+///
+public class DeviceUpdateDto
+{
+ // ==============================================================================
+ // 1. 冷更新参数 (Cold Update)
+ // 修改此类参数涉及物理连接变更,后端会自动执行 "Stop -> Update -> Start" 流程
+ // ==============================================================================
+
+ /// 摄像头IP地址
+ [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$",
+ ErrorMessage = "请输入合法的IPv4地址")]
+ public string? IpAddress { get; set; }
+
+ /// SDK端口 (如海康 8000)
+ [Range(1, 65535, ErrorMessage = "端口号必须在 1-65535 范围内")]
+ public ushort? Port { get; set; }
+
+ /// 登录用户名
+ [MaxLength(32, ErrorMessage = "用户名长度不能超过 32 个字符")]
+ public string? Username { get; set; }
+
+ /// 登录密码
+ [MaxLength(64, ErrorMessage = "密码长度不能超过 64 个字符")]
+ public string? Password { get; set; }
+
+ /// 通道号 (默认 1)
+ [Range(1, 32, ErrorMessage = "通道号必须在 1-32 范围内")]
+ public int? ChannelIndex { get; set; }
+
+ /// 摄像头品牌类型 (0:Hik, 1:Dahua...)
+ [Range(0, 10, ErrorMessage = "品牌类型值必须在 0-10 范围内")]
+ public int? Brand { get; set; }
+
+ /// RTSP流地址 (非SDK模式下使用)
+ [MaxLength(256, ErrorMessage = "RTSP地址长度不能超过 256 个字符")]
+ public string? RtspPath { get; set; }
+
+ /// 关联的主板IP (用于联动控制)
+ [RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)?$",
+ ErrorMessage = "请输入合法的IPv4地址")]
+ public string? MainboardIp { get; set; }
+
+ /// 关联的主板端口
+ [Range(1, 65535, ErrorMessage = "主板端口号必须在 1-65535 范围内")]
+ public int? MainboardPort { get; set; }
+
+ // ==============================================================================
+ // 2. 热更新参数 (Hot Update)
+ // 修改此类参数仅刷新运行时状态,不中断物理连接,无感生效
+ // ==============================================================================
+
+ /// 设备名称/别名
+ [MaxLength(64, ErrorMessage = "设备名称长度不能超过 64 个字符")]
+ public string? Name { get; set; }
+
+ /// 安装位置描述
+ [MaxLength(128, ErrorMessage = "安装位置长度不能超过 128 个字符")]
+ public string? Location { get; set; }
+
+ /// 码流类型 (0:主码流, 1:子码流)
+ [Range(0, 1, ErrorMessage = "码流类型只能是 0(主码流) 或 1(子码流)")]
+ public int? StreamType { get; set; }
+
+ /// 渲染句柄 (IntPtr 的 Long 形式)
+ [Range(0, long.MaxValue, ErrorMessage = "渲染句柄必须是非负整数")]
+ public long? RenderHandle { get; set; }
+
+ // ==============================================================================
+ // 3. 图像处理参数 (Image Processing - Hot Update)
+ // 影响解码后的 SmartFrame 数据格式
+ // ==============================================================================
+
+ /// 是否允许压缩 (影响传输带宽)
+ public bool? AllowCompress { get; set; }
+
+ /// 是否允许放大 (影响渲染质量)
+ public bool? AllowExpand { get; set; }
+
+ /// 目标分辨率 (格式如 "1920x1080",空则保持原图)
+ [RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")]
+ public string? TargetResolution { get; set; }
+
+ /// 是否启用图像增强 (去噪/锐化等)
+ public bool? EnhanceImage { get; set; }
+
+ /// 是否转为灰度图 (用于 AI 纯分析场景加速)
+ public bool? UseGrayscale { get; set; }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs b/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
new file mode 100644
index 0000000..2460736
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
@@ -0,0 +1,31 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SHH.CameraSdk;
+
+// ==============================================================================
+// 2. 订阅策略 DTO (对应 A/B/C/D 进程需求)
+// 用于多进程帧需求的注册与更新,支持显示帧和分析帧的独立配置
+// ==============================================================================
+public class SubscriptionDto
+{
+ ///
+ /// 进程唯一标识 (如 "AI_Process_01"、"Main_Display_02")
+ ///
+ [Required(ErrorMessage = "进程标识 AppId 不能为空")]
+ [MaxLength(50, ErrorMessage = "AppId 长度不能超过 50 个字符")]
+ public string AppId { get; set; } = string.Empty;
+
+ ///
+ /// 显示帧率需求 (单位: fps)
+ /// 不需要显示则设为 0,控制器会自动注销该类型需求
+ ///
+ [Range(0, 60, ErrorMessage = "显示帧率需在 0-60 fps 范围内")]
+ public int DisplayFps { get; set; }
+
+ ///
+ /// 分析帧率需求 (单位: fps)
+ /// 不需要分析则设为 0,控制器会自动注销该类型需求
+ ///
+ [Range(0, 30, ErrorMessage = "分析帧率需在 0-30 fps 范围内")]
+ public int AnalysisFps { get; set; }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs
index 2d5420a..a14b243 100644
--- a/SHH.CameraSdk/Controllers/MonitorController.cs
+++ b/SHH.CameraSdk/Controllers/MonitorController.cs
@@ -96,5 +96,28 @@ public class MonitorController : ControllerBase
return File(imageBytes, "image/jpeg");
}
+ ///
+ /// 获取指定相机的诊断信息(含审计日志)
+ ///
+ /// 相机设备唯一标识
+ /// 200 OK + 诊断信息 | 404 Not Found
+ [HttpGet("diagnose/{id}")]
+ public IActionResult GetDeviceDiagnostic(long id)
+ {
+ var device = _cameraManager.GetDevice(id);
+ if (device == null) return NotFound();
+
+ return Ok(new
+ {
+ Id = device.Id,
+ Status = device.Status.ToString(),
+ RealFps = device.RealFps,
+ TotalFrames = device.TotalFrames,
+ // 关键:将 BaseVideoSource 中的日志列表返回给前端
+ // 注意:属性名 AuditLogs 会被序列化为 auditLogs (首字母小写),符合前端预期
+ AuditLogs = device.GetAuditLogs()
+ });
+ }
+
#endregion
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs
index c2cfd3d..dd82281 100644
--- a/SHH.CameraSdk/Core/Manager/CameraManager.cs
+++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs
@@ -72,6 +72,37 @@ public class CameraManager : IDisposable, IAsyncDisposable
public BaseVideoSource? GetDevice(long id)
=> _cameraPool.TryGetValue(id, out var source) ? source : null;
+ ///
+ /// 获取当前管理的所有相机设备
+ ///
+ /// 设备实例集合
+ public IEnumerable GetAllDevices()
+ {
+ return _cameraPool.Values.ToList();
+ }
+
+ ///
+ /// 从管理池中移除指定设备并释放资源
+ ///
+ /// 设备唯一标识
+ public void RemoveDevice(long id)
+ {
+ if (_cameraPool.TryRemove(id, out var device))
+ {
+ // 记录日志
+ System.Console.WriteLine($"[Manager] 正在移除设备 {id}...");
+
+ // 1. 停止物理连接 (异步转同步等待,防止资源未释放)
+ // 在实际高并发场景建议改为 RemoveDeviceAsync
+ device.StopAsync().GetAwaiter().GetResult();
+
+ // 2. 释放资源 (销毁非托管句柄)
+ device.Dispose();
+
+ System.Console.WriteLine($"[Manager] 设备 {id} 已彻底移除");
+ }
+ }
+
#endregion
#region --- 3. 生命周期控制 (Engine Lifecycle) ---
@@ -82,13 +113,14 @@ public class CameraManager : IDisposable, IAsyncDisposable
public async Task StartAsync()
{
// 防护:已销毁则抛出异常
- if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager));
+ if (_isDisposed) throw new System.ObjectDisposedException(nameof(CameraManager));
// 防护:避免重复启动
if (_isEngineStarted) return;
// 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境
HikSdkManager.Initialize();
+ // 不要运行,手动运行
//// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程
//foreach (var source in _cameraPool.Values)
//{
@@ -105,7 +137,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
- Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
+ System.Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
await Task.CompletedTask;
}
@@ -168,14 +200,93 @@ public class CameraManager : IDisposable, IAsyncDisposable
TotalFrames = cam.TotalFrames,
HealthScore = healthScore,
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
- Timestamp = DateTime.Now
+ Timestamp = System.DateTime.Now
};
}).ToList();
}
#endregion
- #region --- 5. 资源清理 (Disposal) ---
+ #region --- 5. 配置热更新 (Config Hot Update) ---
+
+ ///
+ /// 智能更新设备配置 (含冷热分离逻辑)
+ ///
+ /// 设备唯一标识
+ /// 配置更新传输对象
+ /// 设备不存在时抛出
+ public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto)
+ {
+ if (!_cameraPool.TryGetValue(deviceId, out var device))
+ throw new KeyNotFoundException($"设备 {deviceId} 不存在");
+
+ // 1. 审计
+ device.AddAuditLog("收到配置更新请求");
+
+ // 2. 创建副本进行对比
+ var oldConfig = device.Config;
+ var newConfig = oldConfig.DeepCopy();
+
+ // 3. 映射 DTO 值 (仅当不为空时修改)
+ if (dto.IpAddress != null) newConfig.IpAddress = dto.IpAddress;
+ if (dto.Port != null) newConfig.Port = dto.Port.Value;
+ if (dto.Username != null) newConfig.Username = dto.Username;
+ if (dto.Password != null) newConfig.Password = dto.Password;
+ if (dto.ChannelIndex != null) newConfig.ChannelIndex = dto.ChannelIndex.Value;
+ if (dto.StreamType != null) newConfig.StreamType = dto.StreamType.Value;
+ if (dto.Name != null) newConfig.Name = dto.Name;
+ if (dto.RenderHandle != null) newConfig.RenderHandle = (System.IntPtr)dto.RenderHandle.Value;
+
+ // 4. 判定冷热更新
+ // 核心参数变更 -> 冷重启
+ bool needColdRestart =
+ newConfig.IpAddress != oldConfig.IpAddress ||
+ newConfig.Port != oldConfig.Port ||
+ newConfig.Username != oldConfig.Username ||
+ newConfig.Password != oldConfig.Password ||
+ newConfig.ChannelIndex != oldConfig.ChannelIndex ||
+ newConfig.Brand != oldConfig.Brand;
+
+ if (needColdRestart)
+ {
+ device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)");
+
+ // 记录之前的运行状态
+ bool wasRunning = device.IsRunning;
+
+ // A. 彻底停止
+ if (device.IsOnline) await device.StopAsync();
+
+ // B. 写入新配置
+ device.UpdateConfig(newConfig);
+
+ // C. 如果之前是运行意图,则自动重启连接
+ if (wasRunning) await device.StartAsync();
+ }
+ else
+ {
+ device.AddAuditLog($"检测到运行时参数变更,执行热更新 (HotSwap)");
+
+ // A. 更新配置数据
+ device.UpdateConfig(newConfig);
+
+ // B. 在线应用策略 (无需断线)
+ if (device.IsOnline)
+ {
+ var options = new DynamicStreamOptions
+ {
+ StreamType = dto.StreamType,
+ RenderHandle = dto.RenderHandle.HasValue ? (System.IntPtr)dto.RenderHandle : null
+ };
+ // 触发驱动层的 OnApplyOptions
+ device.ApplyOptions(options);
+ }
+ }
+ }
+
+ #endregion
+
+ #region --- 6. 资源清理 (Disposal) ---
///
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成
diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs
index f76c03b..9304c42 100644
--- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs
+++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs
@@ -38,6 +38,18 @@ public class FrameController
});
}
+ ///
+ /// [新增] 注销订阅者的帧需求
+ /// 功能:移除指定订阅者的配置,该订阅者将不再收到任何分发帧
+ ///
+ /// 订阅者唯一标识
+ public void Unregister(string appId)
+ {
+ // ConcurrentDictionary.TryRemove 是原子的、线程安全的
+ // out _ 表示我们要丢弃移除出的对象,因为我们只关心移除动作本身
+ _requirements.TryRemove(appId, out _);
+ }
+
#endregion
#region --- 帧决策生成 (Frame Decision Generation) ---
diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs
index 67b5bd0..c372cb9 100644
--- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs
+++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs
@@ -1,118 +1,162 @@
-using System.Threading.Channels;
-
-namespace SHH.CameraSdk;
+namespace SHH.CameraSdk;
///
/// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版)
-/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。
-/// 修复记录:
-/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。
-/// 2. [Bug π] 管道安全:Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。
-/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。
+/// 核心职责:
+/// 1. 提供线程安全的生命周期管理(启动/停止/销毁)
+/// 2. 实现状态变更的可靠分发与异常隔离
+/// 3. 支持配置热更新与动态参数应用
+/// 4. 内置 FPS/码率统计、心跳保活、审计日志能力
+/// 关键修复记录:
+/// ✅ [Bug A] 死锁免疫:所有 await 均添加 ConfigureAwait(false),解除 UI 线程依赖
+/// ✅ [Bug π] 管道安全:Dispose 采用优雅关闭策略,确保剩余状态消息被消费
+/// ✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑
///
public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
{
- #region --- 核心配置与锁机制 (Core Config & Locks) ---
+ #region --- 1. 核心配置与锁机制 (Core Config & Locks) ---
- // [Fix Bug δ] 核心配置对象
- // 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例
+ ///
+ /// 核心配置对象(支持热更新,去除 readonly 修饰符)
+ /// 注意:外部修改需通过 UpdateConfig 方法,确保线程安全
+ ///
protected VideoSourceConfig _config;
- ///
- /// 状态同步锁
- /// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致的状态读取不一致
+ ///
+ /// 状态同步锁
+ /// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致状态不一致
///
private readonly object _stateSyncRoot = new();
- ///
- /// 生命周期互斥锁
- /// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致的状态机混乱
+ ///
+ /// 生命周期互斥锁(信号量)
+ /// 作用:确保 StartAsync/StopAsync/UpdateConfig 串行执行,防止重入导致状态机混乱
+ /// 配置:初始计数 1,最大计数 1 → 互斥锁
///
private readonly SemaphoreSlim _lifecycleLock = new(1, 1);
#endregion
- #region --- 内部状态与基础设施 (Internal States & Infrastructure) ---
+ #region --- 2. 内部状态与基础设施 (Internal States & Infrastructure) ---
- // 内部状态标志位
+ /// 设备在线状态标志(volatile 确保多线程可见性)
private volatile bool _isOnline;
+
+ /// 视频源核心状态(受 _stateSyncRoot 保护)
private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
- ///
- /// 状态通知队列 (有界)
- /// 特性:采用 DropOldest 策略,当消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β]
+ ///
+ /// 状态通知有界通道
+ /// 特性:DropOldest 策略,消费者过载时丢弃旧状态,防止内存溢出
+ /// 配置:容量 100 | 单读者多写者
///
private readonly Channel _statusQueue;
- // 状态分发器的取消令牌源
+ /// 状态分发器的取消令牌源
private CancellationTokenSource? _distributorCts;
- // [新增修复 Bug π] 分发任务引用
- // 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费
+ /// 状态分发任务引用(用于 Dispose 时优雅等待)
private Task? _distributorTask;
- // [Fix Bug V] 单调时钟
- // 作用:记录最后一次收到帧的系统 Tick,用于心跳检测,不受系统时间修改影响
+ /// 最后一帧接收的系统 Tick(单调时钟,不受系统时间修改影响)
private long _lastFrameTick = 0;
- /// 获取最后帧的时间戳 (线程安全读取)
- public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
-
- /// 视频帧回调事件 (热路径)
+ /// 视频帧回调事件(热路径,低延迟分发)
public event Action