完善契约与客户端、服务端的收发代码

This commit is contained in:
2026-01-03 00:16:28 +08:00
parent d039559402
commit dcf424a86e
30 changed files with 3292 additions and 349 deletions

View File

@@ -5,72 +5,136 @@ using SHH.NetMQ;
namespace SHH.CameraSdk
{
/// <summary>
/// ZeroMQ 消息桥接服务(后台服务)。
/// 核心职责:订阅系统全局视频帧广播,将帧数据编码为标准协议格式后,通过 ZeroMQ 分发至外部系统。
/// 设计特性:
/// <para>1. 无侵入集成:通过订阅 GlobalStreamDispatcher 事件,无需修改原有帧处理流程。</para>
/// <para>2. 自动适配:支持动态增删设备,无需手动注册设备监听。</para>
/// <para>3. 安全隔离:帧数据深拷贝,避免跨线程内存访问冲突。</para>
/// </summary>
public class ZeroMqBridgeService : BackgroundService
{
#region --- ---
/// <summary>
/// ZeroMQ 分发服务器(用于广播帧数据至多个订阅端)
/// </summary>
private readonly DistributorServer _distributor;
/// <summary>
/// ZeroMQ 转发客户端(用于定向推送帧数据至指定目标)
/// </summary>
private readonly ForwarderClient _forwarder;
#endregion
#region --- ---
/// <summary>
/// 初始化 <see cref="ZeroMqBridgeService"/> 实例。
/// </summary>
/// <param name="distributor">ZeroMQ 分发服务器实例(通过 DI 注入)</param>
/// <param name="forwarder">ZeroMQ 转发客户端实例(通过 DI 注入)</param>
public ZeroMqBridgeService(DistributorServer distributor, ForwarderClient forwarder)
{
_distributor = distributor;
_forwarder = forwarder;
_distributor = distributor ?? throw new ArgumentNullException(nameof(distributor));
_forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder));
}
#endregion
#region --- ---
/// <summary>
/// 启动后台服务,订阅全局视频帧广播。
/// </summary>
/// <param name="stoppingToken">服务停止令牌(用于优雅关闭)</param>
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("[Bridge] 正在连接全局广播总线...");
Console.WriteLine("[ZeroMQ Bridge] 正在连接全局视频帧广播总线...");
// 【关键修改】直接订阅静态的全局事件
// 不需要传入 APP_ID因为这是 C# 原生事件,不是字典查找
GlobalStreamDispatcher.OnGlobalFrame += BridgeHandler;
// 订阅全局帧广播事件:所有设备的帧数据都会触发该事件
// 无需手动绑定设备,动态增删的设备自动适配
GlobalStreamDispatcher.OnGlobalFrame += OnGlobalFrameReceived;
Console.WriteLine("[Bridge] 全局总线连接成功!任何动态增删的设备都会自动转发。");
Console.WriteLine("[ZeroMQ Bridge] 全局总线连接成功!已开始监听所有设备帧数据。");
Console.WriteLine("[ZeroMQ Bridge] 说明:动态增删的设备会自动转发,无需重启服务。");
// 返回空任务:服务通过事件驱动,无需阻塞主线程
return Task.CompletedTask;
}
// 真正的事件处理函数
private void BridgeHandler(long deviceId, SmartFrame frame)
/// <summary>
/// 停止后台服务,取消事件订阅以避免内存泄漏。
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
public override Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("[ZeroMQ Bridge] 正在停止服务,取消全局总线订阅...");
// 取消事件订阅:必须执行,否则会导致内存泄漏
GlobalStreamDispatcher.OnGlobalFrame -= OnGlobalFrameReceived;
Console.WriteLine("[ZeroMQ Bridge] 服务已安全停止。");
return base.StopAsync(cancellationToken);
}
#endregion
#region --- ---
/// <summary>
/// 全局帧数据接收回调(事件处理函数)。
/// 处理流程:安全检查 → 帧数据深拷贝 → JPG 编码 → 封装为标准协议 → ZeroMQ 分发。
/// </summary>
/// <param name="deviceId">产生该帧的设备唯一标识</param>
/// <param name="frame">智能帧对象(包含原始/处理后图像数据)</param>
private void OnGlobalFrameReceived(long deviceId, SmartFrame frame)
{
try
{
// 1. 安全检查
// 1. 安全校验:跳过空帧或已释放的帧
var sourceMat = frame.TargetMat ?? frame.InternalMat;
if (sourceMat == null || sourceMat.Empty()) return;
if (sourceMat == null || sourceMat.Empty() || sourceMat.IsDisposed)
return;
// 2. 内存克隆 (Deep Copy) - 这一步不能省
// 2. 深拷贝图像数据:避免跨线程访问冲突(原帧可能被其他模块异步释放)
using var safeMat = sourceMat.Clone();
// 3. 编码 & 封装
// 建议:可以在这里判断一下 deviceId如果某些设备不想发可以在这里 return
var jpgParams = new int[] { (int)ImwriteFlags.JpegQuality, 70 };
byte[] jpgBytes = safeMat.ImEncode(".jpg", jpgParams);
// 3. 图像编码:将 OpenCV Mat 转换为 JPG 字节数组质量70平衡画质与性能
var jpgEncodeParams = new int[] { (int)ImwriteFlags.JpegQuality, 70 };
byte[] jpgBytes = safeMat.ImEncode(".jpg", jpgEncodeParams);
var payload = new VideoPayload
// 4. 封装为标准传输协议:使用 SHH.Contracts 中的 VideoPayload 统一格式
var videoPayload = new VideoPayload
{
CameraId = deviceId.ToString(),
CaptureTime = DateTime.Now,
DispatchTime = DateTime.Now,
OriginalWidth = safeMat.Width,
OriginalHeight = safeMat.Height,
OriginalImageBytes = jpgBytes,
CameraId = deviceId.ToString(), // 设备ID转为字符串兼容协议标准
CaptureTime = DateTime.Now, // 帧采集时间(当前时间)
DispatchTime = DateTime.Now, // 帧分发时间(当前时间)
OriginalWidth = safeMat.Width, // 图像原始宽度(编码后宽度)
OriginalHeight = safeMat.Height, // 图像原始高度(编码后高度)
OriginalImageBytes = jpgBytes // JPG 编码后的二进制数据
};
payload.SubscriberIds.AddRange(frame.SubscriberIds);
// 4. 发射
_distributor.Broadcast(payload);
_forwarder.Push(payload);
// 5. 传递订阅者ID保持与原帧的订阅者关联
if (frame.SubscriberIds.Any())
videoPayload.SubscriberIds.AddRange(frame.SubscriberIds);
// 6. ZeroMQ 分发:同时执行广播和定向推送(根据业务需求选择,可按需注释)
_distributor.Broadcast(videoPayload); // 广播给所有订阅端
_forwarder.Push(videoPayload); // 定向推送给指定目标
// 调试日志(生产环境建议注释,避免性能损耗)
// Console.WriteLine($"[ZeroMQ Bridge] 转发设备 {deviceId} 帧数据,大小:{jpgBytes.Length / 1024}KB");
}
catch (Exception ex)
{
// Console.WriteLine(ex.Message); // 生产环境建议注释掉,防止日志刷屏
// 异常隔离:单个帧处理失败不影响整体服务运行
Console.WriteLine($"[ZeroMQ Bridge] 帧转发失败设备ID{deviceId}{ex.Message}");
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
// 优雅退订,防止内存泄漏
GlobalStreamDispatcher.OnGlobalFrame -= BridgeHandler;
return base.StopAsync(cancellationToken);
}
#endregion
}
}