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 } }