using Ayay.SerilogLogs; using Newtonsoft.Json.Linq; using Serilog; using SHH.CameraSdk; using SHH.Contracts; namespace SHH.CameraService; /// /// 同步设备配置处理器 /// public class DeviceConfigHandler : ICommandHandler { private ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); private readonly CameraManager _cameraManager; /// /// 命令名称 /// public string ActionName => ProtocolCodes.Sync_Camera; /// /// 构造函数 /// /// public DeviceConfigHandler(CameraManager cameraManager) { _cameraManager = cameraManager; } /// /// 执行处理 /// /// /// public async Task ExecuteAsync(JToken payload) { // 1. 反序列化配置 DTO var dto = payload.ToObject(); if (dto == null) return; // 2. 尝试获取现有设备 var device = _cameraManager.GetDevice(dto.Id); string op = device != null ? "更新" : "新增"; string changeSummary = string.Empty; if (device != null) { var old = device.Config; var sb = new System.Text.StringBuilder(); // 1. 物理参数审计 (冷更新判定) if (dto.IpAddress != old.IpAddress) sb.Append($"IP:{old.IpAddress}->{dto.IpAddress}; "); if (dto.Port != old.Port) sb.Append($"Port:{old.Port}->{dto.Port}; "); if (dto.Username != old.Username) sb.Append($"User:{old.Username}->{dto.Username}; "); if (dto.Password != old.Password) sb.Append("密码:[已变更]; "); if (dto.ChannelIndex != old.ChannelIndex) sb.Append($"通道:{old.ChannelIndex}->{dto.ChannelIndex}; "); // 2. 运行意图审计 (播放/停止) // Modified: 明确呈现播放状态的切换 if (dto.ImmediateExecution != device.IsRunning) sb.Append($"运行状态:{(device.IsRunning ? "播放" : "停止")}->{(dto.ImmediateExecution ? "播放" : "停止")}; "); // 3. 图像参数审计 if (dto.StreamType != old.StreamType) sb.Append($"码流:{old.StreamType}->{dto.StreamType}; "); if (dto.UseGrayscale) sb.Append("灰度模式:开启; "); // 4. 订阅策略深度审计 (使用新增的强类型方法) // Optimized: 通过 AppId 匹配,找出 FPS 变动 if (dto.AutoSubscriptions != null) { var currentReqs = device.Controller?.GetRequirements(); if (currentReqs != null) { foreach (var newSub in dto.AutoSubscriptions) { var matched = currentReqs.FirstOrDefault(x => x.AppId == newSub.AppId); if (matched != null) { if (matched.TargetFps != newSub.TargetFps || (int)matched.Type != newSub.Type) { sb.Append($"[订阅变动:{newSub.AppId}] FPS:{matched.TargetFps}->{newSub.TargetFps}; "); } } else { sb.Append($"[新增订阅:{newSub.AppId}] FPS:{newSub.TargetFps}; "); } } } } changeSummary = sb.Length > 0 ? $" | 变更明目: {sb.ToString().TrimEnd(' ', ';')}" : " | 配置一致"; } _sysLog.Information($"[Sync] 即将{op}设备配置, 新配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}"); _sysLog.Debug($"[Sync] 即将{op}设备配置, 新配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} 详情:" + "{@dto}", dto, dto.AutoSubscriptions); if (!string.IsNullOrEmpty(changeSummary)) _sysLog.Warning($"[Sync] 即将{op}设备配置, ID:{dto.Id} 变更项 => {changeSummary}"); if (device != null) { // ========================================================= // 场景 A: 设备已存在 -> 执行智能更新 (Smart Update) // ========================================================= // 将全量配置映射为部分更新 DTO var updateDto = new DeviceUpdateDto { // --- 冷更新参数 (变更会触发重启) --- IpAddress = dto.IpAddress, Port = dto.Port, Username = dto.Username, Password = dto.Password, ChannelIndex = dto.ChannelIndex, Brand = dto.Brand, RtspPath = dto.RtspPath, RenderHandle = dto.RenderHandle, // long 类型直接赋值 // --- 热更新参数 (变更立即生效) --- Name = dto.Name, Location = dto.Location, StreamType = dto.StreamType, MainboardIp = dto.MainboardIp, MainboardPort = dto.MainboardPort, // --- 图像处理参数 (热更新) --- AllowCompress = dto.AllowCompress, AllowExpand = dto.AllowExpand, TargetResolution = dto.TargetResolution, EnhanceImage = dto.EnhanceImage, UseGrayscale = dto.UseGrayscale }; // 调用 Manager 的核心更新逻辑 (它会自动判断是 Stop->Start 还是直接应用) await _cameraManager.UpdateDeviceConfigAsync(dto.Id, updateDto); } else { // ========================================================= // 场景 B: 设备不存在 -> 执行新增 (Add New) // ========================================================= // 构造全新的设备配置 var newConfig = new VideoSourceConfig { Id = dto.Id, Name = dto.Name, Brand = (DeviceBrand)dto.Brand, // int -> Enum 强转 IpAddress = dto.IpAddress, Port = dto.Port, Username = dto.Username, Password = dto.Password, ChannelIndex = dto.ChannelIndex, StreamType = dto.StreamType, RtspPath = dto.RtspPath, MainboardIp = dto.MainboardIp, MainboardPort = dto.MainboardPort, RenderHandle = (IntPtr)dto.RenderHandle, // long -> IntPtr 转换 ConnectionTimeoutMs = 5000 // 默认超时 }; // 添加到管理器池 _cameraManager.AddDevice(newConfig); // 重新获取引用以进行后续操作 device = _cameraManager.GetDevice(dto.Id); } // ★★★ 核心修复:统一处理“运行意图” ★★★ if (device != null) { // 将 DTO 的立即执行标志直接同步给设备的运行意图 device.IsRunning = dto.ImmediateExecution; if (dto.ImmediateExecution) { // 情况 1: 收到“启动”指令 if (!device.IsActived) // 只有没在线时才点火 { // 必须在线再执行 if (device.IsPhysicalOnline) { _sysLog.Warning($"[Sync] 设备立即启动 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}"); _ = device.StartAsync(); } } } else { // 情况 2: 收到“停止”指令 (即 ImmediateExecution = false) if (device.IsActived) // 只有在线时才熄火 { _sysLog.Warning($"[Sync] 设备立即停止 {dto.Id}"); _ = device.StopAsync(); } } } // ========================================================= // 3. 处理自动订阅策略 (Auto Subscriptions) // ========================================================= // 无论新增还是更新,都确保订阅策略是最新的 if (device != null && dto.AutoSubscriptions != null) { var controller = device.Controller; if (controller != null) { foreach (var sub in dto.AutoSubscriptions) { // 如果没有 AppId,生成一个临时的(通常 Dashboard 会下发固定的 AppId) string appId = string.IsNullOrWhiteSpace(sub.AppId) ? $"AUTO_{Guid.NewGuid().ToString("N")[..8]}" : sub.AppId; // 构造流控需求 var req = new FrameRequirement { AppId = appId, TargetFps = sub.TargetFps, Type = (SubscriptionType)sub.Type, // int -> Enum Memo = sub.Memo ?? "Sync Auto", // 自动订阅通常不包含具体的 Handle 或 SavePath,除非协议里带了 // 如果需要支持网络转发,这里可以扩展映射 sub.TargetIp 等 Handle = "", SavePath = "" }; // 注册到帧控制器 controller.Register(req); } } } } }