From 108c97924fc38cb848e88f4a9de3cc5058135bb2 Mon Sep 17 00:00:00 2001 From: twice109 <3518499@qq.com> Date: Fri, 26 Dec 2025 18:55:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=91=84=E5=83=8F=E5=A4=B4=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=88=86=E8=BE=A8=E7=8E=87?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/CamerasController.cs | 77 +++++------ SHH.CameraSdk/Core/Features/FrameConsumer.cs | 74 +++++++---- SHH.CameraSdk/Core/Manager/CameraManager.cs | 123 +++++++++++++++--- .../Core/Telemetry/CameraTelemetryInfo.cs | 6 + SHH.CameraSdk/Drivers/BaseVideoSource.cs | 10 ++ .../Drivers/HikVision/HikVideoSource.cs | 17 ++- SHH.CameraSdk/Temp/UserActionFilter.cs | 18 +++ 7 files changed, 232 insertions(+), 93 deletions(-) create mode 100644 SHH.CameraSdk/Temp/UserActionFilter.cs diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs index 86fb27b..bd96574 100644 --- a/SHH.CameraSdk/Controllers/CamerasController.cs +++ b/SHH.CameraSdk/Controllers/CamerasController.cs @@ -293,51 +293,40 @@ public class CamerasController : ControllerBase return BadRequest("Device implies no controller"); } + // 1. 获取单个设备详情(用于编辑回填) + [HttpGet("{id}")] + public IActionResult GetDevice(int id) + { + var cam = _manager.GetDevice(id); + if (cam == null) return NotFound(); + return Ok(cam.Config); // 返回原始配置对象 + } - ///// - ///// 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) + // 2. 更新设备(保存功能) + [HttpPut("{id}")] + public async Task UpdateDevice(int id, [FromBody] VideoSourceConfig config) + { + // 核心逻辑:先停止旧设备 -> 更新配置 -> 重新添加到容器 -> 如果之前在运行则重新启动 + await _manager.UpdateDeviceAsync(id, config); + return Ok(); + } + + // 3. 清除特定设备的日志 + [HttpDelete("{id}/logs")] + public IActionResult ClearLogs(int id) + { + var cam = _manager.GetDevice(id); + cam?.ClearAuditLogs(); + return Ok(); + } + + //// 4. 抓图诊断 + //[HttpGet("{id}/capture")] + //public async Task Capture(int id) //{ - // 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 }); + // var cam = _manager.GetDevice(id); + // if (cam == null) return NotFound(); + // var bytes = await cam.CaptureCurrentFrameAsync(); + // return File(bytes, "image/jpeg"); //} } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Features/FrameConsumer.cs b/SHH.CameraSdk/Core/Features/FrameConsumer.cs index 36509e7..7609baa 100644 --- a/SHH.CameraSdk/Core/Features/FrameConsumer.cs +++ b/SHH.CameraSdk/Core/Features/FrameConsumer.cs @@ -110,46 +110,68 @@ public class FrameConsumer : IDisposable /// private void RenderLoop() { - // 创建 OpenCV 显示窗口 - Cv2.NamedWindow(_windowName, WindowFlags.Normal); + bool isWindowCreated = false; try { - // 阻塞遍历队列:队列空时等待,收到取消信号时退出 - foreach (var frame in _frameBuffer.GetConsumingEnumerable(_cts.Token)) + // 我们不再使用简单的 foreach 阻塞等待数据, + // 而是改用非阻塞模式或带有超时的读取,以保证 WaitKey 的活性 + while (!_cts.Token.IsCancellationRequested) { - try + // 尝试在 30ms 内获取一帧数据(相当于 33 fps 的响应速度) + if (_frameBuffer.TryTake(out var frame, 30, _cts.Token)) { - // 渲染有效性校验:Mat 未释放且不为空 - if (frame.InternalMat != null && !frame.InternalMat.IsDisposed) + try { - // 零拷贝渲染:直接引用 InternalMat,无内存复制开销 - Cv2.ImShow(_windowName, frame.InternalMat); - // 1ms 等待 UI 事件响应(必须调用,否则窗口无响应) - Cv2.WaitKey(1); + if (frame.InternalMat != null && !frame.InternalMat.IsDisposed) + { + if (!isWindowCreated) + { + Cv2.NamedWindow(_windowName, WindowFlags.Normal); + isWindowCreated = true; + } + + Cv2.ImShow(_windowName, frame.InternalMat); + } + } + finally + { + frame.Dispose(); } } - catch (Exception ex) + + // 【核心修复】无论有没有取到帧,都要执行 WaitKey + // 只有这样,窗口在没视频时才能被拖动、最小化或手动点击 X 关闭 + if (isWindowCreated) { - Debug.WriteLine($"[RenderError] 窗口[{_windowName}]渲染失败: {ex.Message}"); - } - finally - { - // 至关重要:渲染完成后释放帧引用 - // 引用计数归零 → 帧自动回池复用,避免内存泄漏 - frame.Dispose(); + // 1ms 的等待足以处理 Windows 窗口消息 + int key = Cv2.WaitKey(1); + + // 如果用户点击了 OpenCV 窗口右上角的 X (部分版本支持) + // 或者按下 ESC,可以根据需要在这里处理 + if (key == 27) break; + + // 检查窗口是否还存在(防止用户手动关掉窗口后报错) + // 如果窗口被手动关闭,我们标记为未创建,下次有流时重新弹窗 + try + { + if (Cv2.GetWindowProperty(_windowName, WindowPropertyFlags.Visible) < 1) + { + isWindowCreated = false; + } + } + catch { isWindowCreated = false; } } } } - catch (OperationCanceledException) - { - // 正常取消,无需处理 - Debug.WriteLine($"[RenderInfo] 窗口[{_windowName}]渲染循环已取消"); - } + catch (OperationCanceledException) { } + catch { } finally { - // 销毁 OpenCV 窗口,释放 UI 资源 - Cv2.DestroyWindow(_windowName); + if (isWindowCreated) + { + Cv2.DestroyWindow(_windowName); + } } } diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index 1e21eda..ad572fe 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -39,28 +39,39 @@ public class CameraManager : IDisposable, IAsyncDisposable /// 相机设备配置信息 public void AddDevice(VideoSourceConfig config) { - // [安全防护] 销毁过程中禁止添加新设备 - if (_isDisposed) return; - // 防止重复添加同一设备 - if (_cameraPool.ContainsKey(config.Id)) return; + //// [安全防护] 销毁过程中禁止添加新设备 + //if (_isDisposed) return; + //// 防止重复添加同一设备 + //if (_cameraPool.ContainsKey(config.Id)) return; - // 1. 根据设备品牌实例化对应的驱动实现类 - BaseVideoSource source = config.Brand switch - { - DeviceBrand.HikVision => new HikVideoSource(config), - _ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}") - }; + //// 1. 根据设备品牌实例化对应的驱动实现类 + //BaseVideoSource source = config.Brand switch + //{ + // DeviceBrand.HikVision => new HikVideoSource(config), + // _ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}") + //}; - // 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态 - if (_isEngineStarted) - { - source.IsRunning = true; - } + //// 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态 + //if (_isEngineStarted) + //{ + // source.IsRunning = true; + //} - // 3. 将设备注册到内存池与协调器,纳入统一管理 - if (_cameraPool.TryAdd(config.Id, source)) + //// 3. 将设备注册到内存池与协调器,纳入统一管理 + //if (_cameraPool.TryAdd(config.Id, source)) + //{ + // _coordinator.Register(source); + //} + + // 使用工厂方法创建 + var device = CreateDeviceInstance(config); + + if (!_cameraPool.TryAdd(config.Id, device)) { - _coordinator.Register(source); + // 如果添加失败(ID冲突),由于 device 还没被使用,直接释放掉 + // 这里不需要 await,因为刚 new 出来的对象还没 connect + device.DisposeAsync().AsTask().Wait(); + throw new InvalidOperationException($"设备 ID {config.Id} 已存在"); } } @@ -201,7 +212,10 @@ public class CameraManager : IDisposable, IAsyncDisposable TotalFrames = cam.TotalFrames, HealthScore = healthScore, LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null, - Timestamp = System.DateTime.Now + Timestamp = System.DateTime.Now, + // [新增] 映射分辨率 + Width = cam.Width, + Height = cam.Height, }; }).ToList(); } @@ -342,4 +356,75 @@ public class CameraManager : IDisposable, IAsyncDisposable } #endregion + + /// + /// 更新设备配置(热重载) + /// 流程:停止旧设备 -> 释放资源 -> 创建新设备 -> 替换引用 -> (可选)自动重启 + /// + public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig) + { + // 1. 检查设备是否存在 + if (!_cameraPool.TryGetValue(id, out var oldDevice)) + { + throw new KeyNotFoundException($"设备 #{id} 不存在"); + } + + // 2. 捕获旧状态(用于决定是否需要自动重启新设备) + // 如果旧设备正在运行或尝试连接中,我们在更新后应该尝试恢复它 + bool wasRunning = oldDevice.IsRunning || + oldDevice.Status == VideoSourceStatus.Playing || + oldDevice.Status == VideoSourceStatus.Connecting; + + // 获取旧的流控需求(如果希望更新配置后,之前的订阅依然生效,需要把需求搬过去) + // 这里简化处理:更新配置通常意味着环境变了,我们选择清空旧订阅,让前端重新下发,或者你可以手动拷贝 + // var oldRequirements = oldDevice.Controller.GetCurrentRequirements(); + + Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中..."); + + // 3. 【关键步骤】优雅停止并销毁旧实例 + // 必须先 Stop 再 Dispose,确保 SDK 句柄(如 lUserId, lRealPlayHandle)被释放 + try + { + await oldDevice.StopAsync(); // 停止取流 + await oldDevice.DisposeAsync(); // 注销登录、释放非托管资源 + } + catch (Exception ex) + { + Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}"); + // 继续执行,不要因为旧设备销毁失败阻断新配置的应用 + } + + // 4. 使用新配置创建新实例 + // 建议将创建逻辑提取为私有方法 CreateDeviceInstance,避免与 AddDevice 代码重复 + var newDevice = CreateDeviceInstance(newConfig); + + // 5. 原子替换 (ConcurrentDictionary 的索引器赋值是线程安全的) + _cameraPool[id] = newDevice; + + Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。"); + + // 6. 状态恢复 + if (wasRunning) + { + Console.WriteLine($"[Manager] 检测到设备 #{id} 之前为运行状态,正在自动重启..."); + // 不等待 StartAsync 完成,避免阻塞 HTTP 请求太久 + // 如果希望前端看到转圈直到启动完成,则加上 await + await newDevice.StartAsync(); + } + } + + /// + /// [辅助工厂方法] 根据配置创建具体的驱动实例 + /// 请确保你的 AddDevice 方法也改为调用此方法,减少重复代码 + /// + private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config) + { + return config.Brand switch + { + DeviceBrand.HikVision => new HikVideoSource(config), + // 如果你有大华或其他品牌,在这里扩展 + // DeviceBrand.Dahua => new DahuaVideoSource(config), + _ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}") + }; + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs b/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs index 16c6e17..9b94ee8 100644 --- a/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs +++ b/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs @@ -44,6 +44,12 @@ public class CameraTelemetryInfo /// 实时码率 (Mbps) public double Bitrate { get; set; } + // [新增] 视频宽度 + public int Width { get; set; } + + // [新增] 视频高度 + public int Height { get; set; } + #endregion #region --- 健康度与统计 (Health & Statistics) --- diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index aa652a0..438d7dd 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -683,4 +683,14 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC public FrameController Controller { get; protected set; } #endregion + + // 自动从 SmartFrame 中提取 + public int Width { get; protected set; } + public int Height { get; protected set; } + + public void ClearAuditLogs() + { + _auditLogs.Clear(); + AddAuditLog("用户清空了审计日志"); + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 2c77a00..2fcac11 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -295,6 +295,17 @@ public class HikVideoSource : BaseVideoSource private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2) { + // [优化] 维持心跳,防止被哨兵误杀 + MarkFrameReceived(0); + + // [新增] 捕获并更新分辨率 + // 只有当分辨率发生变化时才写入,减少属性赋值开销 + if (Width != pFrameInfo.nWidth || Height != pFrameInfo.nHeight) + { + Width = pFrameInfo.nWidth; + Height = pFrameInfo.nHeight; + } + // 1. [核心流控] 询问基类控制器:这帧要不要? // 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。 // 传入真实的输入帧率作为参考基准 @@ -303,9 +314,6 @@ public class HikVideoSource : BaseVideoSource // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return; - // [优化] 维持心跳,防止被哨兵误杀 - MarkFrameReceived(0); - int width = pFrameInfo.nWidth; int height = pFrameInfo.nHeight; @@ -348,7 +356,8 @@ public class HikVideoSource : BaseVideoSource } finally { - smartFrame.Dispose(); + if (smartFrame != null) + smartFrame.Dispose(); } } diff --git a/SHH.CameraSdk/Temp/UserActionFilter.cs b/SHH.CameraSdk/Temp/UserActionFilter.cs new file mode 100644 index 0000000..8d171cd --- /dev/null +++ b/SHH.CameraSdk/Temp/UserActionFilter.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +public class UserActionFilter : IActionFilter +{ + public void OnActionExecuted(ActionExecutedContext context) + { + if (context.HttpContext.Request.Method != "GET") + { + var user = context.HttpContext.Connection.RemoteIpAddress; + var path = context.HttpContext.Request.Path; + var method = context.HttpContext.Request.Method; + + // 写入全局操作日志文件或数据库 + File.AppendAllText("user_actions.log", $"{DateTime.Now} | {user} | {method} {path}\n"); + } + } + public void OnActionExecuting(ActionExecutingContext context) { } +} \ No newline at end of file