1041 lines
42 KiB
Plaintext
1041 lines
42 KiB
Plaintext
|
|
|
|||
|
|
|
|||
|
|
==============================================================================
|
|||
|
|
FILE PATH: CameraEngineWorker.cs
|
|||
|
|
==============================================================================
|
|||
|
|
|
|||
|
|
using Microsoft.Extensions.Hosting;
|
|||
|
|
using SHH.CameraSdk;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraService;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 相机服务核心引擎工作者(后台长驻服务)
|
|||
|
|
/// <para>核心职责:</para>
|
|||
|
|
/// <para>1. 管理 CameraManager 全生命周期(启动、配置、释放)</para>
|
|||
|
|
/// <para>2. 初始化网络哨兵(ConnectivitySentinel),监控设备网络连通性</para>
|
|||
|
|
/// <para>3. 无配置时自动添加默认测试设备,降低调试门槛</para>
|
|||
|
|
/// <para>设计说明:</para>
|
|||
|
|
/// <para>- 基于 BackgroundService,运行在独立后台线程,不阻塞 Web 主线程</para>
|
|||
|
|
/// <para>- 与 CameraManager 强绑定,是所有相机设备的统一入口</para>
|
|||
|
|
/// <para>- 包含容错机制,添加设备失败不影响整体服务启动</para>
|
|||
|
|
public class CameraEngineWorker : BackgroundService
|
|||
|
|
{
|
|||
|
|
#region --- 依赖注入字段 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 相机管理器实例(核心业务对象)
|
|||
|
|
/// 功能:管理所有相机设备的生命周期、状态监控、配置更新
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly CameraManager _manager;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 网络连通性哨兵实例
|
|||
|
|
/// 功能:周期性 Ping 设备、检测网络状态、触发断线重连
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly ConnectivitySentinel _sentinel;
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 构造函数 ---
|
|||
|
|
/// <summary>
|
|||
|
|
/// 初始化相机引擎工作者实例
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="manager">相机管理器(通过 DI 注入,已关联存储服务)</param>
|
|||
|
|
/// <param name="sentinel">网络哨兵(通过 DI 注入,已预设监控周期)</param>
|
|||
|
|
public CameraEngineWorker(CameraManager manager, ConnectivitySentinel sentinel)
|
|||
|
|
{
|
|||
|
|
_manager = manager ?? throw new ArgumentNullException(
|
|||
|
|
nameof(manager), "相机管理器实例不能为空,核心引擎启动失败");
|
|||
|
|
_sentinel = sentinel ?? throw new ArgumentNullException(
|
|||
|
|
nameof(sentinel), "网络哨兵实例不能为空,设备监控功能失效");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- BackgroundService 核心实现 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 启动引擎:初始化相机管理器并加载业务配置
|
|||
|
|
/// <para>执行流程:</para>
|
|||
|
|
/// <para>1. 启动 CameraManager(加载本地配置文件中的设备信息)</para>
|
|||
|
|
/// <para>2. 加载默认业务逻辑(无设备时添加测试设备)</para>
|
|||
|
|
/// <para>注意:网络哨兵的启动逻辑已内置在其构造函数中,此处无需额外调用</para>
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="stoppingToken">服务停止令牌(响应应用关闭/重启信号)</param>
|
|||
|
|
/// <returns>异步任务(引擎启动完成后结束)</returns>
|
|||
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine("[Engine] 核心引擎启动中...");
|
|||
|
|
Console.WriteLine("[Engine] 启动相机管理器(加载设备配置)");
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 启动相机管理器:加载 App_Data/Process_X 目录下的设备配置文件
|
|||
|
|
await _manager.StartAsync();
|
|||
|
|
Console.WriteLine("[Engine] 相机管理器启动成功,已加载配置文件中的设备");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[Engine] 相机管理器启动失败:{ex.Message}");
|
|||
|
|
Console.WriteLine("[Engine] 警告:核心引擎将继续运行,但无法管理任何相机设备");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Console.WriteLine("[Engine] 核心引擎启动完成,进入运行状态");
|
|||
|
|
Console.WriteLine("[Engine] 提示:可通过 API 接口添加/编辑/删除设备,实时生效");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 停止引擎:优雅释放资源
|
|||
|
|
/// <para>执行流程:</para>
|
|||
|
|
/// <para>1. 调用 CameraManager.DisposeAsync(),释放所有设备连接、句柄、线程资源</para>
|
|||
|
|
/// <para>2. 调用基类 StopAsync(),标记服务停止状态</para>
|
|||
|
|
/// <para>注意:必须先释放 CameraManager,避免设备连接泄露</para>
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="cancellationToken">取消令牌(用于强制终止释放流程)</param>
|
|||
|
|
/// <returns>异步任务(资源释放完成后结束)</returns>
|
|||
|
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine("[Engine] 核心引擎正在停止...");
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 释放相机管理器:停止所有设备取流、注销登录、释放非托管资源
|
|||
|
|
await _manager.DisposeAsync();
|
|||
|
|
Console.WriteLine("[Engine] 相机管理器资源已释放");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[Engine] 资源释放异常:{ex.Message}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用基类方法,完成服务停止
|
|||
|
|
await base.StopAsync(cancellationToken);
|
|||
|
|
Console.WriteLine("[Engine] 核心引擎已停止");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
==============================================================================
|
|||
|
|
FILE PATH: CommandBusProcessor.cs
|
|||
|
|
==============================================================================
|
|||
|
|
|
|||
|
|
using Newtonsoft.Json;
|
|||
|
|
using SHH.CameraSdk;
|
|||
|
|
using SHH.Contracts;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraService
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 指令业务逻辑分发器 (纯逻辑层)
|
|||
|
|
/// <para>职责:解析业务参数 -> 调用 CameraManager -> 返回执行结果</para>
|
|||
|
|
/// <para>注意:本类不处理网络协议,也不负责 RequestId 的回填,只关注业务本身</para>
|
|||
|
|
/// </summary>
|
|||
|
|
public static class CommandBusProcessor
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 核心业务入口
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="manager">相机管理器实例</param>
|
|||
|
|
/// <param name="payload">已解析的指令包</param>
|
|||
|
|
/// <returns>执行结果 (不含 RequestId,由调用方补充)</returns>
|
|||
|
|
public static CommandResult ProcessBusinessLogic(CameraManager manager, CommandPayload payload)
|
|||
|
|
{
|
|||
|
|
string cmd = payload.CmdCode.ToUpper();
|
|||
|
|
|
|||
|
|
// 忽略客户端发回的 ACK (如果是双向确认模式)
|
|||
|
|
if (cmd == "REGISTER_ACK") return CommandResult.Ok();
|
|||
|
|
|
|||
|
|
// 解析 TargetId (CameraId)
|
|||
|
|
long deviceId = 0;
|
|||
|
|
// 只有非 SYSTEM 指令才需要解析设备ID
|
|||
|
|
if (payload.TargetId != "SYSTEM" && !long.TryParse(payload.TargetId, out deviceId))
|
|||
|
|
{
|
|||
|
|
return CommandResult.Fail($"Invalid Device ID: {payload.TargetId}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
switch (cmd)
|
|||
|
|
{
|
|||
|
|
// ==========================================
|
|||
|
|
// 1. PTZ 云台控制
|
|||
|
|
// ==========================================
|
|||
|
|
case "PTZ":
|
|||
|
|
{
|
|||
|
|
var device = manager.GetDevice(deviceId);
|
|||
|
|
if (device == null) return CommandResult.Fail("Device Not Found");
|
|||
|
|
if (!device.IsOnline) return CommandResult.Fail("Device Offline");
|
|||
|
|
|
|||
|
|
// 检查设备是否支持 PTZ 能力 (接口模式匹配)
|
|||
|
|
if (device is IPtzFeature ptzFeature)
|
|||
|
|
{
|
|||
|
|
var ptzDto = JsonConvert.DeserializeObject<PtzControlDto>(payload.JsonParams);
|
|||
|
|
if (ptzDto == null) return CommandResult.Fail("Invalid PTZ Params");
|
|||
|
|
|
|||
|
|
// 异步转同步执行 (Task.Wait 在后台线程是安全的)
|
|||
|
|
if (ptzDto.Duration > 0)
|
|||
|
|
{
|
|||
|
|
// 点动模式 (例如:向左转 500ms)
|
|||
|
|
ptzFeature.PtzStepAsync(ptzDto.Action, ptzDto.Duration, ptzDto.Speed).Wait();
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// 持续模式 (开始转/停止转)
|
|||
|
|
ptzFeature.PtzControlAsync(ptzDto.Action, ptzDto.Stop, ptzDto.Speed).Wait();
|
|||
|
|
}
|
|||
|
|
return CommandResult.Ok("PTZ Executed");
|
|||
|
|
}
|
|||
|
|
return CommandResult.Fail("Device does not support PTZ");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==========================================
|
|||
|
|
// 2. 远程重启
|
|||
|
|
// ==========================================
|
|||
|
|
case "REBOOT":
|
|||
|
|
{
|
|||
|
|
var device = manager.GetDevice(deviceId);
|
|||
|
|
if (device == null) return CommandResult.Fail("Device Not Found");
|
|||
|
|
|
|||
|
|
if (device is IRebootFeature rebootFeature)
|
|||
|
|
{
|
|||
|
|
rebootFeature.RebootAsync().Wait();
|
|||
|
|
return CommandResult.Ok("Reboot command sent");
|
|||
|
|
}
|
|||
|
|
return CommandResult.Fail("Device does not support Reboot");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==========================================
|
|||
|
|
// 3. 时间同步
|
|||
|
|
// ==========================================
|
|||
|
|
case "SYNC_TIME":
|
|||
|
|
{
|
|||
|
|
var device = manager.GetDevice(deviceId);
|
|||
|
|
if (device == null) return CommandResult.Fail("Device Not Found");
|
|||
|
|
|
|||
|
|
if (device is ITimeSyncFeature timeFeature)
|
|||
|
|
{
|
|||
|
|
timeFeature.SetTimeAsync(DateTime.Now).Wait();
|
|||
|
|
return CommandResult.Ok("Time synced");
|
|||
|
|
}
|
|||
|
|
return CommandResult.Fail("Device does not support TimeSync");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==========================================
|
|||
|
|
// 4. 系统级指令 (心跳/诊断)
|
|||
|
|
// ==========================================
|
|||
|
|
case "PING":
|
|||
|
|
return CommandResult.Ok("PONG");
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
return CommandResult.Fail($"Unknown Command: {cmd}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (AggregateException ae)
|
|||
|
|
{
|
|||
|
|
// 捕获异步任务内部的异常
|
|||
|
|
return CommandResult.Fail($"Execution Error: {ae.InnerException?.Message}");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
return CommandResult.Fail($"Execution Error: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
==============================================================================
|
|||
|
|
FILE PATH: CommandBusService.cs
|
|||
|
|
==============================================================================
|
|||
|
|
|
|||
|
|
using Microsoft.Extensions.Caching.Memory;
|
|||
|
|
using Microsoft.Extensions.Hosting;
|
|||
|
|
using Microsoft.Extensions.Configuration;
|
|||
|
|
using NetMQ;
|
|||
|
|
using NetMQ.Sockets;
|
|||
|
|
using Newtonsoft.Json;
|
|||
|
|
using SHH.CameraSdk;
|
|||
|
|
using SHH.Contracts;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraService
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 双模指令总线服务 (Enterprise V2)
|
|||
|
|
/// <para>核心职责:建立 TCP 指令通道,接收客户端指令并分发给 CameraManager</para>
|
|||
|
|
/// <para>增强特性:</para>
|
|||
|
|
/// <para>1. 支持双模:被动监听 (Bind) 与 主动投递 (Connect)</para>
|
|||
|
|
/// <para>2. 幂等性控制:利用 MemoryCache 防止客户端重试导致的重复执行</para>
|
|||
|
|
/// <para>3. 顺序一致性:利用时间戳防止指令乱序</para>
|
|||
|
|
/// </summary>
|
|||
|
|
public class CommandBusService : BackgroundService
|
|||
|
|
{
|
|||
|
|
#region --- 1. 字段与依赖 ---
|
|||
|
|
|
|||
|
|
private readonly CameraManager _cameraManager;
|
|||
|
|
private readonly IConfiguration _config;
|
|||
|
|
private readonly IMemoryCache _cache; // 核心:用于请求去重
|
|||
|
|
private readonly int _processId;
|
|||
|
|
|
|||
|
|
// 运行状态标志
|
|||
|
|
private volatile bool _isRunning = false;
|
|||
|
|
|
|||
|
|
// 两种模式的 Socket (互斥存在)
|
|||
|
|
private ResponseSocket? _repSocket; // 模式A: 被动监听 (Server-Listening)
|
|||
|
|
private DealerSocket? _dealerSocket; // 模式B: 主动投递 (Server-Dialing)
|
|||
|
|
|
|||
|
|
// 顺序一致性锁:记录每个设备最后处理的指令时间戳
|
|||
|
|
// Key: TargetId (设备ID), Value: Timestamp (最后执行时间)
|
|||
|
|
private readonly Dictionary<string, DateTime> _deviceLastCmdTime = new();
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 2. 构造函数 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 构造函数 (注意:必须在 Program.cs 注册 AddMemoryCache)
|
|||
|
|
/// </summary>
|
|||
|
|
public CommandBusService(CameraManager manager, IConfiguration config, IMemoryCache cache)
|
|||
|
|
{
|
|||
|
|
_cameraManager = manager;
|
|||
|
|
_config = config;
|
|||
|
|
_cache = cache;
|
|||
|
|
// 获取当前进程 ID (默认为 1)
|
|||
|
|
_processId = _config.GetValue<int>("ProcessId", 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 3. 核心生命周期 ---
|
|||
|
|
|
|||
|
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
|||
|
|
{
|
|||
|
|
// 在后台线程启动,避免阻塞 Web 主线程
|
|||
|
|
return Task.Run(() =>
|
|||
|
|
{
|
|||
|
|
_isRunning = true;
|
|||
|
|
|
|||
|
|
// 1. 读取网络策略
|
|||
|
|
// 优先读取配置中的主动目标,如果没有则回退到被动监听
|
|||
|
|
string? activeTargetIp = _config["Network:ActiveTargets:0:Ip"];
|
|||
|
|
bool isActiveMode = !string.IsNullOrEmpty(activeTargetIp);
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (isActiveMode)
|
|||
|
|
{
|
|||
|
|
// === 模式 B: 主动投递 (Server Connects Client) ===
|
|||
|
|
// 场景:服务器在内网,主动连接公网/固定IP的客户端
|
|||
|
|
int cmdPort = _config.GetValue<int>("Network:ActiveTargets:0:CmdPort", 7000);
|
|||
|
|
string addr = $"tcp://{activeTargetIp}:{cmdPort}";
|
|||
|
|
RunActiveMode(addr, stoppingToken);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// === 模式 A: 被动监听 (Server Binds Port) ===
|
|||
|
|
// 场景:服务器有固定IP,等待客户端连接
|
|||
|
|
int basePort = _config.GetValue<int>("Network:Passive:CmdPortBase", 7000);
|
|||
|
|
int listenPort = basePort + (_processId - 1);
|
|||
|
|
string addr = $"tcp://*:{listenPort}";
|
|||
|
|
RunPassiveMode(addr, stoppingToken);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[CmdBus] 致命错误停止: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
_isRunning = false;
|
|||
|
|
CleanupSockets();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}, stoppingToken);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void CleanupSockets()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
_repSocket?.Dispose();
|
|||
|
|
_dealerSocket?.Dispose();
|
|||
|
|
}
|
|||
|
|
catch { /* 忽略销毁时的异常 */ }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 4. 模式实现:被动监听 (Passive) ---
|
|||
|
|
|
|||
|
|
private void RunPassiveMode(string address, CancellationToken token)
|
|||
|
|
{
|
|||
|
|
using (_repSocket = new ResponseSocket())
|
|||
|
|
{
|
|||
|
|
_repSocket.Bind(address);
|
|||
|
|
Console.WriteLine($"[CmdBus] [被动模式] 指令监听已启动: {address}");
|
|||
|
|
|
|||
|
|
while (!token.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 1. 阻塞等待请求 (超时1秒以便响应 Cancel 信号)
|
|||
|
|
if (!_repSocket.TryReceiveFrameString(TimeSpan.FromSeconds(1), out string reqJson))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
// 2. 处理业务 (带 去重 + ID回填 逻辑)
|
|||
|
|
CommandResult result = this.ProcessRequest(reqJson);
|
|||
|
|
|
|||
|
|
// 3. 发送回执
|
|||
|
|
// 注意:REP 模式必须发送应答,即使 result 为 null (Fire-and-Forget) 也建议发一个空 ACK 防止 Socket 状态错乱
|
|||
|
|
// 但为了协议统一,建议 Passive 模式下总是返回结果
|
|||
|
|
string respJson = result != null ? JsonConvert.SerializeObject(result) : "{}";
|
|||
|
|
_repSocket.SendFrame(respJson);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[CmdBus-Passive] 异常: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 5. 模式实现:主动投递 (Active) ---
|
|||
|
|
|
|||
|
|
private void RunActiveMode(string address, CancellationToken token)
|
|||
|
|
{
|
|||
|
|
// 外层循环:断线重连机制
|
|||
|
|
while (!token.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
using (_dealerSocket = new DealerSocket())
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[CmdBus] [主动模式] 正在连接指令中心: {address}");
|
|||
|
|
_dealerSocket.Connect(address);
|
|||
|
|
|
|||
|
|
// ★★★ 关键步骤:连接成功后,立即发送【身份注册包】 ★★★
|
|||
|
|
// 客户端收到这个包后,才能在界面上显示"设备在线"
|
|||
|
|
SendRegistration(_dealerSocket);
|
|||
|
|
|
|||
|
|
// 内层循环:消息收发
|
|||
|
|
while (!token.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
// 1. 接收指令
|
|||
|
|
// DealerSocket 是异步全双工的,这里即使没收到消息也不会阻塞发送
|
|||
|
|
if (!_dealerSocket.TryReceiveFrameString(TimeSpan.FromSeconds(1), out string reqJson))
|
|||
|
|
{
|
|||
|
|
// 空闲周期,可在此处添加心跳发送逻辑 (Ping)
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 处理业务 (带 去重 + ID回填 逻辑)
|
|||
|
|
CommandResult result = this.ProcessRequest(reqJson);
|
|||
|
|
|
|||
|
|
// 3. 发送结果 (QoS控制)
|
|||
|
|
// 如果结果为 null,说明指令是 Fire-and-Forget (无需回执),则不发送网络包节省带宽
|
|||
|
|
if (result != null)
|
|||
|
|
{
|
|||
|
|
_dealerSocket.SendFrame(JsonConvert.SerializeObject(result));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[CmdBus-Active] 连接中断或异常: {ex.Message}");
|
|||
|
|
// 避免死循环狂刷 CPU,等待 3 秒再重连
|
|||
|
|
Thread.Sleep(3000);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 发送身份注册包 (Active 模式专用)
|
|||
|
|
/// </summary>
|
|||
|
|
private void SendRegistration(DealerSocket socket)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 计算实际端口信息
|
|||
|
|
int portOffset = _processId - 1;
|
|||
|
|
var regInfo = new ServerRegistrationDto
|
|||
|
|
{
|
|||
|
|
ProcessId = _processId,
|
|||
|
|
InstanceId = $"Gateway_{_processId}",
|
|||
|
|
ServerIp = GetLocalIpAddress(),
|
|||
|
|
WebApiPort = 5000 + portOffset,
|
|||
|
|
VideoPort = 5555 + portOffset,
|
|||
|
|
CmdPort = 7000 + portOffset,
|
|||
|
|
StartTime = DateTime.Now,
|
|||
|
|
Description = "Active Mode Connection (V2)"
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 封装信封 (系统级指令)
|
|||
|
|
var payload = new CommandPayload
|
|||
|
|
{
|
|||
|
|
CmdCode = "SERVER_REGISTER",
|
|||
|
|
TargetId = "SYSTEM",
|
|||
|
|
JsonParams = JsonConvert.SerializeObject(regInfo),
|
|||
|
|
RequestId = Guid.NewGuid().ToString("N"),
|
|||
|
|
RequireAck = false // 注册包通常不需要回执,只要连上就行
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
socket.SendFrame(JsonConvert.SerializeObject(payload));
|
|||
|
|
Console.WriteLine($"[CmdBus] 身份注册包已发送 -> {regInfo.ServerIp}:{regInfo.WebApiPort}");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[CmdBus] 注册包发送失败: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private string GetLocalIpAddress()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
|
|||
|
|
foreach (var ip in host.AddressList)
|
|||
|
|
{
|
|||
|
|
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
|||
|
|
return ip.ToString();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
return "127.0.0.1";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 6. 协议处理核心 (★★ V2 核心增强 ★★) ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 统一处理请求协议:去重 -> 排序 -> 执行 -> 回填 ID
|
|||
|
|
/// </summary>
|
|||
|
|
private CommandResult ProcessRequest(string json)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(json)) return CommandResult.Fail("Empty Request");
|
|||
|
|
|
|||
|
|
CommandPayload? payload;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
payload = JsonConvert.DeserializeObject<CommandPayload>(json);
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
return CommandResult.Fail("Invalid JSON Protocol");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (payload == null) return CommandResult.Fail("Null Payload");
|
|||
|
|
|
|||
|
|
// =========================================================
|
|||
|
|
// A. 【幂等性检查】(Idempotency Check)
|
|||
|
|
// =========================================================
|
|||
|
|
// 客户端如果没收到回执,会发起重试。我们不能让云台转两次。
|
|||
|
|
// 检查缓存中是否已有该 RequestId 的结果。
|
|||
|
|
if (_cache.TryGetValue(payload.RequestId, out CommandResult cachedResult))
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[Dedup] 拦截重复请求: {payload.RequestId} (Retry: {payload.RetryCount})");
|
|||
|
|
return cachedResult; // 直接返回上次执行的结果,不调用业务层
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =========================================================
|
|||
|
|
// B. 【顺序一致性检查】(Order Guarantee)
|
|||
|
|
// =========================================================
|
|||
|
|
// 仅针对具体的设备指令 (非 SYSTEM)。防止 "停止" 指令先于 "开始" 指令被处理(乱序)。
|
|||
|
|
if (payload.TargetId != "SYSTEM")
|
|||
|
|
{
|
|||
|
|
lock (_deviceLastCmdTime)
|
|||
|
|
{
|
|||
|
|
if (_deviceLastCmdTime.TryGetValue(payload.TargetId, out DateTime lastTime))
|
|||
|
|
{
|
|||
|
|
// 如果当前指令的时间戳 早于 最后一次执行的时间戳,说明是迟到的旧包
|
|||
|
|
if (payload.Timestamp < lastTime)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[Order] 丢弃乱序指令: {payload.CmdCode} (Time: {payload.Timestamp})");
|
|||
|
|
return CommandResult.Fail("Order Violation: Stale Command Dropped");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 更新该设备的最后指令时间
|
|||
|
|
_deviceLastCmdTime[payload.TargetId] = payload.Timestamp;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =========================================================
|
|||
|
|
// C. 【业务执行】
|
|||
|
|
// =========================================================
|
|||
|
|
// 调用纯逻辑层
|
|||
|
|
CommandResult result;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
result = CommandBusProcessor.ProcessBusinessLogic(_cameraManager, payload);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
result = CommandResult.Fail($"Internal Logic Error: {ex.Message}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// =========================================================
|
|||
|
|
// D. 【闭环回填】(Back-fill RequestId)
|
|||
|
|
// =========================================================
|
|||
|
|
// 无论成功失败,必须带上 RequestId,否则客户端 await 无法匹配
|
|||
|
|
result.RequestId = payload.RequestId;
|
|||
|
|
|
|||
|
|
// =========================================================
|
|||
|
|
// E. 【状态缓存】
|
|||
|
|
// =========================================================
|
|||
|
|
// 将结果存入缓存,有效期设为 10 秒 (覆盖客户端的重试窗口)
|
|||
|
|
// 这样 10 秒内收到同样的 RequestId,直接走上面的步骤 A 返回
|
|||
|
|
_cache.Set(payload.RequestId, result, TimeSpan.FromSeconds(10));
|
|||
|
|
|
|||
|
|
// =========================================================
|
|||
|
|
// F. 【QoS 过滤】
|
|||
|
|
// =========================================================
|
|||
|
|
// 如果客户端声明 RequireAck = false (如心跳包),返回 null
|
|||
|
|
// 外层调用者会根据 null 决定不发送网络数据
|
|||
|
|
if (!payload.RequireAck)
|
|||
|
|
{
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
==============================================================================
|
|||
|
|
FILE PATH: PipelineConfigurator.cs
|
|||
|
|
==============================================================================
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
==============================================================================
|
|||
|
|
FILE PATH: Program.cs
|
|||
|
|
==============================================================================
|
|||
|
|
|
|||
|
|
using Microsoft.AspNetCore.Builder;
|
|||
|
|
using Microsoft.Extensions.DependencyInjection;
|
|||
|
|
using Microsoft.OpenApi.Models;
|
|||
|
|
using SHH.CameraSdk; // 引用你的业务核心
|
|||
|
|
using SHH.NetMQ;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraService;
|
|||
|
|
|
|||
|
|
public class Program
|
|||
|
|
{
|
|||
|
|
public static async Task Main(string[] args)
|
|||
|
|
{
|
|||
|
|
#region --- 1. 端口与身份计算 ---
|
|||
|
|
|
|||
|
|
int processId = 1;
|
|||
|
|
// 从命令行参数解析进程ID(默认1)
|
|||
|
|
if (args.Length > 0 && int.TryParse(args[0], out int pid))
|
|||
|
|
processId = pid;
|
|||
|
|
|
|||
|
|
// 计算 Web 服务端口(基础5000 + 进程ID偏移)
|
|||
|
|
int port = 5000 + (processId - 1);
|
|||
|
|
|
|||
|
|
Console.Title = $"SHH Gateway - Instance #{processId} (Port: {port})";
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 2. 硬件环境预热 (【重要】必须在一切开始前调用) ---
|
|||
|
|
|
|||
|
|
InitHardwareEnv();
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 3. 构建 WebHost ---
|
|||
|
|
|
|||
|
|
var builder = WebApplication.CreateBuilder(args);
|
|||
|
|
|
|||
|
|
#region --- A. 注册 ZeroMQ 组件 (传输层) ---
|
|||
|
|
|
|||
|
|
// 注册转发客户端(定向推送)
|
|||
|
|
string zmqBind = $"tcp://*:{5555 + (processId - 1)}";
|
|||
|
|
|
|||
|
|
// ★★★ 新增:注册指令总线服务 ★★★
|
|||
|
|
string zmqTarget = "tcp://127.0.0.1:6000";
|
|||
|
|
|
|||
|
|
// 注册转发客户端(定向推送)
|
|||
|
|
builder.Services.AddSingleton(new ForwarderClient(zmqTarget));
|
|||
|
|
|
|||
|
|
// ★★★ 新增:注册指令总线服务 ★★★
|
|||
|
|
builder.Services.AddHostedService<CommandBusService>();
|
|||
|
|
|
|||
|
|
// 注册分发服务器(广播)
|
|||
|
|
builder.Services.AddSingleton(new DistributorServer(zmqBind));
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- B. 注册核心业务服务 ---
|
|||
|
|
|
|||
|
|
// 注册文件存储服务(进程隔离)
|
|||
|
|
builder.Services.AddSingleton<IStorageService>(new FileStorageService(processId));
|
|||
|
|
|
|||
|
|
// CameraManager 注册为单例,生命周期由 CameraEngineWorker 管理
|
|||
|
|
builder.Services.AddSingleton<CameraManager>();
|
|||
|
|
|
|||
|
|
// 图像处理配置管理器(单例)
|
|||
|
|
builder.Services.AddSingleton<ProcessingConfigManager>();
|
|||
|
|
|
|||
|
|
// 显示窗口管理器(单例)
|
|||
|
|
builder.Services.AddSingleton<DisplayWindowManager>();
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- C. 注册图像处理集群 (修复版) ---
|
|||
|
|
|
|||
|
|
// 说明:通过责任链模式组装 Scale → Enhance 处理流程,确保顺序执行
|
|||
|
|
// 1. 注册图像缩容集群(并行度4)
|
|||
|
|
builder.Services.AddSingleton<ImageScaleCluster>(sp =>
|
|||
|
|
{
|
|||
|
|
var configManager = sp.GetRequiredService<ProcessingConfigManager>();
|
|||
|
|
return new ImageScaleCluster(4, configManager);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 2. 注册图像增强集群(并行度4)
|
|||
|
|
builder.Services.AddSingleton<ImageEnhanceCluster>(sp =>
|
|||
|
|
{
|
|||
|
|
var configManager = sp.GetRequiredService<ProcessingConfigManager>();
|
|||
|
|
return new ImageEnhanceCluster(4, configManager);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3. 注册管道配置服务(组装责任链)
|
|||
|
|
builder.Services.AddHostedService<PipelineConfigurator>();
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- D. 注册 Web 基础服务 ---
|
|||
|
|
|
|||
|
|
// 注册控制器(加载 SDK 中的 CamerasController、MonitorController)
|
|||
|
|
builder.Services.AddControllers()
|
|||
|
|
.AddApplicationPart(typeof(CamerasController).Assembly) // 加载 SDK 中的控制器
|
|||
|
|
.AddApplicationPart(typeof(MonitorController).Assembly)
|
|||
|
|
.AddControllersAsServices();
|
|||
|
|
|
|||
|
|
// 注册全局操作日志过滤器(捕获 API 操作日志)
|
|||
|
|
builder.Services.AddScoped<UserActionFilter>();
|
|||
|
|
|
|||
|
|
// 注册 Swagger 文档(区分实例ID)
|
|||
|
|
builder.Services.AddEndpointsApiExplorer();
|
|||
|
|
builder.Services.AddSwaggerGen(c =>
|
|||
|
|
{
|
|||
|
|
c.SwaggerDoc("v1", new OpenApiInfo
|
|||
|
|
{
|
|||
|
|
Title = $"Gateway #{processId}",
|
|||
|
|
Version = "v1"
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- E. 注册后台服务 (Worker) ---
|
|||
|
|
|
|||
|
|
// 1. 核心引擎工作者 (负责 StartAsync 和 ConfigureBusinessLogic)
|
|||
|
|
builder.Services.AddHostedService<CameraEngineWorker>();
|
|||
|
|
|
|||
|
|
// 2.网络哨兵(负责断线重连)(监控设备断线重连,注册为单例)
|
|||
|
|
builder.Services.AddSingleton<ConnectivitySentinel>();
|
|||
|
|
|
|||
|
|
// 3. ZeroMQ 桥梁服务(转发帧数据到外部系统)
|
|||
|
|
builder.Services.AddHostedService<ZeroMqBridgeService>();
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- F. 配置 CORS(允许所有跨域请求) ---
|
|||
|
|
|
|||
|
|
builder.Services.AddCors(options =>
|
|||
|
|
{
|
|||
|
|
options.AddPolicy("AllowAll", policy =>
|
|||
|
|
{
|
|||
|
|
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 4. 启动应用 ---
|
|||
|
|
|
|||
|
|
var app = builder.Build();
|
|||
|
|
|
|||
|
|
// 启用 Swagger 文档
|
|||
|
|
app.UseSwagger();
|
|||
|
|
app.UseSwaggerUI();
|
|||
|
|
|
|||
|
|
// 启用 CORS 策略
|
|||
|
|
app.UseCors("AllowAll");
|
|||
|
|
|
|||
|
|
// 映射控制器路由
|
|||
|
|
app.MapControllers();
|
|||
|
|
|
|||
|
|
// 输出启动信息
|
|||
|
|
Console.WriteLine($"[System] 绑定 Web 端口: {port}");
|
|||
|
|
Console.WriteLine($"[System] 绑定 ZMQ 端口: {zmqBind}");
|
|||
|
|
|
|||
|
|
// 启动 Web 应用
|
|||
|
|
await app.RunAsync($"http://0.0.0.0:{port}");
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region --- 辅助方法:硬件环境预热 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 初始化硬件环境(海康 SDK 预热)
|
|||
|
|
/// </summary>
|
|||
|
|
static void InitHardwareEnv()
|
|||
|
|
{
|
|||
|
|
Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.5 框架版) ===");
|
|||
|
|
Console.WriteLine("[硬件] 海康驱动预热中...");
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 初始化海康 SDK
|
|||
|
|
HikNativeMethods.NET_DVR_Init();
|
|||
|
|
// 强制预热播放库(避免首次取流延迟)
|
|||
|
|
HikSdkManager.ForceWarmUp();
|
|||
|
|
Console.WriteLine("[硬件] 预热完成。");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine($"[硬件] 预热失败: {ex.Message}");
|
|||
|
|
// 不抛出异常,允许程序在无 DLL 环境下调试
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
==============================================================================
|
|||
|
|
FILE PATH: ZeroMqBridgeService.cs
|
|||
|
|
==============================================================================
|
|||
|
|
|
|||
|
|
using Microsoft.Extensions.Hosting;
|
|||
|
|
using OpenCvSharp;
|
|||
|
|
using SHH.Contracts;
|
|||
|
|
using SHH.NetMQ;
|
|||
|
|
|
|||
|
|
namespace SHH.CameraSdk
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// ZeroMQ 消息桥接服务(后台服务)。
|
|||
|
|
/// 核心职责:订阅系统全局视频帧广播,将帧数据编码为标准协议格式后,通过 ZeroMQ 分发至外部系统。
|
|||
|
|
/// 设计特性:
|
|||
|
|
/// <para>1. 无侵入集成:通过订阅 GlobalStreamDispatcher 事件,无需修改原有帧处理流程。</para>
|
|||
|
|
/// <para>2. 自动适配:支持动态增删设备,无需手动注册设备监听。</para>
|
|||
|
|
/// <para>3. 安全隔离:帧数据深拷贝,避免跨线程内存访问冲突。</para>
|
|||
|
|
/// </summary>
|
|||
|
|
public class ZeroMqBridgeService : BackgroundService
|
|||
|
|
{
|
|||
|
|
#region --- 依赖注入字段 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// ZeroMQ 分发服务器(用于广播帧数据至多个订阅端)
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly DistributorServer _distributor;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// ZeroMQ 转发客户端(用于定向推送帧数据至指定目标)
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly ForwarderClient _forwarder;
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 构造函数 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 初始化 <see cref="ZeroMqBridgeService"/> 实例。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="distributor">ZeroMQ 分发服务器实例(通过 DI 注入)</param>
|
|||
|
|
/// <param name="forwarder">ZeroMQ 转发客户端实例(通过 DI 注入)</param>
|
|||
|
|
public ZeroMqBridgeService(DistributorServer distributor, ForwarderClient forwarder)
|
|||
|
|
{
|
|||
|
|
_distributor = distributor ?? throw new ArgumentNullException(nameof(distributor));
|
|||
|
|
_forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 后台服务核心逻辑 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 启动后台服务,订阅全局视频帧广播。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="stoppingToken">服务停止令牌(用于优雅关闭)</param>
|
|||
|
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine("[ZeroMQ Bridge] 正在连接全局视频帧广播总线...");
|
|||
|
|
|
|||
|
|
// 订阅全局帧广播事件:所有设备的帧数据都会触发该事件
|
|||
|
|
// 无需手动绑定设备,动态增删的设备自动适配
|
|||
|
|
GlobalStreamDispatcher.OnGlobalFrame += OnGlobalFrameReceived;
|
|||
|
|
|
|||
|
|
Console.WriteLine("[ZeroMQ Bridge] 全局总线连接成功!已开始监听所有设备帧数据。");
|
|||
|
|
Console.WriteLine("[ZeroMQ Bridge] 说明:动态增删的设备会自动转发,无需重启服务。");
|
|||
|
|
|
|||
|
|
// 返回空任务:服务通过事件驱动,无需阻塞主线程
|
|||
|
|
return Task.CompletedTask;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 停止后台服务,取消事件订阅以避免内存泄漏。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="cancellationToken">取消令牌</param>
|
|||
|
|
public override Task StopAsync(CancellationToken cancellationToken)
|
|||
|
|
{
|
|||
|
|
Console.WriteLine("[ZeroMQ Bridge] 正在停止服务,取消全局总线订阅...");
|
|||
|
|
|
|||
|
|
// 取消事件订阅:必须执行,否则会导致内存泄漏
|
|||
|
|
GlobalStreamDispatcher.OnGlobalFrame -= OnGlobalFrameReceived;
|
|||
|
|
|
|||
|
|
Console.WriteLine("[ZeroMQ Bridge] 服务已安全停止。");
|
|||
|
|
return base.StopAsync(cancellationToken);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region --- 帧数据处理核心逻辑 ---
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 全局帧数据接收回调(事件处理函数)。
|
|||
|
|
/// 处理流程:安全检查 → 帧数据深拷贝 → JPG 编码 → 封装为标准协议 → ZeroMQ 分发。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="deviceId">产生该帧的设备唯一标识</param>
|
|||
|
|
/// <param name="frame">智能帧对象(包含原始/处理后图像数据)</param>
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|