using Microsoft.Extensions.Hosting; using NetMQ; using NetMQ.Sockets; using SHH.CameraSdk; namespace SHH.CameraService; public class ZeroMQBridgeWorker : BackgroundService { private readonly ServiceConfig _config; private readonly VideoDataChannel _channel; // 数据源 public ZeroMQBridgeWorker(ServiceConfig config, VideoDataChannel channel) { _config = config; _channel = channel; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 1. 如果不是主动/混合模式,不需要连接 if (!_config.ShouldConnect) return; // ★★★ 核心修正:直接读取解析好的视频地址列表 ★★★ // 这些地址来自参数 --uris "IP,VideoPort&CommandPort" 中的 VideoPort 部分 (符号左边) var streamUris = _config.VideoEndpoints; if (streamUris.Count == 0) { Console.WriteLine("[推流] 未在参数中找到视频通道地址(位于&符号左侧),跳过连接。"); return; } // 2. 初始化 Publisher Socket // 特点:只需 Send 一次,底层会自动分发给所有 Connect 的 Dashboard using var pubSocket = new PublisherSocket(); // 设置发送高水位 (HWM) // 防止网络拥塞或接收端处理慢时,内存无限增长。超过50帧积压就开始丢弃旧帧。 pubSocket.Options.SendHighWatermark = 50; // 3. 连接所有视频目标 foreach (var uri in streamUris) { Console.WriteLine($"[推流] 连接视频接收端: {uri}"); pubSocket.Connect(uri); } Console.WriteLine($"[推流] 服务就绪 (AppId: {_config.AppId}),等待视频帧..."); // 4. 推流循环 while (!stoppingToken.IsCancellationRequested) { try { // 从通道读取最新帧 (支持异步等待) // 注意:这里使用了之前 VideoDataChannel 暴露出来的 Reader 属性 var payload = await _channel.Reader.ReadAsync(stoppingToken); // 简单校验 if (payload == null || payload.OriginalImageBytes == null || payload.OriginalImageBytes.Length == 0) continue; // 构造 Topic (通常用 AppId 作为 Topic,这样 Dashboard 可以按需订阅) string topic = _config.AppId; // 发送两帧:[Topic] [ImageBytes] // 这样 Dashboard 的 Subscriber 可以通过 Subscribe(topic) 来过滤 pubSocket.SendMoreFrame(topic) .SendFrame(payload.OriginalImageBytes); // 调试日志 (生产环境建议注释掉,否则刷屏) // Console.WriteLine($"[推流] Sent {payload.OriginalImageBytes.Length} bytes"); } catch (OperationCanceledException) { break; // 正常退出 } catch (Exception ex) { Console.WriteLine($"[推流] 发送异常: {ex.Message}"); // 发生错误稍微停顿,防止死循环占用 CPU await Task.Delay(1000, stoppingToken); } } } }