在 AiVideo 中能看到图像

增加了在线状态同步逻辑
This commit is contained in:
2026-01-09 12:30:36 +08:00
parent 3d47c8f009
commit 3351ae739e
31 changed files with 1090 additions and 477 deletions

View File

@@ -1,8 +1,9 @@
using Microsoft.Extensions.Hosting;
using MessagePack;
using Microsoft.Extensions.Hosting;
using NetMQ;
using NetMQ.Sockets;
using Newtonsoft.Json;
using SHH.CameraSdk;
using SHH.Contracts;
using System.Text;
namespace SHH.CameraService;
@@ -10,130 +11,144 @@ namespace SHH.CameraService;
public class CommandClientWorker : BackgroundService
{
private readonly ServiceConfig _config;
private readonly CommandDispatcher _dispatcher; // 注入分发器
private readonly CommandDispatcher _dispatcher;
public CommandClientWorker(ServiceConfig config, CommandDispatcher dispatcher)
// ★ 1. 注入拦截器管道管理器
private readonly InterceptorPipeline _pipeline;
public CommandClientWorker(
ServiceConfig config,
CommandDispatcher dispatcher,
InterceptorPipeline pipeline) // <--- 注入
{
_config = config;
_dispatcher = dispatcher;
_pipeline = pipeline;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// =================================================================
// ★★★ 核心修复:强制让出主线程 ★★★
// 这行代码会让当前的 ExecuteAsync 立即返回一个未完成的 Task 给 Host
// Host 就会认为 "这个服务启动好了",然后继续去启动 WebAPI。
// 而剩下的代码会被调度到线程池里异步执行,互不干扰。
// =================================================================
await Task.Yield();
// 1. 如果不是主动/混合模式,不需要连接
if (!_config.ShouldConnect) return;
if (_config.CommandEndpoints.Count == 0) return;
var cmdEndpoints = _config.CommandEndpoints;
if (cmdEndpoints.Count == 0)
{
Console.WriteLine("[指令] 未配置指令通道,跳过注册。");
return;
}
// 2. 初始化 Dealer Socket
using var dealer = new DealerSocket();
// ★★★ 关键:设置身份标识 (Identity) ★★★
// 服务端 (Router) 收到消息时,第一帧就是这个 ID
// 如果不设ZMQ 会随机生成一个二进制 ID服务端就不知道你是谁了
string myIdentity = _config.AppId;
dealer.Options.Identity = Encoding.UTF8.GetBytes(myIdentity);
// 3. 连接所有目标 (遍历 ServiceEndpoint 对象)
foreach (var ep in cmdEndpoints)
foreach (var ep in _config.CommandEndpoints)
{
Console.WriteLine($"[指令] 连接控制端: {ep.Uri} [{ep.Description}]");
try
{
dealer.Connect(ep.Uri);
}
catch (Exception ex)
{
Console.WriteLine($"[指令] 连接失败 {ep.Uri}: {ex.Message}");
}
try { dealer.Connect(ep.Uri); }
catch (Exception ex) { Console.WriteLine($"[指令] 连接失败 {ep.Uri}: {ex.Message}"); }
}
// 1. 获取本机 IP (简单的获取方式,用于上报给 Dashboard)
string localIp = "127.0.0.1";
try
// ... (获取 IP 代码省略,保持不变) ...
// =================================================================
// 构建注册包
// =================================================================
var registerPayload = new RegisterPayload
{
// 简单获取首个非回环 IP生产环境建议用更严谨的帮助类
var host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
localIp = host.AddressList.FirstOrDefault(ip =>
ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)?.ToString() ?? "127.0.0.1";
}
catch { }
// 4. 构建注册/登录包
var registerPayload = new
{
Action = "Register",
Payload = new
{
// 1. AppId (身份)
Id = _config.AppId,
// 2. Version (程序集版本)
Version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "1.0.0",
// 3. 进程 ID (用于远程监控)
Pid = Environment.ProcessId,
// 4. 关键端口信息
// 告诉 Dashboard如果你想调我的 REST API请访问这个端口
WebPort = _config.BasePort,
// 如果您有本地绑定的 ZMQ 端口也可以在这里上报
// VideoPort = _config.BasePort + 1,
// 基础网络信息
Ip = localIp,
// 附带信息:我是要把视频推给谁 (供 Dashboard 调试用)
TargetVideoNodes = _config.VideoEndpoints.Select(e => e.Uri).ToList()
},
Time = DateTime.Now
Protocol = ProtocolHeaders.ServerRegister,
InstanceId = _config.AppId,
ProcessId = Environment.ProcessId,
Version = "1.0.0",
ServerIp = localIp,
WebApiPort = _config.BasePort,
StartTime = DateTime.Now
};
string json = JsonConvert.SerializeObject(registerPayload);
try
{
byte[] regData = MessagePackSerializer.Serialize(registerPayload);
// 5. 发送注册包
// Dealer 连接建立是异步的所以这里直接发ZMQ 会在底层连接成功后自动把消息推出去
// 为了保险,对于多个 EndpointDealer 默认是负载均衡发送的(轮询)。
// 如果想让每个 Endpoint 都收到注册包,这在 Dealer 模式下稍微有点特殊。
// 但通常我们只需要发一次,只要有一个 Dashboard 收到并建立会话即可。
// 或者简单粗暴:循环发送几次,确保覆盖。
// =============================================================
// ★ 2. 拦截点 A: 发送注册包 (Outbound)
// =============================================================
var ctx = await _pipeline.ExecuteSendAsync(ProtocolHeaders.ServerRegister, regData);
Console.WriteLine($"[指令] 发送注册包: {json}");
dealer.SendFrame(json);
if (ctx != null) // 如果未被拦截
{
// 注意:这里使用 ctx.Protocol 和 ctx.Data允许拦截器修改内容
dealer.SendMoreFrame(ctx.Protocol)
.SendFrame(ctx.Data);
// 6. 进入监听循环 (等待 ACK 或 指令)
// 进入监听循环
while (!stoppingToken.IsCancellationRequested)
Console.WriteLine($"[指令] 注册包已发送 ({ctx.Data.Length} bytes)");
}
}
catch (Exception ex)
{
Console.WriteLine($"[致命错误] 注册流程异常: {ex.Message}");
return;
}
// =================================================================
// 定义 ACK 发送逻辑 (包含拦截器)
// =================================================================
// 注意:这里需要 async因为拦截器是异步的
Action<CommandResult> sendAckHandler = async (result) =>
{
try
{
if (dealer.TryReceiveFrameString(TimeSpan.FromMilliseconds(500), out string msg))
{
Console.WriteLine($"[指令] 收到消息: {msg}");
byte[] resultBytes = MessagePackSerializer.Serialize(result);
// ★★★ 核心变化:直接扔给分发器 ★★★
// 无论未来加多少指令,这里都不用改代码
await _dispatcher.DispatchAsync(msg);
// =========================================================
// ★ 3. 拦截点 B: 发送 ACK 回执 (Outbound)
// =========================================================
// 协议头是 COMMAND_RESULT
var ctx = await _pipeline.ExecuteSendAsync(ProtocolHeaders.CommandResult, resultBytes);
if (ctx != null)
{
dealer.SendMoreFrame(ctx.Protocol)
.SendFrame(ctx.Data);
Console.WriteLine($"[指令] 已回复 ACK -> Req: {result.RequestId}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[指令] 异常: {ex.Message}");
Console.WriteLine($"[ACK Error] 回执发送失败: {ex.Message}");
}
};
// 订阅事件 (需要适配 async void注意异常捕获)
_dispatcher.OnResponseReady += async (res) => await Task.Run(() => sendAckHandler(res));
// =================================================================
// 接收循环
// =================================================================
try
{
while (!stoppingToken.IsCancellationRequested)
{
NetMQMessage incomingMsg = new NetMQMessage();
if (dealer.TryReceiveMultipartMessage(TimeSpan.FromMilliseconds(500), ref incomingMsg))
{
if (incomingMsg.FrameCount >= 2)
{
string rawProtocol = incomingMsg[0].ConvertToString();
byte[] rawData = incomingMsg[1].ToByteArray();
// =================================================
// ★ 4. 拦截点 C: 接收指令 (Inbound)
// =================================================
var ctx = await _pipeline.ExecuteReceiveAsync(rawProtocol, rawData);
if (ctx != null) // 如果未被拦截
{
// 将处理后的数据交给 Dispatcher
await _dispatcher.DispatchAsync(ctx.Protocol, ctx.Data);
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[指令] 接收循环异常: {ex.Message}");
}
}
}

View File

@@ -1,46 +1,91 @@
using Newtonsoft.Json.Linq;
// 文件: Core\CmdClients\CommandDispatcher.cs
using MessagePack;
using Newtonsoft.Json.Linq;
using SHH.Contracts;
using System.Text;
namespace SHH.CameraService;
public class CommandDispatcher
{
// 路由表Key = ActionName, Value = Handler
// 1. 注入路由表
private readonly Dictionary<string, ICommandHandler> _handlers;
// 通过依赖注入拿到所有实现了 ICommandHandler 的类
// 2. 定义回执事件 (ACK闭环的核心)
public event Action<CommandResult>? OnResponseReady;
// 3. 构造函数:注入所有 Handler
public CommandDispatcher(IEnumerable<ICommandHandler> handlers)
{
_handlers = handlers.ToDictionary(h => h.ActionName, h => h);
// 将注入的 Handler 转换为字典Key = ActionName (e.g. "SyncCamera")
_handlers = handlers.ToDictionary(h => h.ActionName, h => h, StringComparer.OrdinalIgnoreCase);
}
public async Task DispatchAsync(string jsonMessage)
public async Task DispatchAsync(string protocol, byte[] data)
{
try
{
var jObj = JObject.Parse(jsonMessage);
string action = jObj["Action"]?.ToString();
var payload = jObj["Payload"];
// 只处理 COMMAND 协议
if (protocol != ProtocolHeaders.Command) return;
if (string.IsNullOrEmpty(action)) return;
// 反序列化信封
var envelope = MessagePackSerializer.Deserialize<CommandPayload>(data);
if (envelope == null) return;
// 1. 查找是否有对应的处理器
if (_handlers.TryGetValue(action, out var handler))
string cmdCode = envelope.CmdCode; // e.g. "SyncCamera"
Console.WriteLine($"[分发] 收到指令: {cmdCode} (ID: {envelope.RequestId})");
bool isSuccess = true;
string message = "OK";
// --- 路由匹配逻辑 ---
if (_handlers.TryGetValue(cmdCode, out var handler))
{
await handler.ExecuteAsync(payload);
}
else if (action == "ACK")
{
// ACK 是特殊的,可以直接在这里处理或者忽略
Console.WriteLine($"[指令] 握手成功: {jObj["Message"]}");
try
{
// 数据适配:你的 Handler 需要 JToken
// 如果 envelope.JsonParams 是空的,传个空对象防止报错
var jsonStr = string.IsNullOrEmpty(envelope.JsonParams) ? "{}" : envelope.JsonParams;
var token = JToken.Parse(jsonStr);
// ★★★ 核心:调用 SyncCameraHandler.ExecuteAsync ★★★
await handler.ExecuteAsync(token);
message = $"Executed {cmdCode}";
}
catch (Exception ex)
{
isSuccess = false;
message = $"Handler Error: {ex.Message}";
Console.WriteLine($"[业务异常] {message}");
}
}
else
{
Console.WriteLine($"[警告] 未知的指令: {action}");
isSuccess = false;
message = $"No handler found for {cmdCode}";
Console.WriteLine($"[警告] {message}");
}
// --- ACK 闭环逻辑 ---
if (envelope.RequireAck)
{
var result = new CommandResult
{
Protocol = ProtocolHeaders.CommandResult,
RequestId = envelope.RequestId, // 必须带回 ID
Success = isSuccess,
Message = message,
Timestamp = DateTime.Now.Ticks
};
// 触发事件
OnResponseReady?.Invoke(result);
}
}
catch (Exception ex)
{
Console.WriteLine($"[分发错误] {ex.Message}");
Console.WriteLine($"[Dispatcher] 致命错误: {ex.Message}");
}
}
}

View File

@@ -8,7 +8,7 @@ public class SyncCameraHandler : ICommandHandler
{
private readonly CameraManager _cameraManager;
public string ActionName => "SyncCamera";
public string ActionName => ProtocolHeaders.SyncCamera;
public SyncCameraHandler(CameraManager cameraManager)
{