Files
Ayay/SHH.CameraService/ZeroMqBridgeService.cs

140 lines
6.5 KiB
C#
Raw Normal View History

2025-12-29 08:09:14 +08:00
using Microsoft.Extensions.Hosting;
using OpenCvSharp;
using SHH.Contracts;
using SHH.NetMQ;
namespace SHH.CameraSdk
{
/// <summary>
/// ZeroMQ 消息桥接服务(后台服务)。
/// 核心职责:订阅系统全局视频帧广播,将帧数据编码为标准协议格式后,通过 ZeroMQ 分发至外部系统。
/// 设计特性:
/// <para>1. 无侵入集成:通过订阅 GlobalStreamDispatcher 事件,无需修改原有帧处理流程。</para>
/// <para>2. 自动适配:支持动态增删设备,无需手动注册设备监听。</para>
/// <para>3. 安全隔离:帧数据深拷贝,避免跨线程内存访问冲突。</para>
/// </summary>
2025-12-29 08:09:14 +08:00
public class ZeroMqBridgeService : BackgroundService
{
#region --- ---
/// <summary>
/// ZeroMQ 分发服务器(用于广播帧数据至多个订阅端)
/// </summary>
2025-12-29 08:09:14 +08:00
private readonly DistributorServer _distributor;
/// <summary>
/// ZeroMQ 转发客户端(用于定向推送帧数据至指定目标)
/// </summary>
2025-12-29 08:09:14 +08:00
private readonly ForwarderClient _forwarder;
#endregion
#region --- ---
/// <summary>
/// 初始化 <see cref="ZeroMqBridgeService"/> 实例。
/// </summary>
/// <param name="distributor">ZeroMQ 分发服务器实例(通过 DI 注入)</param>
/// <param name="forwarder">ZeroMQ 转发客户端实例(通过 DI 注入)</param>
2025-12-29 08:09:14 +08:00
public ZeroMqBridgeService(DistributorServer distributor, ForwarderClient forwarder)
{
_distributor = distributor ?? throw new ArgumentNullException(nameof(distributor));
_forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder));
2025-12-29 08:09:14 +08:00
}
#endregion
#region --- ---
/// <summary>
/// 启动后台服务,订阅全局视频帧广播。
/// </summary>
/// <param name="stoppingToken">服务停止令牌(用于优雅关闭)</param>
2025-12-29 08:09:14 +08:00
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("[ZeroMQ Bridge] 正在连接全局视频帧广播总线...");
2025-12-29 08:09:14 +08:00
// 订阅全局帧广播事件:所有设备的帧数据都会触发该事件
// 无需手动绑定设备,动态增删的设备自动适配
GlobalStreamDispatcher.OnGlobalFrame += OnGlobalFrameReceived;
2025-12-29 08:09:14 +08:00
Console.WriteLine("[ZeroMQ Bridge] 全局总线连接成功!已开始监听所有设备帧数据。");
Console.WriteLine("[ZeroMQ Bridge] 说明:动态增删的设备会自动转发,无需重启服务。");
// 返回空任务:服务通过事件驱动,无需阻塞主线程
2025-12-29 08:09:14 +08:00
return Task.CompletedTask;
}
/// <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)
2025-12-29 08:09:14 +08:00
{
try
{
// 1. 安全校验:跳过空帧或已释放的帧
2025-12-29 08:09:14 +08:00
var sourceMat = frame.TargetMat ?? frame.InternalMat;
if (sourceMat == null || sourceMat.Empty() || sourceMat.IsDisposed)
return;
2025-12-29 08:09:14 +08:00
// 2. 深拷贝图像数据:避免跨线程访问冲突(原帧可能被其他模块异步释放)
2025-12-29 08:09:14 +08:00
using var safeMat = sourceMat.Clone();
// 3. 图像编码:将 OpenCV Mat 转换为 JPG 字节数组质量70平衡画质与性能
var jpgEncodeParams = new int[] { (int)ImwriteFlags.JpegQuality, 70 };
byte[] jpgBytes = safeMat.ImEncode(".jpg", jpgEncodeParams);
2025-12-29 08:09:14 +08:00
// 4. 封装为标准传输协议:使用 SHH.Contracts 中的 VideoPayload 统一格式
var videoPayload = new VideoPayload
2025-12-29 08:09:14 +08:00
{
CameraId = deviceId.ToString(), // 设备ID转为字符串兼容协议标准
CaptureTime = DateTime.Now, // 帧采集时间(当前时间)
DispatchTime = DateTime.Now, // 帧分发时间(当前时间)
OriginalWidth = safeMat.Width, // 图像原始宽度(编码后宽度)
OriginalHeight = safeMat.Height, // 图像原始高度(编码后高度)
OriginalImageBytes = jpgBytes // JPG 编码后的二进制数据
2025-12-29 08:09:14 +08:00
};
// 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");
2025-12-29 08:09:14 +08:00
}
catch (Exception ex)
{
// 异常隔离:单个帧处理失败不影响整体服务运行
Console.WriteLine($"[ZeroMQ Bridge] 帧转发失败设备ID{deviceId}{ex.Message}");
2025-12-29 08:09:14 +08:00
}
}
#endregion
2025-12-29 08:09:14 +08:00
}
}