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