在 AiVideo 中能看到图像
增加了在线状态同步逻辑
This commit is contained in:
@@ -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 会在底层连接成功后自动把消息推出去
|
||||
// 为了保险,对于多个 Endpoint,Dealer 默认是负载均衡发送的(轮询)。
|
||||
// 如果想让每个 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
109
SHH.CameraService/Core/JsonHelper.cs
Normal file
109
SHH.CameraService/Core/JsonHelper.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace SHH.CameraService
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 序列化与反序列化帮助类
|
||||
/// 职责:
|
||||
/// 1. 提供全局统一的 JSON 转换配置(如驼峰命名、日期格式)。
|
||||
/// 2. 封装常见的序列化和反序列化操作。
|
||||
/// 3. 增加对 null 输入和无效 JSON 的健壮性处理。
|
||||
/// </summary>
|
||||
public static class JsonHelper
|
||||
{
|
||||
#region --- 静态配置 ---
|
||||
|
||||
/// <summary>
|
||||
/// 全局共享的 JSON 序列化设置。
|
||||
/// 静态构造函数保证其只被初始化一次。
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerSettings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 静态构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 静态构造函数,用于初始化全局的 JSON 序列化设置。
|
||||
/// </summary>
|
||||
static JsonHelper()
|
||||
{
|
||||
_settings = new JsonSerializerSettings
|
||||
{
|
||||
// 1. 命名策略:将 C# 的 PascalCase 属性名序列化为 JSON 的 camelCase。
|
||||
// 这是与 JavaScript/TypeScript 前端交互的标准做法。
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
|
||||
// 2. 日期格式:统一使用 "yyyy-MM-dd HH:mm:ss" 格式,避免时区和格式差异导致的问题。
|
||||
DateFormatString = "yyyy-MM-dd HH:mm:ss",
|
||||
|
||||
// 3. Null 值处理:在序列化时忽略值为 null 的属性。
|
||||
// 这可以显著减小 JSON 字符串的大小,并使生成的 JSON 更干净。
|
||||
// 例如,`{ Name = "Alice", Age = null }` 会被序列化为 `{"name":"Alice"}`。
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
// 4. 枚举转换:将枚举值序列化为其字符串表示,而不是数字。
|
||||
// 例如,`LogLevel.Info` 会被序列化为 `"info"`,而不是 `1`。
|
||||
_settings.Converters.Add(new StringEnumConverter());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 公共方法 ---
|
||||
|
||||
/// <summary>
|
||||
/// 将对象序列化为 JSON 字符串。
|
||||
/// </summary>
|
||||
/// <param name="obj">要序列化的对象。</param>
|
||||
/// <returns>序列化后的 JSON 字符串。如果输入为 null,则返回空字符串。</returns>
|
||||
public static string Serialize(object obj)
|
||||
{
|
||||
// [健壮性] 如果输入对象为 null,返回空字符串,而不是 "null"。
|
||||
// 这可以防止在创建 HTTP 请求内容时出现意外行为。
|
||||
if (obj == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(obj, _settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JSON 字符串反序列化为指定类型的对象。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标对象的类型(必须是引用类型)。</typeparam>
|
||||
/// <param name="json">要反序列化的 JSON 字符串。</param>
|
||||
/// <returns>成功时返回反序列化后的对象;失败或输入无效时返回 null。</returns>
|
||||
public static T? Deserialize<T>(string json) where T : class
|
||||
{
|
||||
// [健壮性] 检查输入是否为 null、空字符串或仅包含空白字符。
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// [健壮性] 处理 JSON 字符串为 "null" 的特殊情况。
|
||||
if (json.Trim() == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试使用预配置的设置进行反序列化。
|
||||
return JsonConvert.DeserializeObject<T>(json, _settings);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// [健壮性] 如果 JSON 格式无效,捕获异常并返回 null。
|
||||
// 这可以防止程序因一个格式错误的 JSON 字符串而崩溃。
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
151
SHH.CameraService/Core/NetSenders/DeviceStateMonitorWorker.cs
Normal file
151
SHH.CameraService/Core/NetSenders/DeviceStateMonitorWorker.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NetMQ;
|
||||
using NetMQ.Sockets;
|
||||
using MessagePack;
|
||||
using SHH.CameraSdk;
|
||||
using SHH.Contracts;
|
||||
|
||||
namespace SHH.CameraService
|
||||
{
|
||||
/// <summary>
|
||||
/// [二合一] 设备状态聚合与上报服务
|
||||
/// </summary>
|
||||
public class DeviceStateMonitorWorker : BackgroundService
|
||||
{
|
||||
private readonly CameraManager _manager;
|
||||
private readonly ServiceConfig _config;
|
||||
|
||||
// ★ 2. 注入拦截器管道
|
||||
private readonly InterceptorPipeline _pipeline;
|
||||
|
||||
// 本地状态全集缓存
|
||||
private readonly ConcurrentDictionary<string, StatusEventPayload> _stateStore = new();
|
||||
|
||||
// 标记是否有新变更
|
||||
private volatile bool _isDirty = false;
|
||||
private long _lastSendTick = 0;
|
||||
|
||||
// ★ 3. 构造函数增加 InterceptorPipeline 参数
|
||||
public DeviceStateMonitorWorker(
|
||||
CameraManager manager,
|
||||
ServiceConfig config,
|
||||
InterceptorPipeline pipeline) // <--- 注入点
|
||||
{
|
||||
_manager = manager;
|
||||
_config = config;
|
||||
_pipeline = pipeline;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// 1. 初始化缓存 (默认离线)
|
||||
foreach (var dev in _manager.GetAllDevices())
|
||||
{
|
||||
UpdateLocalState(dev.Id, false, "Init");
|
||||
}
|
||||
|
||||
// 2. 挂载 SDK 事件
|
||||
_manager.OnDeviceStatusChanged += OnSdkStatusChanged;
|
||||
|
||||
// 3. 建立连接
|
||||
var cmdEndpoint = _config.CommandEndpoints.FirstOrDefault()?.Uri;
|
||||
if (string.IsNullOrEmpty(cmdEndpoint))
|
||||
{
|
||||
Console.WriteLine("[StatusWorker] 警告: 未配置 Command 端点,状态上报无法启动。");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[StatusWorker] 启动状态上报,直连服务端: {cmdEndpoint}");
|
||||
|
||||
using var socket = new DealerSocket();
|
||||
socket.Options.SendHighWatermark = 1000;
|
||||
// 设置 Identity 是个好习惯,虽然这里只发不收
|
||||
// socket.Options.Identity = ...
|
||||
socket.Connect(cmdEndpoint);
|
||||
|
||||
// 4. 定时循环 (1秒1次)
|
||||
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
// ★ 4. 关键修正:必须使用 await 调用新的异步方法
|
||||
await CheckAndDirectSendAsync(socket);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_manager.OnDeviceStatusChanged -= OnSdkStatusChanged;
|
||||
socket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSdkStatusChanged(long deviceId, bool isOnline, string reason)
|
||||
{
|
||||
UpdateLocalState(deviceId, isOnline, reason);
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
private void UpdateLocalState(long deviceId, bool isOnline, string reason)
|
||||
{
|
||||
var evt = new StatusEventPayload
|
||||
{
|
||||
CameraId = deviceId.ToString(),
|
||||
IsOnline = isOnline,
|
||||
Reason = reason,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
_stateStore[deviceId.ToString()] = evt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并在当前线程直接发送 (已改为异步 Task)
|
||||
/// </summary>
|
||||
// ★ 5. 关键修正:void -> async Task
|
||||
private async Task CheckAndDirectSendAsync(NetMQSocket socket)
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
// 策略: 有变更 或 超过5秒(心跳)
|
||||
bool shouldSend = _isDirty || (now - _lastSendTick > 5000);
|
||||
|
||||
if (shouldSend)
|
||||
{
|
||||
try
|
||||
{
|
||||
// A. 组包 (全量)
|
||||
var snapshot = _stateStore.Values.ToList();
|
||||
var batch = new StatusBatchPayload
|
||||
{
|
||||
Items = snapshot,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
|
||||
// B. 序列化
|
||||
byte[] data = MessagePackSerializer.Serialize(batch);
|
||||
|
||||
// =========================================================
|
||||
// ★ 6. 拦截器调用
|
||||
// =========================================================
|
||||
// 这里的 "STATUS_BATCH" 是协议头,你可以替换为 ProtocolHeaders.StatusBatch (如果定义了的话)
|
||||
var ctx = await _pipeline.ExecuteSendAsync("STATUS_BATCH", data);
|
||||
|
||||
if (ctx != null) // 如果没被拦截
|
||||
{
|
||||
// C. 直接发送
|
||||
socket.SendMoreFrame(ctx.Protocol)
|
||||
.SendFrame(ctx.Data);
|
||||
|
||||
// D. 重置标记
|
||||
_isDirty = false;
|
||||
_lastSendTick = now;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[StatusWorker] 发送失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using NetMQ;
|
||||
using MessagePack;
|
||||
using NetMQ;
|
||||
using SHH.Contracts;
|
||||
|
||||
namespace SHH.CameraService
|
||||
@@ -21,8 +22,16 @@ namespace SHH.CameraService
|
||||
// Frame 0: 协议魔数
|
||||
msg.Append(PROTOCOL_HEADER);
|
||||
|
||||
// Frame 1: 元数据 JSON
|
||||
msg.Append(payload.GetMetadataJson());
|
||||
////// Frame 1: 元数据 JSON
|
||||
////msg.Append(payload.GetMetadataJson());
|
||||
|
||||
// ★★★ 修复点:在序列化之前,手动更新 Payload 的标志位 ★★★
|
||||
payload.HasOriginalImage = (payload.OriginalImageBytes != null && payload.OriginalImageBytes.Length > 0);
|
||||
payload.HasTargetImage = (payload.TargetImageBytes != null && payload.TargetImageBytes.Length > 0);
|
||||
|
||||
// Frame 1: Metadata (MessagePack)
|
||||
byte[] metaBytes = MessagePackSerializer.Serialize(payload);
|
||||
msg.Append(metaBytes);
|
||||
|
||||
// Frame 2: 原始图 (保持帧位对齐,无数据则发空帧)
|
||||
if (payload.HasOriginalImage && payload.OriginalImageBytes != null)
|
||||
@@ -49,9 +58,14 @@ namespace SHH.CameraService
|
||||
// Frame 0 Check
|
||||
if (msg[0].ConvertToString() != PROTOCOL_HEADER) return null;
|
||||
|
||||
// Frame 1: Metadata
|
||||
string json = msg[1].ConvertToString();
|
||||
var payload = VideoPayload.FromMetadataJson(json);
|
||||
//// Frame 1: Metadata
|
||||
//string json = msg[1].ConvertToString();
|
||||
//var payload = VideoPayload.FromMetadataJson(json);
|
||||
|
||||
// [新代码] 直接从二进制还原
|
||||
// ToByteArray() 虽然会产生一次拷贝,但对于 Metadata 这种小数据影响微乎其微
|
||||
// 相比 JSON 解析 String 的开销,这已经非常快了
|
||||
var payload = MessagePackSerializer.Deserialize<VideoPayload>(msg[1].ToByteArray());
|
||||
if (payload == null) return null;
|
||||
|
||||
// Frame 2: Raw Image
|
||||
|
||||
@@ -92,6 +92,9 @@ public class NetworkStreamingWorker : BackgroundService
|
||||
DispatchTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
|
||||
// 添加订阅者
|
||||
payload.SubscriberIds.AddRange(frame.SubscriberIds);
|
||||
|
||||
// 计算转码耗时(ms)
|
||||
double processMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency;
|
||||
payload.Diagnostics["encode_ms"] = Math.Round(processMs, 2);
|
||||
|
||||
Reference in New Issue
Block a user