87 lines
3.2 KiB
C#
87 lines
3.2 KiB
C#
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|