diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs
index ed42fbf..86fb27b 100644
--- a/SHH.CameraSdk/Controllers/CamerasController.cs
+++ b/SHH.CameraSdk/Controllers/CamerasController.cs
@@ -8,9 +8,11 @@ public class CamerasController : ControllerBase
{
private readonly CameraManager _manager;
- public CamerasController(CameraManager manager)
+ // 构造函数注入管理器
+ public CamerasController(CameraManager manager, DisplayWindowManager displayManager)
{
_manager = manager;
+ _displayManager = displayManager;
}
// ==========================================================================
@@ -91,52 +93,6 @@ public class CamerasController : ControllerBase
// 区域 B: 多进程流控订阅 (Subscription Strategy)
// ==========================================================================
- ///
- /// 5. 注册/更新进程的流需求 (A/B/C/D 场景核心)
- ///
- ///
- /// 示例场景:
- /// - 主进程配置(B): { "appId": "Main_Config", "displayFps": 25, "analysisFps": 0 }
- /// - AI进程(C): { "appId": "AI_Core", "displayFps": 0, "analysisFps": 5 }
- ///
- [HttpPost("{id}/subscriptions")]
- public IActionResult UpdateSubscription(long id, [FromBody] SubscriptionDto sub)
- {
- var device = _manager.GetDevice(id);
- if (device == null) return NotFound();
-
- // 逻辑转换:将 "显示帧" 和 "分析帧" 映射到底层控制器的注册表
-
- // 1. 处理显示需求
- string displayKey = $"{sub.AppId}_Display";
- if (sub.DisplayFps > 0)
- {
- // 告诉控制器:这个 App 需要 X 帧用于显示
- device.Controller.Register(displayKey, sub.DisplayFps);
- }
- else
- {
- // 如果不需要,移除注册
- device.Controller.Unregister(displayKey);
- }
-
- // 2. 处理分析需求
- string analysisKey = $"{sub.AppId}_Analysis";
- if (sub.AnalysisFps > 0)
- {
- // 告诉控制器:这个 App 需要 Y 帧用于分析
- device.Controller.Register(analysisKey, sub.AnalysisFps);
- }
- else
- {
- device.Controller.Unregister(analysisKey);
- }
-
- // 运维审计
- device.AddAuditLog($"更新订阅策略 [{sub.AppId}]: Display={sub.DisplayFps}, Analysis={sub.AnalysisFps}");
-
- return Ok(new { Message = "订阅策略已更新", DeviceId = id });
- }
// ==========================================================================
// 区域 C: 句柄动态绑定 (Handle Binding)
@@ -295,4 +251,93 @@ public class CamerasController : ControllerBase
return Ok(subs);
}
+
+ private readonly DisplayWindowManager _displayManager; // [新增]
+
+ [HttpPost("{id}/subscriptions")]
+ public IActionResult UpdateSubscription(int id, [FromBody] SubscriptionDto dto)
+ {
+ var device = _manager.GetDevice(id);
+ if (device == null) return NotFound("设备不存在");
+
+ if (device is HikVideoSource hikCam)
+ {
+ // 1. 更新流控策略 (FrameController)
+ // 告诉底层:这个 AppId 需要多少帧
+ int totalFps = dto.DisplayFps + dto.AnalysisFps;
+
+ if (totalFps > 0)
+ {
+ // 情况 A: 这是一个新增或更新订阅
+ hikCam.Controller.Register(dto.AppId, totalFps);
+
+ // 如果是预览模式,启动窗口
+ if (dto.DisplayFps > 0)
+ {
+ _displayManager.StartDisplay(dto.AppId, id);
+ }
+ }
+ else
+ {
+ // 情况 B: 这是一个停止订阅请求 (FPS 为 0)
+ // 1. 【核心修复】从调度中心物理删除,不再出现在列表中
+ hikCam.Controller.Unregister(dto.AppId);
+
+ // 2. 关闭可能存在的本地窗口
+ _displayManager.StopDisplay(dto.AppId);
+ }
+
+ return Ok(new { message = "Policy updated", currentConfig = hikCam.Controller.GetCurrentRequirements() });
+ }
+
+ return BadRequest("Device implies no controller");
+ }
+
+
+ /////
+ ///// 5. 注册/更新进程的流需求 (A/B/C/D 场景核心)
+ /////
+ /////
+ ///// 示例场景:
+ ///// - 主进程配置(B): { "appId": "Main_Config", "displayFps": 25, "analysisFps": 0 }
+ ///// - AI进程(C): { "appId": "AI_Core", "displayFps": 0, "analysisFps": 5 }
+ /////
+ //[HttpPost("{id}/subscriptions")]
+ //public IActionResult UpdateSubscription(long id, [FromBody] SubscriptionDto sub)
+ //{
+ // var device = _manager.GetDevice(id);
+ // if (device == null) return NotFound();
+
+ // // 逻辑转换:将 "显示帧" 和 "分析帧" 映射到底层控制器的注册表
+
+ // // 1. 处理显示需求
+ // string displayKey = $"{sub.AppId}_Display";
+ // if (sub.DisplayFps > 0)
+ // {
+ // // 告诉控制器:这个 App 需要 X 帧用于显示
+ // device.Controller.Register(displayKey, sub.DisplayFps);
+ // }
+ // else
+ // {
+ // // 如果不需要,移除注册
+ // device.Controller.Unregister(displayKey);
+ // }
+
+ // // 2. 处理分析需求
+ // string analysisKey = $"{sub.AppId}_Analysis";
+ // if (sub.AnalysisFps > 0)
+ // {
+ // // 告诉控制器:这个 App 需要 Y 帧用于分析
+ // device.Controller.Register(analysisKey, sub.AnalysisFps);
+ // }
+ // else
+ // {
+ // device.Controller.Unregister(analysisKey);
+ // }
+
+ // // 运维审计
+ // device.AddAuditLog($"更新订阅策略 [{sub.AppId}]: Display={sub.DisplayFps}, Analysis={sub.AnalysisFps}");
+
+ // return Ok(new { Message = "订阅策略已更新", DeviceId = id });
+ //}
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs b/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs
index 285c98d..4417280 100644
--- a/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs
+++ b/SHH.CameraSdk/Core/Pipeline/GlobalStreamDispatcher.cs
@@ -177,4 +177,25 @@ public static class GlobalStreamDispatcher
}
#endregion
+
+ #region Unsubscribe
+
+ ///
+ /// [新增重载] 强制取消订阅:直接移除指定 AppId 的整个路由项
+ /// 用途:当业务模块(如播放窗口)销毁时,彻底切断该 AppId 的数据流
+ ///
+ /// 业务唯一标识
+ public static void Unsubscribe(string appId)
+ {
+ if (string.IsNullOrWhiteSpace(appId)) return;
+
+ // 直接从字典中移除 Key,这将丢弃该 Key 下挂载的所有委托链
+ // TryRemove 是原子的、线程安全的
+ if (_routingTable.TryRemove(appId, out _))
+ {
+ Console.WriteLine($"[Dispatcher] 已强制移除 AppId [{appId}] 的所有订阅路由");
+ }
+ }
+
+ #endregion
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs
index 2b5ff9b..e3f2f79 100644
--- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs
+++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs
@@ -36,8 +36,15 @@ public class FrameController
public void Unregister(string appId)
{
+ if (string.IsNullOrWhiteSpace(appId)) return;
+
+ // 1. 从需求配置中移除
_requirements.TryRemove(appId, out _);
- _accumulators.TryRemove(appId, out _); // 同步清理,防止内存泄漏
+
+ // 2. 从积分累加器中移除(防止内存泄漏)
+ _accumulators.TryRemove(appId, out _);
+
+ Console.WriteLine($"[Scheduler] 已从调度中心彻底移除 AppId: {appId}");
}
// ---------------------------------------------------------
diff --git a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
new file mode 100644
index 0000000..407193e
--- /dev/null
+++ b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
@@ -0,0 +1,63 @@
+namespace SHH.CameraSdk;
+
+///
+/// 动态窗口管理器
+/// 职责:根据业务指令动态创建/销毁 OpenCV 播放窗口,并管理流订阅
+///
+public class DisplayWindowManager
+{
+ // 存储活跃的渲染器实例:Key = AppId (如 "UI_Preview_Main")
+ private readonly ConcurrentDictionary _activeWindows = new();
+
+ ///
+ /// 开启一个本地播放窗口
+ ///
+ /// 业务标识 (将作为窗口标题)
+ /// 要观看的设备ID
+ public void StartDisplay(string appId, int deviceId)
+ {
+ // 如果窗口已存在,直接返回(防止重复创建)
+ if (_activeWindows.ContainsKey(appId)) return;
+
+ Console.WriteLine($"[DisplayManager] 正在创建窗口: {appId} -> Device {deviceId}...");
+
+ // 1. 动态创建渲染器
+ var renderer = new FrameConsumer(appId);
+
+ // 2. 启动渲染循环 (由于我们之前加了懒加载逻辑,此时不会立即弹窗,直到有帧数据过来)
+ renderer.Start();
+
+ // 3. 存入字典管理
+ if (_activeWindows.TryAdd(appId, renderer))
+ {
+ // 4. 【关键】建立数据订阅:将设备流导向这个渲染器
+ GlobalStreamDispatcher.Subscribe(appId, deviceId, frame =>
+ {
+ // 引用计数 +1,防止在渲染前被回收
+ frame.AddRef();
+ renderer.Enqueue(frame);
+ });
+ }
+ else
+ {
+ renderer.Dispose(); // 并发冲突处理
+ }
+ }
+
+ ///
+ /// 关闭并销毁窗口
+ ///
+ public void StopDisplay(string appId)
+ {
+ if (_activeWindows.TryRemove(appId, out var renderer))
+ {
+ Console.WriteLine($"[DisplayManager] 正在关闭窗口: {appId}");
+
+ // 1. 取消订阅 (停止接收数据)
+ GlobalStreamDispatcher.Unsubscribe(appId);
+
+ // 2. 销毁渲染器 (OpenCV DestroyWindow 会在 FrameConsumer 内部触发)
+ renderer.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs
index 6d547ed..c3bd6d1 100644
--- a/SHH.CameraSdk/Program.cs
+++ b/SHH.CameraSdk/Program.cs
@@ -2,165 +2,149 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
-using OpenCvSharp;
-using SHH.CameraSdk;
-using System.Diagnostics;
-namespace SHH.CameraSdk
+namespace SHH.CameraSdk;
+
+///
+/// A 方案:标准控制台结构 (动态窗口版)
+///
+public class Program
{
- ///
- /// A 方案:标准控制台结构 (显式 Main 方法 + STAThread)
- ///
- public class Program
+ [STAThread]
+ public static async Task Main(string[] args)
{
- // [关键点 1] 显式声明 STA 线程模式,确保 OpenCV/GUI 窗口消息循环正常
- [STAThread]
- public static async Task Main(string[] args)
- {
- // ==============================================================================
- // 1. 基础设施初始化
- // ==============================================================================
- InitHardwareEnv();
- using var cameraManager = new CameraManager();
+ // ==============================================================================
+ // 1. 基础设施初始化
+ // ==============================================================================
+ InitHardwareEnv();
- // [关键点 2] 提升变量作用域
- // 将渲染器(消费者)在 Main 中声明,确保它与主程序同寿命,不会被中途回收
- using var remoteRenderer = new FrameConsumer("Process A Remote Preview");
- remoteRenderer.Start();
+ // 核心设备管理器
+ using var cameraManager = new CameraManager();
- // ==============================================================================
- // 2. 启动 Web 监控与诊断服务
- // ==============================================================================
- var app = await StartWebMonitoring(cameraManager);
-
- // [新增] 启动网络哨兵 (它会自动在后台跑)
- // 就像保安一样,你不需要管它,它每3秒会把所有摄像头的 IsOnline 状态刷一遍
- var sentinel = new ConnectivitySentinel(cameraManager);
-
- // ==============================================================================
- // 3. 业务编排:配置设备与流控策略 (8+2 演示)
- // ==============================================================================
- // [关键点 3] 将渲染器作为参数传递进去
- await ConfigureBusinessLogic(cameraManager, remoteRenderer);
-
- // ==============================================================================
- // 4. 启动引擎与交互
- // ==============================================================================
- Console.WriteLine("\n[系统] 正在启动全局管理引擎...");
- await cameraManager.StartAsync();
-
- Console.WriteLine(">> 系统就绪。访问 http://localhost:5000/swagger 查看诊断信息。");
- Console.WriteLine(">> 按 'S' 键退出...");
-
- // [关键点 4] 阻塞主线程
- // 只要这个循环在跑,remoteRenderer 就不会被 Dispose,窗口就会一直存在
- while (Console.ReadKey(true).Key != ConsoleKey.S)
- {
- Thread.Sleep(100);
- }
-
- Console.WriteLine("[系统] 正在停机...");
- await app.StopAsync();
- }
+ // [新增] 动态窗口管理器 (不再直接 new FrameConsumer)
+ // 这是一个单例服务,负责在运行期间管理所有弹出的窗口
+ var displayManager = new DisplayWindowManager();
// ==============================================================================
- // Static Methods (原 Local Functions 转换为类的静态方法)
+ // 2. 启动 Web 监控与诊断服务 (注入两个管理器)
// ==============================================================================
+ var app = await StartWebMonitoring(cameraManager, displayManager);
- static void InitHardwareEnv()
+ // 启动网络哨兵 (后台 Ping)
+ var sentinel = new ConnectivitySentinel(cameraManager);
+
+ // ==============================================================================
+ // 3. 业务编排:仅配置设备,不配置窗口
+ // ==============================================================================
+ await ConfigureBusinessLogic(cameraManager);
+
+ // ==============================================================================
+ // 4. 启动引擎与交互
+ // ==============================================================================
+ Console.WriteLine("\n[系统] 正在启动全局管理引擎...");
+ await cameraManager.StartAsync();
+
+ Console.WriteLine(">> 系统就绪。");
+ Console.WriteLine(">>当前无播放窗口。请通过 Web 界面 '新增订阅' -> 模式选 'UI_Preview' 来动态打开。");
+ Console.WriteLine(">> 按 'S' 键退出...");
+
+ // 阻塞主线程,保持程序运行
+ while (Console.ReadKey(true).Key != ConsoleKey.S)
{
- Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.3 分层版 - STA模式) ===");
- Console.WriteLine("[硬件] 海康驱动预热中...");
- HikNativeMethods.NET_DVR_Init();
- HikSdkManager.ForceWarmUp(); // 强制加载 PlayCtrl.dll
- Console.WriteLine("[硬件] 预热完成。");
+ Thread.Sleep(100);
}
- static async Task StartWebMonitoring(CameraManager manager)
+ Console.WriteLine("[系统] 正在停机...");
+ await app.StopAsync();
+ }
+
+ // ==============================================================================
+ // Static Methods
+ // ==============================================================================
+
+ static void InitHardwareEnv()
+ {
+ Console.WriteLine("=== 工业级视频 SDK 架构测试 (V3.3 动态窗口版) ===");
+ Console.WriteLine("[硬件] 海康驱动预热中...");
+ HikNativeMethods.NET_DVR_Init();
+ HikSdkManager.ForceWarmUp();
+ Console.WriteLine("[硬件] 预热完成。");
+ }
+
+ // [修改] 签名增加 DisplayWindowManager 参数
+ static async Task StartWebMonitoring(CameraManager manager, DisplayWindowManager displayMgr)
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ builder.Services.AddCors(options =>
{
- var builder = WebApplication.CreateBuilder();
-
- // [新增] 屏蔽日志配置
- builder.Logging.AddFilter("Microsoft", Microsoft.Extensions.Logging.LogLevel.Warning);
- builder.Logging.AddFilter("System", Microsoft.Extensions.Logging.LogLevel.Warning);
- builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", Microsoft.Extensions.Logging.LogLevel.Warning);
-
- // 注入服务
- builder.Services.AddControllers();
- builder.Services.AddEndpointsApiExplorer();
- builder.Services.AddSwaggerGen(c =>
+ options.AddPolicy("AllowAll", policy =>
{
- c.SwaggerDoc("v1", new OpenApiInfo { Title = "SHH Camera Diagnostics", Version = "v1" });
+ policy.AllowAnyOrigin()
+ .AllowAnyHeader()
+ .AllowAnyMethod();
});
- builder.Services.AddCors(o => o.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
+ });
- // 关键:注入单例 Manager
- builder.Services.AddSingleton(manager);
+ // 日志屏蔽
+ builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
+ builder.Logging.AddFilter("System", LogLevel.Warning);
+ builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
- var webApp = builder.Build();
-
- // 配置管道
- webApp.UseSwagger();
- webApp.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Diagnostics V1"));
- webApp.UseCors("AllowAll");
- webApp.MapControllers();
-
- // 异步启动,不阻塞主线程
- _ = webApp.RunAsync("http://0.0.0.0:5000");
- Console.WriteLine("[Web] 监控API已启动: http://localhost:5000");
-
- return webApp;
- }
-
- // [关键点 5] 方法签名修改:接收 FrameConsumer 参数
- static async Task ConfigureBusinessLogic(CameraManager manager, FrameConsumer renderer)
+ builder.Services.AddControllers();
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen(c =>
{
- // 1. 配置设备
- var config = new VideoSourceConfig
- {
- Id = 101,
- Brand = DeviceBrand.HikVision,
- IpAddress = "172.16.41.206",
- //IpAddress = "192.168.5.9",
- Port = 8000,
- Username = "admin",
- Password = "abcd1234",
- //Password = "RRYFOA",
- StreamType = 0 // 主码流
- };
- manager.AddDevice(config);
+ c.SwaggerDoc("v1", new OpenApiInfo { Title = "SHH Camera Diagnostics", Version = "v1" });
+ });
+ builder.Services.AddCors(o => o.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
- if (manager.GetDevice(101) is HikVideoSource hikCamera)
- {
- // 1. 注册差异化需求 (给每个消费者唯一的 AppId)
- // ----------------------------------------------------
- // 1. 注册需求时,手动加上 _Display 后缀
- hikCamera.Controller.Register("Process_A_Remote_Display", 20);
+ // [关键] 注入两个单例服务,让 Controller 能调用它们
+ builder.Services.AddSingleton(manager);
+ builder.Services.AddSingleton(displayMgr);
- // 2. 订阅时,也改用带后缀的名称
- GlobalStreamDispatcher.Subscribe("Process_A_Remote_Display", 101, frame =>
- {
- frame.AddRef();
- renderer.Enqueue(frame);
- });
- }
+ var webApp = builder.Build();
- var config2 = new VideoSourceConfig
- {
- Id = 102,
- Brand = DeviceBrand.HikVision,
- IpAddress = "172.16.41.20",
- Port = 8000,
- Username = "admin",
- Password = "abcd1234",
- StreamType = 0 // 主码流
- };
- manager.AddDevice(config2);
+ webApp.UseSwagger();
+ webApp.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Diagnostics V1"));
+ webApp.UseCors("AllowAll");
+ webApp.MapControllers();
- //if (manager.GetDevice(102) is HikVideoSource hikCamera2)
- //{
+ _ = webApp.RunAsync("http://0.0.0.0:5000");
+ Console.WriteLine("[Web] 监控API已启动: http://localhost:5000");
- //}
- }
+ return webApp;
+ }
+
+ // [修改] 移除 FrameConsumer 参数,不再进行硬编码订阅
+ static async Task ConfigureBusinessLogic(CameraManager manager)
+ {
+ // 1. 仅添加设备配置
+ var config = new VideoSourceConfig
+ {
+ Id = 101,
+ Brand = DeviceBrand.HikVision,
+ IpAddress = "172.16.41.206",
+ Port = 8000,
+ Username = "admin",
+ Password = "abcd1234",
+ StreamType = 0
+ };
+ manager.AddDevice(config);
+
+ var config2 = new VideoSourceConfig
+ {
+ Id = 102,
+ Brand = DeviceBrand.HikVision,
+ IpAddress = "172.16.41.20",
+ Port = 8000,
+ Username = "admin",
+ Password = "abcd1234",
+ StreamType = 0
+ };
+ manager.AddDevice(config2);
+
+ // 注意:此处不再调用 Register 或 Subscribe
+ // 所有的播放请求都将由 WebAPI 收到前端指令后,调用 DisplayWindowManager 来动态发起
}
}
\ No newline at end of file