using Microsoft.Extensions.Hosting;
using OpenCvSharp;
using SHH.Contracts;
using SHH.NetMQ;
namespace SHH.CameraSdk
{
///
/// ZeroMQ 消息桥接服务(后台服务)。
/// 核心职责:订阅系统全局视频帧广播,将帧数据编码为标准协议格式后,通过 ZeroMQ 分发至外部系统。
/// 设计特性:
/// 1. 无侵入集成:通过订阅 GlobalStreamDispatcher 事件,无需修改原有帧处理流程。
/// 2. 自动适配:支持动态增删设备,无需手动注册设备监听。
/// 3. 安全隔离:帧数据深拷贝,避免跨线程内存访问冲突。
///
public class ZeroMqBridgeService : BackgroundService
{
#region --- 依赖注入字段 ---
///
/// ZeroMQ 分发服务器(用于广播帧数据至多个订阅端)
///
private readonly DistributorServer _distributor;
///
/// ZeroMQ 转发客户端(用于定向推送帧数据至指定目标)
///
private readonly ForwarderClient _forwarder;
#endregion
#region --- 构造函数 ---
///
/// 初始化 实例。
///
/// ZeroMQ 分发服务器实例(通过 DI 注入)
/// ZeroMQ 转发客户端实例(通过 DI 注入)
public ZeroMqBridgeService(DistributorServer distributor, ForwarderClient forwarder)
{
_distributor = distributor ?? throw new ArgumentNullException(nameof(distributor));
_forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder));
}
#endregion
#region --- 后台服务核心逻辑 ---
///
/// 启动后台服务,订阅全局视频帧广播。
///
/// 服务停止令牌(用于优雅关闭)
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("[ZeroMQ Bridge] 正在连接全局视频帧广播总线...");
// 订阅全局帧广播事件:所有设备的帧数据都会触发该事件
// 无需手动绑定设备,动态增删的设备自动适配
GlobalStreamDispatcher.OnGlobalFrame += OnGlobalFrameReceived;
Console.WriteLine("[ZeroMQ Bridge] 全局总线连接成功!已开始监听所有设备帧数据。");
Console.WriteLine("[ZeroMQ Bridge] 说明:动态增删的设备会自动转发,无需重启服务。");
// 返回空任务:服务通过事件驱动,无需阻塞主线程
return Task.CompletedTask;
}
///
/// 停止后台服务,取消事件订阅以避免内存泄漏。
///
/// 取消令牌
public override Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("[ZeroMQ Bridge] 正在停止服务,取消全局总线订阅...");
// 取消事件订阅:必须执行,否则会导致内存泄漏
GlobalStreamDispatcher.OnGlobalFrame -= OnGlobalFrameReceived;
Console.WriteLine("[ZeroMQ Bridge] 服务已安全停止。");
return base.StopAsync(cancellationToken);
}
#endregion
#region --- 帧数据处理核心逻辑 ---
///
/// 全局帧数据接收回调(事件处理函数)。
/// 处理流程:安全检查 → 帧数据深拷贝 → JPG 编码 → 封装为标准协议 → ZeroMQ 分发。
///
/// 产生该帧的设备唯一标识
/// 智能帧对象(包含原始/处理后图像数据)
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
}
}