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

增加了指令维护通道的支持
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

@@ -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();
}
}

View File

@@ -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; }

View File

@@ -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);
}

View File

@@ -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
{

View File

@@ -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)
{