Files
Ayay/SHH.CameraService/ZeroMqBridgeService.cs

140 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
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 ?? 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("[ZeroMQ Bridge] 正在连接全局视频帧广播总线...");
// 订阅全局帧广播事件:所有设备的帧数据都会触发该事件
// 无需手动绑定设备,动态增删的设备自动适配
GlobalStreamDispatcher.OnGlobalFrame += OnGlobalFrameReceived;
Console.WriteLine("[ZeroMQ Bridge] 全局总线连接成功!已开始监听所有设备帧数据。");
Console.WriteLine("[ZeroMQ Bridge] 说明:动态增删的设备会自动转发,无需重启服务。");
// 返回空任务:服务通过事件驱动,无需阻塞主线程
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)
{
try
{
// 1. 安全校验:跳过空帧或已释放的帧
var sourceMat = frame.TargetMat ?? frame.InternalMat;
if (sourceMat == null || sourceMat.Empty() || sourceMat.IsDisposed)
return;
// 2. 深拷贝图像数据:避免跨线程访问冲突(原帧可能被其他模块异步释放)
using var safeMat = sourceMat.Clone();
// 3. 图像编码:将 OpenCV Mat 转换为 JPG 字节数组质量70平衡画质与性能
var jpgEncodeParams = new int[] { (int)ImwriteFlags.JpegQuality, 70 };
byte[] jpgBytes = safeMat.ImEncode(".jpg", jpgEncodeParams);
// 4. 封装为标准传输协议:使用 SHH.Contracts 中的 VideoPayload 统一格式
var videoPayload = new VideoPayload
{
CameraId = deviceId.ToString(), // 设备ID转为字符串兼容协议标准
CaptureTime = DateTime.Now, // 帧采集时间(当前时间)
DispatchTime = DateTime.Now, // 帧分发时间(当前时间)
OriginalWidth = safeMat.Width, // 图像原始宽度(编码后宽度)
OriginalHeight = safeMat.Height, // 图像原始高度(编码后高度)
OriginalImageBytes = jpgBytes // JPG 编码后的二进制数据
};
// 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($"[ZeroMQ Bridge] 帧转发失败设备ID{deviceId}{ex.Message}");
}
}
#endregion
}
}