增加了通过网络主动上报图像的支持

增加了指令维护通道的支持
This commit is contained in:
2026-01-07 10:59:03 +08:00
parent a697aab3e0
commit 3d47c8f009
47 changed files with 1613 additions and 1734 deletions

View File

@@ -0,0 +1,139 @@
using Microsoft.Extensions.Hosting;
using NetMQ;
using NetMQ.Sockets;
using Newtonsoft.Json;
using SHH.CameraSdk;
using System.Text;
namespace SHH.CameraService;
public class CommandClientWorker : BackgroundService
{
private readonly ServiceConfig _config;
private readonly CommandDispatcher _dispatcher; // 注入分发器
public CommandClientWorker(ServiceConfig config, CommandDispatcher dispatcher)
{
_config = config;
_dispatcher = dispatcher;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// =================================================================
// ★★★ 核心修复:强制让出主线程 ★★★
// 这行代码会让当前的 ExecuteAsync 立即返回一个未完成的 Task 给 Host
// Host 就会认为 "这个服务启动好了",然后继续去启动 WebAPI。
// 而剩下的代码会被调度到线程池里异步执行,互不干扰。
// =================================================================
await Task.Yield();
// 1. 如果不是主动/混合模式,不需要连接
if (!_config.ShouldConnect) return;
var cmdEndpoints = _config.CommandEndpoints;
if (cmdEndpoints.Count == 0)
{
Console.WriteLine("[指令] 未配置指令通道,跳过注册。");
return;
}
// 2. 初始化 Dealer Socket
using var dealer = new DealerSocket();
// ★★★ 关键:设置身份标识 (Identity) ★★★
// 服务端 (Router) 收到消息时,第一帧就是这个 ID
// 如果不设ZMQ 会随机生成一个二进制 ID服务端就不知道你是谁了
string myIdentity = _config.AppId;
dealer.Options.Identity = Encoding.UTF8.GetBytes(myIdentity);
// 3. 连接所有目标 (遍历 ServiceEndpoint 对象)
foreach (var ep in cmdEndpoints)
{
Console.WriteLine($"[指令] 连接控制端: {ep.Uri} [{ep.Description}]");
try
{
dealer.Connect(ep.Uri);
}
catch (Exception ex)
{
Console.WriteLine($"[指令] 连接失败 {ep.Uri}: {ex.Message}");
}
}
// 1. 获取本机 IP (简单的获取方式,用于上报给 Dashboard)
string localIp = "127.0.0.1";
try
{
// 简单获取首个非回环 IP生产环境建议用更严谨的帮助类
var host = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
localIp = host.AddressList.FirstOrDefault(ip =>
ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)?.ToString() ?? "127.0.0.1";
}
catch { }
// 4. 构建注册/登录包
var registerPayload = new
{
Action = "Register",
Payload = new
{
// 1. AppId (身份)
Id = _config.AppId,
// 2. Version (程序集版本)
Version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "1.0.0",
// 3. 进程 ID (用于远程监控)
Pid = Environment.ProcessId,
// 4. 关键端口信息
// 告诉 Dashboard如果你想调我的 REST API请访问这个端口
WebPort = _config.BasePort,
// 如果您有本地绑定的 ZMQ 端口也可以在这里上报
// VideoPort = _config.BasePort + 1,
// 基础网络信息
Ip = localIp,
// 附带信息:我是要把视频推给谁 (供 Dashboard 调试用)
TargetVideoNodes = _config.VideoEndpoints.Select(e => e.Uri).ToList()
},
Time = DateTime.Now
};
string json = JsonConvert.SerializeObject(registerPayload);
// 5. 发送注册包
// Dealer 连接建立是异步的所以这里直接发ZMQ 会在底层连接成功后自动把消息推出去
// 为了保险,对于多个 EndpointDealer 默认是负载均衡发送的(轮询)。
// 如果想让每个 Endpoint 都收到注册包,这在 Dealer 模式下稍微有点特殊。
// 但通常我们只需要发一次,只要有一个 Dashboard 收到并建立会话即可。
// 或者简单粗暴:循环发送几次,确保覆盖。
Console.WriteLine($"[指令] 发送注册包: {json}");
dealer.SendFrame(json);
// 6. 进入监听循环 (等待 ACK 或 指令)
// 进入监听循环
while (!stoppingToken.IsCancellationRequested)
{
try
{
if (dealer.TryReceiveFrameString(TimeSpan.FromMilliseconds(500), out string msg))
{
Console.WriteLine($"[指令] 收到消息: {msg}");
// ★★★ 核心变化:直接扔给分发器 ★★★
// 无论未来加多少指令,这里都不用改代码
await _dispatcher.DispatchAsync(msg);
}
}
catch (Exception ex)
{
Console.WriteLine($"[指令] 异常: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,46 @@
using Newtonsoft.Json.Linq;
namespace SHH.CameraService;
public class CommandDispatcher
{
// 路由表Key = ActionName, Value = Handler
private readonly Dictionary<string, ICommandHandler> _handlers;
// 通过依赖注入拿到所有实现了 ICommandHandler 的类
public CommandDispatcher(IEnumerable<ICommandHandler> handlers)
{
_handlers = handlers.ToDictionary(h => h.ActionName, h => h);
}
public async Task DispatchAsync(string jsonMessage)
{
try
{
var jObj = JObject.Parse(jsonMessage);
string action = jObj["Action"]?.ToString();
var payload = jObj["Payload"];
if (string.IsNullOrEmpty(action)) return;
// 1. 查找是否有对应的处理器
if (_handlers.TryGetValue(action, out var handler))
{
await handler.ExecuteAsync(payload);
}
else if (action == "ACK")
{
// ACK 是特殊的,可以直接在这里处理或者忽略
Console.WriteLine($"[指令] 握手成功: {jObj["Message"]}");
}
else
{
Console.WriteLine($"[警告] 未知的指令: {action}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[分发错误] {ex.Message}");
}
}
}

View File

@@ -0,0 +1,30 @@
namespace SHH.CameraService;
/// <summary>
/// 在线客户端信息模型 (已更新)
/// </summary>
public class ConnectedClient
{
/// <summary> 唯一标识 (AppId) </summary>
public string ServiceId { get; set; } = string.Empty;
/// <summary> 版本号 </summary>
public string Version { get; set; } = "1.0.0";
/// <summary> 远程进程 ID </summary>
public int Pid { get; set; }
/// <summary> 客户端 IP </summary>
public string Ip { get; set; } = string.Empty;
/// <summary> WebAPI 端口 (Dashboard 调用 REST 接口用) </summary>
public int WebPort { get; set; }
/// <summary> 该客户端正在推流的目标地址 </summary>
public List<string> TargetVideoNodes { get; set; } = new List<string>();
public DateTime LastHeartbeat { get; set; }
// 辅助属性:拼接出完整的 API BaseUrl
public string WebApiUrl => $"http://{Ip}:{WebPort}";
}

View File

@@ -0,0 +1,18 @@
namespace SHH.CameraService;
/// <summary>
/// 抽象指令处理器接口
/// </summary>
public interface ICommandHandler
{
/// <summary>
/// 该处理器支持的 Action 名称 (如 "AddCamera", "Reboot")
/// </summary>
string ActionName { get; }
/// <summary>
/// 执行指令逻辑
/// </summary>
/// <param name="payload">指令携带的数据 (JSON JToken)</param>
Task ExecuteAsync(Newtonsoft.Json.Linq.JToken payload);
}

View File

@@ -0,0 +1,103 @@
using Newtonsoft.Json.Linq;
using SHH.CameraSdk; // 引用包含 FrameController 和 FrameRequirement 的命名空间
using SHH.Contracts;
namespace SHH.CameraService;
public class SyncCameraHandler : ICommandHandler
{
private readonly CameraManager _cameraManager;
public string ActionName => "SyncCamera";
public SyncCameraHandler(CameraManager cameraManager)
{
_cameraManager = cameraManager;
}
public async Task ExecuteAsync(JToken payload)
{
// 1. 解析配置
var dto = payload.ToObject<CameraConfigDto>();
if (dto == null) return;
// 2. 添加设备到管理器 (这一步是必须的,不然没有 Device 就没有 Controller)
var videoConfig = new VideoSourceConfig
{
Id = dto.Id,
Name = dto.Name,
IpAddress = dto.IpAddress,
Port = dto.Port,
Username = dto.Username,
Password = dto.Password,
ChannelIndex = dto.ChannelIndex,
StreamType = dto.StreamType,
Brand = (DeviceBrand)dto.Brand,
RenderHandle = (IntPtr)dto.RenderHandle,
MainboardIp = dto.MainboardIp,
MainboardPort = dto.MainboardPort,
// 必须给个默认值,防止空引用
VendorArguments = new Dictionary<string, string>(),
};
// 如果设备不存在才添加,如果已存在,后续逻辑会直接获取
if (_cameraManager.GetDevice(videoConfig.Id) == null)
{
_cameraManager.AddDevice(videoConfig);
}
// 3. 核心:直接获取设备实例
var device = _cameraManager.GetDevice(dto.Id);
if (device == null)
{
Console.WriteLine($"[SyncError] 设备 {dto.Id} 创建失败,无法执行自动订阅。");
return;
}
// 4. 拿到你的“宝贝”控制器 (FrameController)
var controller = device.Controller;
if (controller == null)
{
Console.WriteLine($"[SyncError] 设备 {dto.Id} 不支持流控调度 (Controller is null)。");
return;
}
// 5. 暴力注册订阅需求 (Loop AutoSubscriptions)
if (dto.AutoSubscriptions != null && dto.AutoSubscriptions.Count > 0)
{
foreach (var subItem in dto.AutoSubscriptions)
{
// 生成 AppId (照抄你给的逻辑)
string finalAppId = string.IsNullOrWhiteSpace(subItem.AppId)
? $"SUB_{Guid.NewGuid().ToString("N").Substring(0, 8).ToUpper()}"
: subItem.AppId;
Console.WriteLine($"[自动化] 正在注册流控: {finalAppId}, 目标: {subItem.TargetFps} FPS");
// 构造 FrameRequirement 对象 (完全匹配你 FrameController 的入参)
// 这里的属性赋值对应你代码里 req.Type, req.SavePath 等逻辑
var requirement = new FrameRequirement
{
AppId = finalAppId,
TargetFps = subItem.TargetFps, // 8帧 或 1帧
Type = (SubscriptionType)subItem.Type, // 业务类型 (LocalWindow, NetworkTrans...)
Memo = subItem.Memo ?? "Auto Sync",
// 其它字段给默认空值,防止 Controller 内部逻辑报错
Handle = "",
SavePath = ""
};
// ★★★ 见证奇迹的时刻:直接调用 Register ★★★
controller.Register(requirement);
}
}
//// 6. 启动设备
//// 你的积分算法会在 device 内部的推流循环中被 MakeDecision 调用
if (dto.ImmediateExecution)
await device.StartAsync();
Console.WriteLine($"[SyncSuccess] 设备 {dto.Id} 同步完成,策略已下发。");
}
}