2026-01-07 10:59:03 +08:00
|
|
|
|
using Microsoft.Extensions.Hosting;
|
|
|
|
|
|
using OpenCvSharp;
|
|
|
|
|
|
using SHH.CameraSdk; // 引用 SDK 核心
|
|
|
|
|
|
using SHH.Contracts;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SHH.CameraService;
|
|
|
|
|
|
|
|
|
|
|
|
public class NetworkStreamingWorker : BackgroundService
|
|
|
|
|
|
{
|
|
|
|
|
|
// 注入所有注册的目标(云端、大屏等),实现动态分发
|
|
|
|
|
|
private readonly IEnumerable<StreamTarget> _targets;
|
|
|
|
|
|
|
|
|
|
|
|
// 编码参数:JPG 质量 75 (平衡画质与带宽)
|
|
|
|
|
|
// 工业经验:75 是甜点,体积只有 100 的 1/3,肉眼几无区别。
|
|
|
|
|
|
// 如果您确实需要 100,请注意带宽压力。此处我保留您要求的 100,但建议未来调优。
|
|
|
|
|
|
private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 };
|
|
|
|
|
|
|
|
|
|
|
|
public NetworkStreamingWorker(IEnumerable<StreamTarget> targets)
|
|
|
|
|
|
{
|
|
|
|
|
|
_targets = targets;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine("[StreamWorker] 启动流媒体采集引擎...");
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 订阅逻辑:接入 "上帝模式" (God Mode)
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 理由:NetMQ 网关需要无差别地获取所有设备的图像。
|
|
|
|
|
|
GlobalStreamDispatcher.OnGlobalFrame += ProcessFrame;
|
|
|
|
|
|
|
|
|
|
|
|
//Console.WriteLine($"[StreamWorker] 已挂载至全局广播总线,正在监听 {GlobalStreamDispatcher.OnGlobalFrame?.GetInvocationList().Length ?? 0} 个订阅者...");
|
|
|
|
|
|
|
|
|
|
|
|
var tcs = new TaskCompletionSource();
|
|
|
|
|
|
stoppingToken.Register(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 停止时反注册,防止静态事件内存泄漏
|
|
|
|
|
|
GlobalStreamDispatcher.OnGlobalFrame -= ProcessFrame;
|
|
|
|
|
|
Console.WriteLine("[StreamWorker] 已断开全局广播连接");
|
|
|
|
|
|
tcs.SetResult();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return tcs.Task;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [回调函数] 处理每一帧图像
|
|
|
|
|
|
/// 注意:此方法运行在 SDK 的采集线程池中,必须极速处理,严禁阻塞!
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void ProcessFrame(long deviceId, SmartFrame frame)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 基础校验 (合法性检查)
|
|
|
|
|
|
if (frame == null || frame.InternalMat.Empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
long startTick = Stopwatch.GetTimestamp();
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 2. 一次编码 (One Encode) - CPU 消耗点
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 理由:在这里同步编码是最安全的,因为出了这个函数 frame 内存就会失效。
|
|
|
|
|
|
// 且只编一次,后续分发给 10 个目标也只用这一份数据。
|
|
|
|
|
|
|
|
|
|
|
|
byte[] jpgBytes = null;
|
|
|
|
|
|
// 如果有更小的图片, 原始图片不压缩, 除非有特殊需求
|
|
|
|
|
|
if (frame.TargetMat == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
jpgBytes = EncodeImage(frame.InternalMat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 双流支持:如果存在处理后的 AI 图,也一并编码
|
|
|
|
|
|
byte[] targetBytes = null;
|
|
|
|
|
|
if (frame.TargetMat != null && !frame.TargetMat.Empty())
|
|
|
|
|
|
{
|
|
|
|
|
|
targetBytes = EncodeImage(frame.TargetMat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 3. 构建 Payload (数据载荷)
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
var payload = new VideoPayload
|
|
|
|
|
|
{
|
|
|
|
|
|
CameraId = deviceId.ToString(),
|
|
|
|
|
|
CaptureTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
|
|
|
|
OriginalImageBytes = jpgBytes, // 引用赋值
|
|
|
|
|
|
TargetImageBytes = targetBytes, // 引用赋值
|
|
|
|
|
|
OriginalWidth = frame.TargetWidth,
|
|
|
|
|
|
OriginalHeight = frame.TargetHeight,
|
|
|
|
|
|
DispatchTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-09 12:30:36 +08:00
|
|
|
|
// 添加订阅者
|
|
|
|
|
|
payload.SubscriberIds.AddRange(frame.SubscriberIds);
|
|
|
|
|
|
|
2026-01-07 10:59:03 +08:00
|
|
|
|
// 计算转码耗时(ms)
|
|
|
|
|
|
double processMs = (Stopwatch.GetTimestamp() - startTick) * 1000.0 / Stopwatch.Frequency;
|
|
|
|
|
|
payload.Diagnostics["encode_ms"] = Math.Round(processMs, 2);
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 4. 动态扇出 (Dynamic Fan-Out) - 内存消耗极低
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 遍历所有目标,往各自独立的管道里写数据。
|
|
|
|
|
|
// 实现了"物理隔离":一个管道满了(云端卡顿),不影响另一个管道(大屏流畅)。
|
|
|
|
|
|
foreach (var target in _targets)
|
|
|
|
|
|
{
|
|
|
|
|
|
// WriteLog 是非阻塞的。满了就丢弃,返回 false。
|
|
|
|
|
|
target.Channel.WriteLog(payload);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 极少发生的内存错误,打印日志但不抛出,避免崩溃 SDK 线程
|
|
|
|
|
|
Console.WriteLine($"[StreamWorker] 采集处理异常: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 辅助:OpenCV 内存编码
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private byte[] EncodeImage(Mat mat)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ImEncode 将 Mat 编码为一维字节数组 (托管内存)
|
|
|
|
|
|
Cv2.ImEncode(".jpg", mat, out byte[] buf, _encodeParams);
|
|
|
|
|
|
return buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|