增加了通过网络主动上报图像的支持
增加了指令维护通道的支持
This commit is contained in:
@@ -8,38 +8,44 @@ namespace SHH.CameraSdk;
|
||||
/// </summary>
|
||||
public enum SubscriptionType
|
||||
{
|
||||
/// <summary>
|
||||
/// 仅提供流
|
||||
/// </summary>
|
||||
[Description("仅提供流")]
|
||||
Stream = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 本地窗口渲染
|
||||
/// <para>直接在服务器端显示器绘制(如 OpenCV Window、WinForm 控件)</para>
|
||||
/// </summary>
|
||||
[Description("本地窗口显示")]
|
||||
LocalWindow = 0,
|
||||
LocalWindow = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 本地录像存储
|
||||
/// <para>写入磁盘文件(如 MP4/AVI 格式,支持定时切割、循环覆盖)</para>
|
||||
/// </summary>
|
||||
[Description("本地录像存储")]
|
||||
LocalRecord = 1,
|
||||
LocalRecord = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 句柄绑定显示
|
||||
/// <para>渲染到指定 HWND 窗口句柄(如 SDK 硬件解码渲染到客户端控件)</para>
|
||||
/// </summary>
|
||||
[Description("句柄绑定显示")]
|
||||
HandleDisplay = 2,
|
||||
HandleDisplay = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 自定义网络传输
|
||||
/// <para>通过私有协议转发给第三方系统(如工控机、告警服务器)</para>
|
||||
/// </summary>
|
||||
[Description("自定义网络传输")]
|
||||
NetworkTrans = 3,
|
||||
NetworkTrans = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 网页端推流
|
||||
/// <para>转码为 Web 标准协议(如 WebRTC、HLS、RTMP)供浏览器播放</para>
|
||||
/// </summary>
|
||||
[Description("网页端推流")]
|
||||
WebPush = 4
|
||||
WebPush = 5
|
||||
}
|
||||
@@ -1,227 +1,141 @@
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 全局服务配置模型 (V3 最终版)
|
||||
/// <para>负责解析命令行参数,构建网络拓扑和身份标识</para>
|
||||
/// </summary>
|
||||
public class ServiceConfig
|
||||
{
|
||||
// ==========================================
|
||||
// 1. 身份与进程属性
|
||||
// 1. 基础属性
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// 父进程 PID (用于哨兵守护,--pid)
|
||||
/// </summary>
|
||||
public int ParentPid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用完整标识 (例如 "CameraApp_01", --appid)
|
||||
/// </summary>
|
||||
public string AppId { get; private set; } = "Unknown_01";
|
||||
|
||||
/// <summary>
|
||||
/// 【核心】从 AppId 自动提取的数字编号
|
||||
/// <para>规则:取最后一个下划线后的数字</para>
|
||||
/// <para>示例:"CameraApp_05" -> 5</para>
|
||||
/// </summary>
|
||||
public int NumericId { get; private set; } = 1;
|
||||
|
||||
// ==========================================
|
||||
// 2. 网络连接属性 (分流)
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// 视频流目标地址列表 (对应 & 符号左侧)
|
||||
/// <para>ZeroMQBridgeWorker 使用此列表</para>
|
||||
/// </summary>
|
||||
public List<string> VideoEndpoints { get; private set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 指令控制目标地址列表 (对应 & 符号右侧)
|
||||
/// <para>CommandClientWorker 使用此列表</para>
|
||||
/// </summary>
|
||||
public List<string> CommandEndpoints { get; private set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// WebAPI 基础端口 (--ports 的第一个值)
|
||||
/// </summary>
|
||||
public int BasePort { get; private set; } = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// 端口扫描范围 (--ports 的第二个值)
|
||||
/// </summary>
|
||||
public int MaxPortRange { get; private set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 网络模式 (--mode)
|
||||
/// </summary>
|
||||
public NetworkMode Mode { get; private set; } = NetworkMode.Passive;
|
||||
|
||||
// ==========================================
|
||||
// 3. 辅助属性
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// 是否需要执行 Connect 操作
|
||||
/// </summary>
|
||||
public bool ShouldConnect => Mode == NetworkMode.Active || Mode == NetworkMode.Hybrid;
|
||||
|
||||
// ==========================================
|
||||
// 4. 解析入口 (Factory Method)
|
||||
// 2. 目标地址列表 (类型变了!)
|
||||
// ==========================================
|
||||
|
||||
// ★★★ 修改点:从 List<string> 变为 List<ServiceEndpoint> ★★★
|
||||
public List<ServiceEndpoint> VideoEndpoints { get; private set; } = new List<ServiceEndpoint>();
|
||||
public List<ServiceEndpoint> CommandEndpoints { get; private set; } = new List<ServiceEndpoint>();
|
||||
|
||||
// ==========================================
|
||||
// 3. 工厂方法 (保持不变)
|
||||
// ==========================================
|
||||
public static ServiceConfig BuildFromArgs(string[] args)
|
||||
{
|
||||
var config = new ServiceConfig();
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
// 1. 预处理 Key
|
||||
var key = args[i].ToLower().Trim();
|
||||
var value = (i + 1 < args.Length && !args[i + 1].StartsWith("--")) ? args[i + 1] : string.Empty;
|
||||
bool consumed = !string.IsNullOrEmpty(value);
|
||||
|
||||
// 2. 预取 Value (如果存在且不是下一个 flag)
|
||||
var value = (i + 1 < args.Length) ? args[i + 1] : string.Empty;
|
||||
|
||||
// 简单判断:如果 value 以 -- 开头,说明当前 key 是开关,或者参数值缺失
|
||||
if (value.StartsWith("--")) value = string.Empty;
|
||||
|
||||
bool consumed = false; // 标记是否消耗了下一个参数
|
||||
|
||||
// 3. 匹配参数
|
||||
switch (key)
|
||||
{
|
||||
case "--pid":
|
||||
if (int.TryParse(value, out int pid)) config.ParentPid = pid;
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
case "--pid": if (int.TryParse(value, out int pid)) config.ParentPid = pid; break;
|
||||
case "--appid":
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
config.AppId = value;
|
||||
// ★★★ 立即解析数字编号 ★★★
|
||||
config.NumericId = ParseIdFromAppId(value);
|
||||
}
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
case "--uris":
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
// ★★★ 解析复杂 URI 字符串 ★★★
|
||||
ParseUris(config, value);
|
||||
}
|
||||
consumed = true;
|
||||
if (!string.IsNullOrWhiteSpace(value)) ParseSingleUriConfig(config, value);
|
||||
break;
|
||||
|
||||
case "--mode":
|
||||
if (int.TryParse(value, out int m) && Enum.IsDefined(typeof(NetworkMode), m))
|
||||
{
|
||||
config.Mode = (NetworkMode)m;
|
||||
}
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
case "--mode": if (int.TryParse(value, out int m)) config.Mode = (NetworkMode)m; break;
|
||||
case "--ports":
|
||||
// 格式: "BasePort,Range" -> "6003,100"
|
||||
if (!string.IsNullOrWhiteSpace(value) && value.Contains(","))
|
||||
{
|
||||
var parts = value.Split(',');
|
||||
if (parts.Length >= 1)
|
||||
{
|
||||
if (int.TryParse(parts[0], out int baseP)) config.BasePort = baseP;
|
||||
}
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
if (int.TryParse(parts[1], out int range)) config.MaxPortRange = range;
|
||||
}
|
||||
if (parts.Length >= 1 && int.TryParse(parts[0], out int p)) config.BasePort = p;
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out int r)) config.MaxPortRange = r;
|
||||
}
|
||||
consumed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. 如果消耗了 Value,跳过下一个索引
|
||||
if (consumed) i++;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 5. 核心解析算法实现
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// 算法:提取下划线后的数字
|
||||
/// </summary>
|
||||
private static int ParseIdFromAppId(string appId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appId)) return 1;
|
||||
|
||||
// 查找最后一个下划线
|
||||
int lastIdx = appId.LastIndexOf('_');
|
||||
|
||||
// 确保下划线存在,且后面还有字符
|
||||
if (lastIdx >= 0 && lastIdx < appId.Length - 1)
|
||||
{
|
||||
string numPart = appId.Substring(lastIdx + 1);
|
||||
if (int.TryParse(numPart, out int id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
if (int.TryParse(appId.Substring(lastIdx + 1), out int id)) return id;
|
||||
}
|
||||
|
||||
// 解析失败默认返回 1
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 算法:解析 URI 列表并分流
|
||||
/// <para>格式: IP,VideoPort&CommandPort</para>
|
||||
/// <para>空缺处理: "&6001" (仅指令), "6002&" (仅视频)</para>
|
||||
/// </summary>
|
||||
private static void ParseUris(ServiceConfig config, string rawValue)
|
||||
// ==========================================
|
||||
// 4. 解析算法实现 (核心修改)
|
||||
// ==========================================
|
||||
private static void ParseSingleUriConfig(ServiceConfig config, string rawValue)
|
||||
{
|
||||
// 1. 按分号拆分不同主机配置
|
||||
// "127.0.0.1,6002&6001; 192.168.1.5,&6001"
|
||||
var groups = rawValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var segments = rawValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var group in groups)
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
// 2. 按逗号拆分 IP 和 端口段
|
||||
var hostParts = group.Split(',');
|
||||
if (hostParts.Length < 2) continue; // 格式非法
|
||||
var parts = segment.Split(',');
|
||||
if (parts.Length < 3) continue;
|
||||
|
||||
string ip = hostParts[0].Trim();
|
||||
string portSection = hostParts[1].Trim(); // "6002&6001"
|
||||
string ip = parts[0].Trim();
|
||||
string portStr = parts[1].Trim();
|
||||
string type = parts[2].Trim().ToLower();
|
||||
|
||||
// 3. 按 & 拆分端口 (注意:不要 RemoveEmptyEntries,位置很重要)
|
||||
var ports = portSection.Split('&');
|
||||
// ★★★ 提取第四个字段作为备注 ★★★
|
||||
string desc = parts.Length >= 4 ? parts[3].Trim() : "未命名终端";
|
||||
|
||||
// --- 索引 0: 视频端口 ---
|
||||
if (ports.Length > 0)
|
||||
if (int.TryParse(portStr, out int port))
|
||||
{
|
||||
string p = ports[0].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(p) && int.TryParse(p, out int port))
|
||||
string zmqUri = $"tcp://{ip}:{port}";
|
||||
|
||||
// 构建对象
|
||||
var endpoint = new ServiceEndpoint
|
||||
{
|
||||
string uri = $"tcp://{ip}:{port}";
|
||||
if (!config.VideoEndpoints.Contains(uri))
|
||||
config.VideoEndpoints.Add(uri);
|
||||
Uri = zmqUri,
|
||||
Description = desc
|
||||
};
|
||||
|
||||
// 添加前检查 Uri 是否重复 (备注不参与排重)
|
||||
if (type == "video")
|
||||
{
|
||||
if (!config.VideoEndpoints.Any(e => e.Uri == zmqUri))
|
||||
config.VideoEndpoints.Add(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 索引 1: 指令端口 ---
|
||||
if (ports.Length > 1)
|
||||
{
|
||||
string p = ports[1].Trim();
|
||||
if (!string.IsNullOrWhiteSpace(p) && int.TryParse(p, out int port))
|
||||
else if (type == "command" || type == "text")
|
||||
{
|
||||
string uri = $"tcp://{ip}:{port}";
|
||||
if (!config.CommandEndpoints.Contains(uri))
|
||||
config.CommandEndpoints.Add(uri);
|
||||
if (!config.CommandEndpoints.Any(e => e.Uri == zmqUri))
|
||||
config.CommandEndpoints.Add(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 端点配置对象,包含地址和备注
|
||||
/// </summary>
|
||||
public class ServiceEndpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// ZeroMQ 连接地址 (e.g. "tcp://127.0.0.1:6001")
|
||||
/// </summary>
|
||||
public string Uri { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息 (e.g. "调试机", "大屏")
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ToString
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString() => $"{Uri} ({Description})";
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SHH.Contracts;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
// ==============================================================================
|
||||
// 1. 物理与运行配置 DTO (对应 CRUD 操作)
|
||||
// 用于设备新增/全量配置查询,包含基础身份、连接信息、运行参数等全量字段
|
||||
// ==============================================================================
|
||||
public class CameraConfigDto
|
||||
{
|
||||
// --- 基础身份 (Identity) ---
|
||||
/// <summary>
|
||||
/// 设备唯一标识
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "设备ID不能为空")]
|
||||
[Range(1, long.MaxValue, ErrorMessage = "设备ID必须为正整数")]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备友好名称
|
||||
/// </summary>
|
||||
[MaxLength(64, ErrorMessage = "设备名称长度不能超过64个字符")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头品牌类型 (0:HikVision, 1:Dahua, 2:RTSP...)
|
||||
/// </summary>
|
||||
[Range(0, 10, ErrorMessage = "品牌类型值必须在0-10范围内")]
|
||||
public int Brand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设备安装位置描述
|
||||
/// </summary>
|
||||
[MaxLength(128, ErrorMessage = "安装位置长度不能超过128个字符")]
|
||||
public string Location { get; set; } = string.Empty;
|
||||
|
||||
// --- 核心连接 (Connectivity) - 修改此类参数触发冷重启 ---
|
||||
/// <summary>
|
||||
/// 摄像头IP地址
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "IP地址不能为空")]
|
||||
[RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$",
|
||||
ErrorMessage = "请输入合法的IPv4地址")]
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SDK端口 (如海康默认8000)
|
||||
/// </summary>
|
||||
[Range(1, 65535, ErrorMessage = "端口号必须在1-65535范围内")]
|
||||
public ushort Port { get; set; } = 8000;
|
||||
|
||||
/// <summary>
|
||||
/// 登录用户名
|
||||
/// </summary>
|
||||
[MaxLength(32, ErrorMessage = "用户名长度不能超过32个字符")]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 登录密码
|
||||
/// </summary>
|
||||
[MaxLength(64, ErrorMessage = "密码长度不能超过64个字符")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public long RenderHandle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通道号 (通常为1)
|
||||
/// </summary>
|
||||
[Range(1, 32, ErrorMessage = "通道号必须在1-32范围内")]
|
||||
public int ChannelIndex { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// RTSP流路径 (备用或非SDK模式使用)
|
||||
/// </summary>
|
||||
[MaxLength(256, ErrorMessage = "RTSP地址长度不能超过256个字符")]
|
||||
public string RtspPath { get; set; } = string.Empty;
|
||||
|
||||
// --- 主板关联信息 (Metadata) ---
|
||||
/// <summary>
|
||||
/// 关联主板IP地址
|
||||
/// </summary>
|
||||
[RegularExpression(@"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)?$",
|
||||
ErrorMessage = "请输入合法的IPv4地址")]
|
||||
public string MainboardIp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关联主板端口
|
||||
/// </summary>
|
||||
[Range(1, 65535, ErrorMessage = "主板端口号必须在1-65535范围内")]
|
||||
public int MainboardPort { get; set; } = 80;
|
||||
|
||||
// --- 运行时参数 (Runtime Options) - 支持热更新 ---
|
||||
/// <summary>
|
||||
/// 码流类型 (0:主码流, 1:子码流)
|
||||
/// </summary>
|
||||
[Range(0, 1, ErrorMessage = "码流类型只能是0(主码流)或1(子码流)")]
|
||||
public int StreamType { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用灰度图 (用于AI分析场景加速)
|
||||
/// </summary>
|
||||
public bool UseGrayscale { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用图像增强 (去噪/锐化等)
|
||||
/// </summary>
|
||||
public bool EnhanceImage { get; set; } = true;
|
||||
|
||||
// --- 画面变换 (Transform) - 支持热更新 ---
|
||||
/// <summary>
|
||||
/// 是否允许图像压缩 (降低带宽占用)
|
||||
/// </summary>
|
||||
public bool AllowCompress { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许图像放大 (提升渲染质量)
|
||||
/// </summary>
|
||||
public bool AllowExpand { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 目标分辨率 (格式如 1920x1080,空则保持原图)
|
||||
/// </summary>
|
||||
[RegularExpression(@"^\d+x\d+$", ErrorMessage = "分辨率格式必须为 宽度x高度 (如 1920x1080)")]
|
||||
public string TargetResolution { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace SHH.CameraSdk;
|
||||
public class SubscriptionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 进程唯一标识 (如 "AI_Process_01"、"Main_Display_02")
|
||||
/// 订阅标识 (如 "AI_Process_01"、"Main_Display_02")
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "进程标识 AppId 不能为空")]
|
||||
[MaxLength(50, ErrorMessage = "AppId 长度不能超过 50 个字符")]
|
||||
|
||||
@@ -309,7 +309,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
{
|
||||
var options = new DynamicStreamOptions
|
||||
{
|
||||
StreamType = dto.StreamType,
|
||||
StreamType = dto.StreamType ?? newConfig.StreamType,
|
||||
RenderHandle = (IntPtr)dto.RenderHandle
|
||||
};
|
||||
device.ApplyOptions(options);
|
||||
@@ -428,4 +428,13 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 获取当前管理的所有相机设备(兼容网络引擎接口)
|
||||
/// </summary>
|
||||
public IEnumerable<BaseVideoSource> GetAllCameras()
|
||||
{
|
||||
// 复用现有的 GetAllDevices 逻辑
|
||||
return GetAllDevices();
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,12 @@ public class SmartFrame : IDisposable
|
||||
/// <remarks> 内存由帧池预分配,全程复用,不触发 GC </remarks>
|
||||
public Mat InternalMat { get; private set; }
|
||||
|
||||
/// <summary> [快捷属性] 原始图像宽度 (若 TargetMat 为空则返回 0) </summary>
|
||||
public int InternalWidth => InternalMat?.Width ?? 0;
|
||||
|
||||
/// <summary> [快捷属性] 原始图像高度 (若 TargetMat 为空则返回 0) </summary>
|
||||
public int InnernalHeight => InternalMat?.Height ?? 0;
|
||||
|
||||
/// <summary> 帧激活时间戳(记录帧被取出池的时刻) </summary>
|
||||
public DateTime Timestamp { get; private set; }
|
||||
|
||||
|
||||
@@ -36,11 +36,6 @@ public static class GlobalStreamDispatcher
|
||||
// =================================================================
|
||||
public static event Action<long, SmartFrame> OnGlobalFrame;
|
||||
|
||||
// =================================================================
|
||||
// 2. 原有:定向分发逻辑 (保留不动,给图像处理集群用)
|
||||
// =================================================================
|
||||
// private static ConcurrentDictionary<string, ...> _subscribers ...
|
||||
|
||||
/// <summary>
|
||||
/// 统一入口:驱动层调用此方法分发图像
|
||||
/// </summary>
|
||||
@@ -71,6 +66,10 @@ public static class GlobalStreamDispatcher
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, Action<long, SmartFrame>> _routingTable = new();
|
||||
|
||||
// [新增] 旁路订阅支持
|
||||
// 用于 NetworkService 这种需要针对单个设备进行订阅/取消订阅的场景
|
||||
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<long, Action<SmartFrame>>> _deviceSpecificTable = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 3. 订阅管理接口 (Subscription Management API) ---
|
||||
@@ -98,27 +97,63 @@ public static class GlobalStreamDispatcher
|
||||
);
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// [新增] 精准订阅:仅监听指定设备的特定 AppId 帧
|
||||
///// 优势:内部自动过滤 DeviceId,回调函数无需再写 if 判断
|
||||
///// </summary>
|
||||
///// <param name="appId">需求标识</param>
|
||||
///// <param name="specificDeviceId">只接收此设备的帧</param>
|
||||
///// <param name="handler">处理回调(注意:此处签名不含 deviceId,因为已隐式确定)</param>
|
||||
//public static void Subscribe(string appId, long specificDeviceId, Action<SmartFrame> handler)
|
||||
//{
|
||||
// // 创建一个“过滤器”闭包
|
||||
// Action<long, SmartFrame> wrapper = (id, frame) =>
|
||||
// {
|
||||
// // 只有当来源 ID 与订阅 ID 一致时,才触发用户的业务回调
|
||||
// if (id == specificDeviceId)
|
||||
// {
|
||||
// handler(frame);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // 将过滤器注册到基础路由表中
|
||||
// Subscribe(appId, wrapper);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 精准订阅:仅监听指定设备的特定 AppId 帧
|
||||
/// 优势:内部自动过滤 DeviceId,回调函数无需再写 if 判断
|
||||
/// [重写] 精准订阅:仅监听指定设备的特定 AppId 帧
|
||||
/// 修改说明:不再使用闭包 + 多播委托,而是存入二级字典,以便能精准取消
|
||||
/// </summary>
|
||||
/// <param name="appId">需求标识</param>
|
||||
/// <param name="specificDeviceId">只接收此设备的帧</param>
|
||||
/// <param name="handler">处理回调(注意:此处签名不含 deviceId,因为已隐式确定)</param>
|
||||
public static void Subscribe(string appId, long specificDeviceId, Action<SmartFrame> handler)
|
||||
{
|
||||
// 创建一个“过滤器”闭包
|
||||
Action<long, SmartFrame> wrapper = (id, frame) =>
|
||||
{
|
||||
// 只有当来源 ID 与订阅 ID 一致时,才触发用户的业务回调
|
||||
if (id == specificDeviceId)
|
||||
{
|
||||
handler(frame);
|
||||
}
|
||||
};
|
||||
if (string.IsNullOrWhiteSpace(appId) || handler == null) return;
|
||||
|
||||
// 将过滤器注册到基础路由表中
|
||||
Subscribe(appId, wrapper);
|
||||
// 1. 获取或创建该 AppId 的设备映射表
|
||||
var deviceMap = _deviceSpecificTable.GetOrAdd(appId, _ => new ConcurrentDictionary<long, Action<SmartFrame>>());
|
||||
|
||||
// 2. 添加或更新该设备的订阅
|
||||
// 注意:这里使用多播委托 (+),支持同一个 App 同一个 Device 有多个处理逻辑(虽然很少见)
|
||||
deviceMap.AddOrUpdate(specificDeviceId, handler, (_, existing) => existing + handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [新增] 精准取消订阅:移除指定 AppId 下指定设备的订阅
|
||||
/// NetworkService 必须调用此方法来防止内存泄漏
|
||||
/// </summary>
|
||||
public static void Unsubscribe(string appId, long specificDeviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appId)) return;
|
||||
|
||||
// 1. 查找该 AppId 是否有记录
|
||||
if (_deviceSpecificTable.TryGetValue(appId, out var deviceMap))
|
||||
{
|
||||
// 2. 移除该设备的订阅委托
|
||||
if (deviceMap.TryRemove(specificDeviceId, out _))
|
||||
{
|
||||
// 可选:如果该 AppId 下没设备了,是否清理外层字典?(为了性能通常不清理,或者定期清理)
|
||||
// Console.WriteLine($"[Dispatcher] {appId} 已停止订阅设备 {specificDeviceId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -192,6 +227,26 @@ public static class GlobalStreamDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// B. [新增逻辑] 匹配设备级 AppId 订阅 (如 NetworkService)
|
||||
// =========================================================
|
||||
if (_deviceSpecificTable.TryGetValue(appId, out var deviceMap))
|
||||
{
|
||||
// 查找当前设备是否有订阅者
|
||||
if (deviceMap.TryGetValue(deviceId, out var deviceHandler))
|
||||
{
|
||||
try
|
||||
{
|
||||
deviceHandler.Invoke(frame);
|
||||
task.Context.AddLog($"帧任务 设备级 [Seq:{sequence}] 投递到 AppId:{appId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DispatchError] DeviceSpecific AppId={appId}, Dev={deviceId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 匹配预设的全局通道(兼容旧版订阅逻辑)
|
||||
switch (appId.ToUpperInvariant())
|
||||
{
|
||||
@@ -204,6 +259,43 @@ public static class GlobalStreamDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. [旁路通道] 扫描设备级订阅表 (NetworkService, 录像服务 等)
|
||||
// 这是外部服务“被动”监听的目标,不在 targetAppIds 白名单里也要发
|
||||
// =========================================================================
|
||||
if (!_deviceSpecificTable.IsEmpty)
|
||||
{
|
||||
// 遍历所有注册了旁路监听的 AppId (例如 "NetService")
|
||||
foreach (var kvp in _deviceSpecificTable)
|
||||
{
|
||||
string sidecarAppId = kvp.Key;
|
||||
var deviceMap = kvp.Value;
|
||||
|
||||
// 优化:如果这个 AppId 已经在上面的 targetAppIds 里处理过了,就跳过,防止重复发送
|
||||
// (例如:如果设备未来真的把 NetService 加入了白名单,这里就不重复发了)
|
||||
if (targetAppIds.Contains(sidecarAppId)) continue;
|
||||
|
||||
// 检查这个 AppId 下,是否有人订阅了当前这台设备
|
||||
if (deviceMap.TryGetValue(deviceId, out var handler))
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.Invoke(frame);
|
||||
// task.Context.AddLog($"帧任务 [Seq:{sequence}] 旁路投递到: {sidecarAppId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SidecarDispatchError] App={sidecarAppId}, Dev={deviceId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. [上帝通道] 全局广播
|
||||
// =========================================================================
|
||||
OnGlobalFrame?.Invoke(deviceId, frame);
|
||||
|
||||
// 分发完成后记录遥测数据
|
||||
GlobalTelemetry.RecordLog(sequence, task.Context);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
using OpenCvSharp;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SHH.CameraSdk
|
||||
{
|
||||
@@ -166,6 +161,13 @@ namespace SHH.CameraSdk
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先检查队列容量 (虽然 BlockingCollection 没有完美的无锁 IsFull,但可以通过 Count 判断)
|
||||
// 这是一个不需要 100% 精确的优化,只要能拦截掉大部分无用功即可
|
||||
if (_uiActionQueue.Count >= 30)
|
||||
{
|
||||
return; // 直接丢弃,不进行克隆,节省 CPU
|
||||
}
|
||||
|
||||
Mat frameClone = null;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ public class FileStorageService : IStorageService
|
||||
|
||||
var list = JsonSerializer.Deserialize<List<VideoSourceConfig>>(json, _jsonOptions);
|
||||
return list ?? new List<VideoSourceConfig>();
|
||||
//return new List<VideoSourceConfig>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using OpenCvSharp;
|
||||
using SHH.CameraSdk.HikFeatures;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Security;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
@@ -325,6 +327,9 @@ public class HikVideoSource : BaseVideoSource,
|
||||
|
||||
#region --- 解码与帧分发 (Decoding) ---
|
||||
|
||||
// 必须同时加上 SecurityCritical
|
||||
[HandleProcessCorruptedStateExceptions]
|
||||
[SecurityCritical]
|
||||
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
|
||||
{
|
||||
// [优化] 维持心跳,防止被哨兵误杀
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SHH.Contracts\SHH.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System.Collections.Concurrent" />
|
||||
<Using Include="System.Collections.ObjectModel" />
|
||||
|
||||
Reference in New Issue
Block a user