增加了通过网络主动上报图像的支持
增加了指令维护通道的支持
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user