2026-01-16 07:23:56 +08:00
|
|
|
|
using Ayay.SerilogLogs;
|
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
using Serilog;
|
2026-01-12 18:27:58 +08:00
|
|
|
|
using SHH.CameraSdk;
|
2026-01-07 10:59:03 +08:00
|
|
|
|
using SHH.Contracts;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SHH.CameraService;
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 同步设备配置处理器
|
|
|
|
|
|
/// </summary>
|
2026-01-15 11:04:38 +08:00
|
|
|
|
public class DeviceConfigHandler : ICommandHandler
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-16 15:17:23 +08:00
|
|
|
|
private ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
|
2026-01-16 14:30:42 +08:00
|
|
|
|
|
2026-01-07 10:59:03 +08:00
|
|
|
|
private readonly CameraManager _cameraManager;
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 命令名称
|
|
|
|
|
|
/// </summary>
|
2026-01-21 19:03:59 +08:00
|
|
|
|
public string ActionName => ProtocolCodes.Sync_Camera;
|
2026-01-07 10:59:03 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构造函数
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="cameraManager"></param>
|
2026-01-15 11:04:38 +08:00
|
|
|
|
public DeviceConfigHandler(CameraManager cameraManager)
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
|
|
|
|
|
_cameraManager = cameraManager;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 执行处理
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="payload"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2026-01-07 10:59:03 +08:00
|
|
|
|
public async Task ExecuteAsync(JToken payload)
|
|
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 1. 反序列化配置 DTO
|
2026-01-07 10:59:03 +08:00
|
|
|
|
var dto = payload.ToObject<CameraConfigDto>();
|
|
|
|
|
|
if (dto == null) return;
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 2. 尝试获取现有设备
|
|
|
|
|
|
var device = _cameraManager.GetDevice(dto.Id);
|
|
|
|
|
|
|
2026-01-16 14:30:42 +08:00
|
|
|
|
string op = device != null ? "更新" : "新增";
|
2026-01-17 13:13:17 +08:00
|
|
|
|
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}");
|
2026-01-16 14:30:42 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
if (device != null)
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 场景 A: 设备已存在 -> 执行智能更新 (Smart Update)
|
|
|
|
|
|
// =========================================================
|
2026-01-07 10:59:03 +08:00
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 将全量配置映射为部分更新 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
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 场景 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);
|
|
|
|
|
|
|
2026-01-07 10:59:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// ★★★ 核心修复:统一处理“运行意图” ★★★
|
|
|
|
|
|
if (device != null)
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 将 DTO 的立即执行标志直接同步给设备的运行意图
|
|
|
|
|
|
device.IsRunning = dto.ImmediateExecution;
|
|
|
|
|
|
|
|
|
|
|
|
if (dto.ImmediateExecution)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 情况 1: 收到“启动”指令
|
2026-01-17 00:03:16 +08:00
|
|
|
|
if (!device.IsActived) // 只有没在线时才点火
|
2026-01-12 18:27:58 +08:00
|
|
|
|
{
|
2026-01-17 00:03:16 +08:00
|
|
|
|
// 必须在线再执行
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-01-12 18:27:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 情况 2: 收到“停止”指令 (即 ImmediateExecution = false)
|
2026-01-17 00:03:16 +08:00
|
|
|
|
if (device.IsActived) // 只有在线时才熄火
|
2026-01-12 18:27:58 +08:00
|
|
|
|
{
|
2026-01-16 14:30:42 +08:00
|
|
|
|
_sysLog.Warning($"[Sync] 设备立即停止 {dto.Id}");
|
2026-01-12 18:27:58 +08:00
|
|
|
|
_ = device.StopAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-07 10:59:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 3. 处理自动订阅策略 (Auto Subscriptions)
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 无论新增还是更新,都确保订阅策略是最新的
|
|
|
|
|
|
if (device != null && dto.AutoSubscriptions != null)
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
var controller = device.Controller;
|
|
|
|
|
|
if (controller != null)
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
foreach (var sub in dto.AutoSubscriptions)
|
2026-01-07 10:59:03 +08:00
|
|
|
|
{
|
2026-01-12 18:27:58 +08:00
|
|
|
|
// 如果没有 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);
|
|
|
|
|
|
}
|
2026-01-07 10:59:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|