From 4ab88e3cfe5ee4b6c52e9f10bf1c6e7df0c3c91b Mon Sep 17 00:00:00 2001 From: wilson <3518499@qq.com> Date: Mon, 19 Jan 2026 07:39:59 +0800 Subject: [PATCH] =?UTF-8?q?SDK=E6=A0=B8=E5=BF=83=20Bug=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Drivers/DaHua/DahuaVideoSource.cs | 93 +++++++++++++++---- .../Drivers/HikVision/HikVideoSource.cs | 73 +++++---------- SHH.CameraService/Bootstrapper.cs | 1 + 3 files changed, 100 insertions(+), 67 deletions(-) diff --git a/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs b/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs index 5267688..a099339 100644 --- a/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs +++ b/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs @@ -189,78 +189,137 @@ public class DahuaVideoSource : BaseVideoSource } } - [HandleProcessCorruptedStateExceptions] + /// + /// 解码回调:YUV -> SmartFrame -> Pipeline + /// 已集成:平滑重建、引用交换、Finally释放、[诊断陷阱]、[空帧防御] + /// + [HandleProcessCorruptedStateExceptions] // 捕获非托管状态损坏异常 (AccessViolation) [SecurityCritical] private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref DahuaPlaySDK.FRAME_INFO pFrameInfo, IntPtr nUser, int nReserved2) { + // 1. 基础指针检查 if (pBuf == IntPtr.Zero || nSize <= 0) return; - MarkFrameReceived(0); // 心跳 + // 2. [关键] 音频过滤:如果类型是音频,直接静默返回,防止刷屏日志 + // 大华 SDK 文档:nType >= 100 通常是音频 + if (pFrameInfo.nType >= 100) + { + // 这是一个音频包 (Size=2048 来源于此),不需要记录 Warning,直接丢弃即可 + return; + } + + // 3. 视频有效性检查 + // 如果不是音频,但宽高依然为 0,说明是异常数据或非图像私有头 + if (pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0) + { + // 如果你想调试音频,可以在这里处理 pBuf + // 但对于视频分析业务,这里直接 return,不要写 Log,否则磁盘会爆 + return; + } + + MarkFrameReceived(0); // 维持心跳 (告诉 Watchdog 我还活着) int currentWidth = pFrameInfo.nWidth; int currentHeight = pFrameInfo.nHeight; - // 帧池平滑重建逻辑 (保持与海康版一致,防止死锁) + // ========================================================================================= + // 🛡️ [第二道防线:防死锁重建] 帧池平滑重建逻辑 + // 场景:分辨率变化(主辅码流切换/ROI裁剪)。 + // 作用:使用 TryEnter 替代 lock,防止主线程 Stop 时此处死等导致死锁。 + // ========================================================================================= if (!_isPoolReady || Width != currentWidth || Height != currentHeight) { bool lockTaken = false; try { + // 等待 50ms,拿不到锁就放弃这一帧 Monitor.TryEnter(_initLock, 50, ref lockTaken); + if (lockTaken) { + // Double Check if (!_isPoolReady || Width != currentWidth || Height != currentHeight) { - _framePool?.Dispose(); - var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5); - _framePool = newPool; + _sdkLog.Information($"[Res] 大华分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight}"); + + _framePool?.Dispose(); // 销毁旧池 + + // 更新基类维护的分辨率属性 Width = currentWidth; Height = currentHeight; + + // 重建帧池:initialSize 设为 3 保证高并发缓冲,maxSize 设为 5 严格控制内存总额 + var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5); + _framePool = newPool; + _isPoolReady = true; } } - else return; + else return; // 竞争失败(可能正在停止),直接丢弃,防止卡死 + } + catch (Exception ex) + { + _sdkLog.Error(ex, "[SDK] 大华帧池重建失败"); + return; } finally { if (lockTaken) Monitor.Exit(_initLock); } } + // 1. [核心流控] 询问基类控制器:这帧要不要? + // 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。 + // 传入真实的输入帧率作为参考基准 var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps); + + // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return; SmartFrame? smartFrame = null; try { smartFrame = _framePool?.Get(); - if (smartFrame == null) return; + if (smartFrame == null) return; // 池满丢帧 - // 大华 YUV 转换为 BGR (I420) + // ========================================================================================= + // ⚡ [核心操作:零拷贝转换] + // 大华 PlaySDK 默认输出 I420 (YUV420P)。 + // 使用 Mat.FromPixelData 封装指针,避免内存拷贝。 + // ========================================================================================= using (var yuvMat = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf)) { Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420); } - // ========================================================= - // 【新增防御】: 检查转换结果是否有效 - // 如果转换失败,或者 Mat 为空,绝对不能传给 Router - // ========================================================= + // ========================================================================================= + // 🛡️ [第三道防线:空结果防御] + // 场景:虽然入参合法,但 OpenCV 转换可能失败,或者 Mat 内部状态异常。 + // 作用:绝对禁止将 Empty Mat 传给 Worker,否则 Worker 里的 Resize 会 100% 崩溃。 + // ========================================================================================= if (smartFrame.InternalMat.Empty()) { - _sdkLog.Warning($"[SDK] Dahua 解码帧无效 (Empty Mat), 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}"); - // finally 会负责 Dispose,这里直接返回 - return; + _sdkLog.Warning($"[SDK] Dahua 解码后 Mat 为空 (转换失败), 丢弃. ID:{Config.Id}"); + return; // finally 会负责 Dispose,这里直接安全退出 } + // 填充订阅者 foreach (var appId in decision.TargetAppIds) smartFrame.SubscriberIds.Enqueue(appId); + // 发送到全局路由 (注意:Router 内部必须 AddRef) GlobalPipelineRouter.Enqueue(Id, smartFrame, decision); } catch (Exception ex) { - _sdkLog.Error($"[SDK] Dahua 解码异常: {ex.Message}"); + // 捕获所有异常,包括非托管转换错误,防止回调线程崩溃带崩整个程序 + _sdkLog.Warning(ex, $"[SDK] Dahua 解码/转换流程异常. ID:{Config.Id} Size:{currentWidth}x{currentHeight}"); + AddAuditLog($"[SDK] HDahua SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, Exception: {ex.Message}"); } finally { + // ========================================================================================= + // ♻️ [引用闭环] + // 驱动层完成了它的任务,必须释放它持有的那次引用 (Count 1 -> 0, 或 2 -> 1)。 + // 只有这样,当 Worker 处理完 Dispose 后,帧才能真正回到池子里。 + // ========================================================================================= smartFrame?.Dispose(); } } diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 8a98ac0..feb112c 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -515,9 +515,18 @@ public class HikVideoSource : BaseVideoSource, { //Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达."); - // Optimized: [原因] 增加前置防御性检查,若回调入参异常立即退出,防止后续 OpenCV 封装崩溃 - if (pBuf == IntPtr.Zero || nSize <= 0 - || pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0) return; + // 1. 基础指针检查 + if (pBuf == IntPtr.Zero || nSize <= 0) return; + + // 2. 视频有效性检查 + // 如果不是音频,但宽高依然为 0,说明是异常数据或非图像私有头 + if (pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0) + { + // 如果你想调试音频,可以在这里处理 pBuf + // 但对于视频分析业务,这里直接 return,不要写 Log,否则磁盘会爆 + return; + } + // [优化] 维持心跳,防止被哨兵误杀 MarkFrameReceived(0); @@ -584,59 +593,23 @@ public class HikVideoSource : BaseVideoSource, //Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵处理."); - int width = pFrameInfo.nWidth; - int height = pFrameInfo.nHeight; - - // 2. 初始化帧池 - if (!_isPoolReady) - { - // ==================================================================================== - // 【修改点 Start】: 使用 Monitor.TryEnter 替换 lock - // 原因:防止死锁。如果主线程 CleanupSync 持有 _initLock 正在 Stop, - // 这里如果用 lock 会死等,导致 StopRealPlay 无法返回。 - // 改用 TryEnter,如果拿不到锁(说明正在停止),直接放弃这一帧并退出。 - // ==================================================================================== - bool lockTaken = false; - try - { - // 尝试获取锁,超时时间 20ms (拿不到立即返回 false) - Monitor.TryEnter(_initLock, 20, ref lockTaken); - - if (lockTaken) - { - // 拿到锁了,执行原有的初始化逻辑 (Double Check) - if (!_isPoolReady) - { - _framePool?.Dispose(); - _framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5); - _isPoolReady = true; - } - } - else - { - // 【关键逻辑】如果 20ms 没拿到锁,说明主线程正在操作 (通常是正在 Stop) - // 既然都要停止了,这一帧直接丢弃,立即返回,防止死锁 - return; - } - } - finally - { - if (lockTaken) Monitor.Exit(_initLock); - } - } - - if (_framePool == null) return; - // Optimized: [原因] 将 smartFrame 定义在 try 外部,确保 finally 块能够可靠执行 Dispose 归还逻辑 SmartFrame? smartFrame = null; try - { // 3. 转换与分发 + { + if (_framePool == null) + { + _sdkLog.Warning($"[SDK] Hik framePool 为空, 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}"); + return; + } + + // 3. 转换与分发 smartFrame = _framePool.Get(); if (smartFrame == null) return; // 池满丢帧 // Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离 - using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf)) + using (var rawYuvWrapper = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf)) { Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12); } @@ -647,7 +620,7 @@ public class HikVideoSource : BaseVideoSource, // ========================================================= if (smartFrame.InternalMat.Empty()) { - _sdkLog.Warning($"[SDK] Dahua 解码帧无效 (Empty Mat), 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}"); + _sdkLog.Warning($"[SDK] Hik 解码帧无效 (Empty Mat), 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}"); // finally 会负责 Dispose,这里直接返回 return; } @@ -669,7 +642,7 @@ public class HikVideoSource : BaseVideoSource, catch (Exception ex) { // 这里为了性能不频繁写日志,仅在调试时开启 - _sdkLog.Debug($"[SDK] Hik SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); + _sdkLog.Warning($"[SDK] Hik SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } finally diff --git a/SHH.CameraService/Bootstrapper.cs b/SHH.CameraService/Bootstrapper.cs index 75748fb..a612762 100644 --- a/SHH.CameraService/Bootstrapper.cs +++ b/SHH.CameraService/Bootstrapper.cs @@ -62,6 +62,7 @@ public static class Bootstrapper MaxRetentionDays = 10, FileSizeLimitBytes = 1024L * 1024 * 1024, + RollOnFileSizeLimit = true, }; LogBootstrapper.Init(ops);