新通讯图像协议对接成功
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using OpenCvSharp;
|
||||
using SHH.CameraSdk; // 引用 SDK 核心
|
||||
using SHH.Contracts;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
public class ImageMonitorController : BackgroundService
|
||||
{
|
||||
// 注入所有注册的目标(云端、大屏等),实现动态分发
|
||||
private readonly IEnumerable<StreamTarget> _targets;
|
||||
|
||||
// 编码参数:JPG 质量 75 (平衡画质与带宽)
|
||||
// 工业经验:75 是甜点,体积只有 100 的 1/3,肉眼几无区别。
|
||||
// 如果您确实需要 100,请注意带宽压力。此处我保留您要求的 100,但建议未来调优。
|
||||
private readonly int[] _encodeParams = { (int)ImwriteFlags.JpegQuality, 100 };
|
||||
|
||||
public ImageMonitorController(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()
|
||||
};
|
||||
|
||||
// 添加订阅者
|
||||
payload.SubscriberIds.AddRange(frame.SubscriberIds);
|
||||
|
||||
// 计算转码耗时(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)
|
||||
{
|
||||
bool ok = target.Channel.WriteLog(payload);
|
||||
if (!ok)
|
||||
{
|
||||
// 如果这里打印,说明管道由于某种原因被关闭了(通常是程序正在退出)
|
||||
Console.WriteLine($"[DEBUG] 管道写入失败,目标: {target.Config.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SHH.CameraSdk;
|
||||
|
||||
namespace SHH.CameraService;
|
||||
|
||||
/// <summary>
|
||||
/// 图像处理管道配置服务(基于责任链模式)
|
||||
/// <para>核心职责:</para>
|
||||
/// <para>1. 组装图像处理集群的执行顺序,形成 "缩放 → 增强" 的固定流程</para>
|
||||
/// <para>2. 将组装好的管道挂载到全局路由,统一接收驱动层输出的帧数据</para>
|
||||
/// <para>设计说明:</para>
|
||||
/// <para>- 采用责任链模式,支持动态扩展处理节点(如后续新增滤镜、裁剪等功能)</para>
|
||||
/// <para>- 依赖 IHostedService 生命周期,确保在应用启动时完成管道初始化</para>
|
||||
/// <para>- 与 GlobalPipelineRouter 强关联,是帧数据进入处理流程的唯一入口</para>
|
||||
public class PipelineConfigurator : IHostedService
|
||||
{
|
||||
#region --- 依赖注入字段 ---
|
||||
|
||||
/// <summary>
|
||||
/// 图像缩放集群实例(责任链第一节点)
|
||||
/// 功能:根据配置缩放帧分辨率、控制图像放大/缩小开关
|
||||
/// </summary>
|
||||
private readonly ImageScaleCluster _scale;
|
||||
|
||||
/// <summary>
|
||||
/// 图像增强集群实例(责任链第二节点)
|
||||
/// 功能:调整图像亮度、对比度等增强效果(基于 ProcessingConfigManager 配置)
|
||||
/// </summary>
|
||||
private readonly ImageEnhanceCluster _enhance;
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 构造函数 ---
|
||||
|
||||
/// <summary>
|
||||
/// 初始化管道配置服务实例
|
||||
/// </summary>
|
||||
/// <param name="scale">图像缩放集群(通过 DI 注入,已预设并行度和配置管理器)</param>
|
||||
/// <param name="enhance">图像增强集群(通过 DI 注入,已预设并行度和配置管理器)</param>
|
||||
|
||||
public PipelineConfigurator(ImageScaleCluster scale, ImageEnhanceCluster enhance)
|
||||
{
|
||||
_scale = scale;
|
||||
_enhance = enhance;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- IHostedService 实现 ---
|
||||
|
||||
/// <summary>
|
||||
/// 启动服务:组装责任链并挂载到全局路由
|
||||
/// <para>执行时机:应用启动时,在所有 Singleton 服务初始化完成后触发</para>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">服务停止令牌(用于响应应用关闭信号)</param>
|
||||
/// <returns>异步任务(无返回值)</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 建立责任链关系:缩放集群处理完成后,将帧数据传递给增强集群
|
||||
// 设计逻辑:Scale 是入口节点,Enhance 是后续节点,可按需求插入更多处理节点
|
||||
_scale.SetNext(_enhance);
|
||||
|
||||
// 2. 将责任链入口挂载到全局路由:驱动层输出的所有帧数据都会进入该管道
|
||||
// 关键作用:统一帧数据处理入口,屏蔽驱动层与处理层的直接依赖
|
||||
GlobalPipelineRouter.SetProcessor(_scale);
|
||||
|
||||
// 启动日志:打印管道组装结果,便于运维排查
|
||||
Console.WriteLine("[Pipeline] 图像处理链组装完成: ImageScaleCluster -> ImageEnhanceCluster");
|
||||
Console.WriteLine("[Pipeline] 提示:帧数据将按 '缩放 → 增强' 顺序处理,可通过 GlobalPipelineRouter 调整流程");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务:空实现(无资源需要释放)
|
||||
/// <para>说明:图像处理集群的资源释放由各自的 Dispose 方法管理,此处无需额外操作</para>
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">服务停止令牌</param>
|
||||
/// <returns>空异步任务</returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
#endregion
|
||||
}
|
||||
41
SHH.CameraService/GrpcImpls/ImageFactory/VideoDataChannel.cs
Normal file
41
SHH.CameraService/GrpcImpls/ImageFactory/VideoDataChannel.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Threading.Channels;
|
||||
using SHH.Contracts;
|
||||
|
||||
namespace SHH.CameraService
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频数据内部总线 (线程安全的生产者-消费者通道)
|
||||
/// <para>作用:解耦 [采集编码线程] 与 [网络发送线程]</para>
|
||||
/// </summary>
|
||||
public class VideoDataChannel
|
||||
{
|
||||
// 限制容量为 100 帧。如果积压超过 100 帧,说明发送端彻底堵死了,必须丢帧。
|
||||
private readonly Channel<VideoPayload> _channel;
|
||||
|
||||
public VideoDataChannel(int capacity = 10)
|
||||
{
|
||||
var options = new BoundedChannelOptions(capacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.DropOldest, // 核心策略:满了就丢弃最旧的帧
|
||||
SingleReader = false, // 允许多个发送 Worker (如 CloudWorker, ScreenWorker) 同时读取
|
||||
SingleWriter = true // 只有一个采集线程在写
|
||||
};
|
||||
_channel = Channel.CreateBounded<VideoPayload>(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [生产者] 写入一个封装好的数据包 (非阻塞)
|
||||
/// </summary>
|
||||
public bool WriteLog(VideoPayload payload) // 改为返回 bool
|
||||
{
|
||||
// TryWrite 在 DropOldest 模式下虽然几乎总是返回 true,
|
||||
// 但如果 Channel 被 Complete (关闭) 了,它会返回 false。
|
||||
return _channel.Writer.TryWrite(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [消费者] 读取器
|
||||
/// </summary>
|
||||
public ChannelReader<VideoPayload> Reader => _channel.Reader;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user