From 2992306056d68f4b4a0c8c207951728bdd0b6178 Mon Sep 17 00:00:00 2001 From: wilson Date: Sat, 17 Jan 2026 15:41:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20SmartFrame=20=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E6=94=B6=E5=9B=9E=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/DynamicStreamOptions.cs | 14 +-- SHH.CameraSdk/Core/Memory/FramePool.cs | 55 ++++++++- SHH.CameraSdk/Core/Memory/SmartFrame.cs | 30 +++++ .../Core/Pipeline/GlobalPipelineRouter.cs | 5 + SHH.CameraSdk/Drivers/BaseVideoSource.cs | 39 +++++-- .../Drivers/HikVision/HikSdkManager.cs | 4 +- .../Drivers/HikVision/HikVideoSource.cs | 106 ++++++++++++++---- 7 files changed, 205 insertions(+), 48 deletions(-) diff --git a/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs b/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs index 155beec..fc23e4e 100644 --- a/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs +++ b/SHH.CameraSdk/Abstractions/Models/DynamicStreamOptions.cs @@ -45,18 +45,11 @@ public class DynamicStreamOptions #region --- 2. 频率控制 (Frame Rate Control) --- /// - /// 目标渲染/显示帧率(fps) - /// Nullable 规则:null = 不修改;0 = 跟随原始流速度;非 0 = 强制限定显示帧率 - /// 作用域:仅影响 UI 预览层,不会改变底层码流的采集帧率 - /// - public int? TargetDisplayFps { get; set; } - - /// - /// 目标 AI 分析帧率(fps) + /// 目标帧率(fps) /// Nullable 规则:null = 不修改;非 null = 限定算法处理的输入帧率 /// 性能优化:降低此值可显著减少高分辨率下的 GPU/CPU 负荷(如 4K 从 30fps 降到 5fps) /// - public int? TargetAnalyzeFps { get; set; } + public int? TargetFps { get; set; } #endregion @@ -109,8 +102,7 @@ public class DynamicStreamOptions TargetHeight is null && AllowEnlarge is null && AllowShrink is null && - TargetDisplayFps is null && - TargetAnalyzeFps is null && + TargetFps is null && EnableStreamOutput is null && RenderHandle is null && StreamType is null && diff --git a/SHH.CameraSdk/Core/Memory/FramePool.cs b/SHH.CameraSdk/Core/Memory/FramePool.cs index 646bd97..9a547d5 100644 --- a/SHH.CameraSdk/Core/Memory/FramePool.cs +++ b/SHH.CameraSdk/Core/Memory/FramePool.cs @@ -1,4 +1,6 @@ -using OpenCvSharp; +using Ayay.SerilogLogs; +using OpenCvSharp; +using Serilog; namespace SHH.CameraSdk; @@ -12,6 +14,8 @@ namespace SHH.CameraSdk; /// public class FramePool : IDisposable { + private ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.HikVisionSdk); + #region --- 私有资源与配置 (Private Resources & Configurations) --- /// 可用帧队列(线程安全):存储待借出的空闲智能帧 @@ -79,12 +83,13 @@ public class FramePool : IDisposable /// 从池借出一个智能帧(O(1) 时间复杂度) /// /// 可用智能帧 / 池空且达上限时返回 null(触发背压丢帧) - public SmartFrame Get() + public SmartFrame? Get() { // 1. 优先从可用队列取帧,无锁快速路径 if (_availableFrames.TryDequeue(out var frame)) { frame.Activate(); + frame.MarkBorrowed(); // 记录起始时间 return frame; } @@ -104,11 +109,57 @@ public class FramePool : IDisposable return Get(); } + // ============================================================ + // 3. [自愈触发] 如果走到这里,说明池子满了且所有帧都在外借中。 + // 可能存在“僵尸帧”死锁。执行强制回收哨兵。 + // ============================================================ + if (ForceRecycleZombies()) + { + // 如果哨兵成功救回了至少一帧,递归重试就能拿到帧 + return Get(); + } + // 3. 背压策略:池空且达上限,返回 null 强制丢帧,保证生产端不阻塞 // 适用场景:消费端处理过慢导致帧堆积,丢帧保实时性 return null; } + /// + /// 哨兵巡检:强制回收占用超过 5 秒不还的僵尸帧 + /// + private bool ForceRecycleZombies() + { + bool anyRescued = false; + long now = Environment.TickCount64; + + // Optimized: [原因] 使用 lock 或 ToArray() 防止在哨兵巡检期间, + // 其他线程触发 CreateNewFrame 导致 List 集合修改异常。 + SmartFrame[] snapshot; + lock (_lock) { snapshot = _allAllocatedFrames.ToArray(); } + + // 遍历所有已分配的帧,找出超时的僵尸 + foreach (var frame in snapshot) + { + // 条件:当前不在池中(_refCount > 0)且借出时间超过 2000ms + // 注意:这里需要给 SmartFrame 暴露一个只读的 RefCount 属性,或者直接判断 BorrowedTick + if ((now - frame.BorrowedTick) > 2000) + { + // 强行重置该帧的所有权 + frame.ForceReset(); + + // 重新塞回队列 + _availableFrames.Enqueue(frame); + + anyRescued = true; + + // 记录一条警告日志,告诉你哪路视频出问题了 + // 可以在 AddAuditLog 里看到 + _sdkLog.Warning("[Sdk] SmartFrame(借出超2秒) 被强制回收."); + } + } + return anyRescued; + } + /// /// [系统内部调用] 将帧归还至池(由 SmartFrame.Dispose 自动触发) /// diff --git a/SHH.CameraSdk/Core/Memory/SmartFrame.cs b/SHH.CameraSdk/Core/Memory/SmartFrame.cs index e7e0955..3310028 100644 --- a/SHH.CameraSdk/Core/Memory/SmartFrame.cs +++ b/SHH.CameraSdk/Core/Memory/SmartFrame.cs @@ -181,4 +181,34 @@ public class SmartFrame : IDisposable } #endregion + + // 在 SmartFrame 类中添加此方法 + /// + /// [哨兵专用] 强制重置帧状态 + /// 用于自愈机制:当引用计数由于逻辑 Bug 永久无法归零时,由 FramePool 强行回收 + /// + internal void ForceReset() + { + // 1. 强行将计数器和归还标记归零 + Interlocked.Exchange(ref _refCount, 0); + Interlocked.Exchange(ref _isReturned, 1); + + // 2. 清理衍生数据,防止内存堆积 + ResetDerivatives(); + + // 3. 标记已被强制回收,便于日志追踪 + IsForceRecycled = true; + } + + // 记录帧被从池中取出的精确时间 + public long BorrowedTick { get; private set; } + + // 标记是否已被哨兵强制回收 + internal bool IsForceRecycled { get; set; } + + public void MarkBorrowed() + { + BorrowedTick = Environment.TickCount64; + IsForceRecycled = false; + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Pipeline/GlobalPipelineRouter.cs b/SHH.CameraSdk/Core/Pipeline/GlobalPipelineRouter.cs index 5d44bfe..7e8122f 100644 --- a/SHH.CameraSdk/Core/Pipeline/GlobalPipelineRouter.cs +++ b/SHH.CameraSdk/Core/Pipeline/GlobalPipelineRouter.cs @@ -22,6 +22,11 @@ public static class GlobalPipelineRouter /// public static void Enqueue(long deviceId, SmartFrame frame, FrameDecision decision) { + // Optimized: [原因] 撤回冗余的 AddRef。实测发现 ProcessingPipeline.TrySubmit + // 内部已包含 AddRef 逻辑,此处若再加会导致引用计数无法归零,进而撑爆帧池导致卡死。 + + frame.AddRef(); + if (_currentProcessor != null) { // 场景 A: 有处理器 (如缩放服务) -> 改道进入处理器 diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index 8ff6137..ae7de75 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -257,6 +257,10 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC /// public async Task StartAsync() { + // Optimized: [原因] 增加销毁前置检查,配合 Bug 修复方案 + if (_isDisposed || _lifecycleLock == null) + throw new ObjectDisposedException(nameof(BaseVideoSource), "设备实例已销毁"); + // 死锁免疫:不捕获当前同步上下文 await _lifecycleLock.WaitAsync().ConfigureAwait(false); try @@ -443,7 +447,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC var changeLog = new List(); if (options.StreamType.HasValue) changeLog.Add($"码流类型={options.StreamType}"); if (options.RenderHandle.HasValue) changeLog.Add($"渲染句柄已更新"); - if (options.TargetAnalyzeFps.HasValue) changeLog.Add($"分析帧率={options.TargetAnalyzeFps}fps"); + if (options.TargetFps.HasValue) changeLog.Add($"分析帧率={options.TargetFps}fps"); AddAuditLog($"动态参数应用: {string.Join(" | ", changeLog)}"); } @@ -533,13 +537,13 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC // --- B. 结算网络带宽 (Mbps) --- // 公式: (字节数 * 8位) / 1024 / 1024 / 秒数 long bytes = Interlocked.Exchange(ref _tempByteCounter, 0); - _currentBitrate = Math.Round((bytes * 8.0) / 1024 / 1024 / duration, 2); + _currentBitrate = Math.Round((bytes * 8.0) / 1048576.0 / duration, 2); } else { - // 初始化重置 - _tempFrameCounter = 0; - _tempByteCounter = 0; + // 初始化重置:确保原子性 + Interlocked.Exchange(ref _tempFrameCounter, 0); + Interlocked.Exchange(ref _tempByteCounter, 0); } // 更新结算时间锚点 @@ -635,6 +639,14 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC { _sdkLog.Fatal(ex, "设备 {Id} 状态分发器致命异常", Id); } + finally + { + // Optimized: [原因] 确保在分发器关闭后,缓冲区内残余的状态消息能被强制消费完。 + while (_statusQueue.Reader.TryRead(out var args)) + { + try { StatusChanged?.Invoke(this, args); } catch { /* 忽略销毁期的回调异常 */ } + } + } } /// @@ -744,6 +756,10 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC // 防止重复 Dispose if (_isDisposed) return; + // 提前锁定并获取引用 + var semaphore = _lifecycleLock; + if (semaphore == null) return; + // Optimized: [原因] 获取生命周期锁,防止在 DisposeAsync 执行期间被并发触发 Start/Stop 操作 await _lifecycleLock.WaitAsync().ConfigureAwait(false); @@ -751,11 +767,12 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC { // 防止重复 Dispose if (_isDisposed) return; - _isDisposed = true; // 1. 停止业务逻辑 await StopAsync().ConfigureAwait(false); + _isDisposed = true; + // 2. 优雅关闭状态分发器 _statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息 _distributorCts?.Cancel(); // 触发分发器取消 @@ -776,9 +793,13 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC } finally { - // Modified: [原因] 保证计数锁在任何情况下都能释放 - if (!_isDisposed) - _lifecycleLock.Release(); + // Optimized: [原因] 修复信号量销毁顺序。 + // 先释放锁,让可能存在的阻塞线程(虽然被 _isDisposed 阻断)能正常通过, + // 然后检查是否为销毁流程的最后一步。 + semaphore.Release(); + + // 彻底销毁信号量并清空引用,确保后续调用不再访问已释放的对象 + _lifecycleLock?.Dispose(); // 6. 抑制垃圾回收器的终结器 GC.SuppressFinalize(this); diff --git a/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs b/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs index f093812..27167e6 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikSdkManager.cs @@ -85,8 +85,8 @@ public static class HikSdkManager // 引用计数归 0 时执行物理卸载,关闭 SDK 所有隐形线程与资源 if (_referenceCount == 0) { - // [物理卸载] 释放 SDK 占用的非托管资源(如网络连接、内存缓冲区) - HikNativeMethods.NET_DVR_Cleanup(); + //// [物理卸载] 释放 SDK 占用的非托管资源(如网络连接、内存缓冲区) + //HikNativeMethods.NET_DVR_Cleanup(); } } } diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 7ff3b1a..e08633d 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -259,30 +259,33 @@ public class HikVideoSource : BaseVideoSource, // 2. 停止解码 if (_playPort >= 0) { - try - { - HikPlayMethods.PlayM4_Stop(_playPort); - HikPlayMethods.PlayM4_CloseStream(_playPort); - } - catch (Exception ex) - { - _sdkLog.Debug($"[SDK] Hik 停止解码失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); - AddAuditLog($"[SDK] Hik 停止解码失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); - } - finally + lock(_globalPortLock) { try { - HikPlayMethods.PlayM4_FreePort(_playPort); + HikPlayMethods.PlayM4_Stop(_playPort); + HikPlayMethods.PlayM4_CloseStream(_playPort); } catch (Exception ex) { - _sdkLog.Warning($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); - AddAuditLog($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); + _sdkLog.Debug($"[SDK] Hik 停止解码失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); + AddAuditLog($"[SDK] Hik 停止解码失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } + finally + { + try + { + HikPlayMethods.PlayM4_FreePort(_playPort); + } + catch (Exception ex) + { + _sdkLog.Warning($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); + AddAuditLog($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); + } + } + + _playPort = -1; } - - _playPort = -1; } lock (_bufferLock) @@ -443,13 +446,15 @@ public class HikVideoSource : BaseVideoSource, // 因为 dwBufSize > 0,MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数 MarkFrameReceived(dwBufSize); + // Optimized: [原因] 增加前置失效判定,若当前句柄已释放则不再处理后续流数据 if (_realPlayHandle == -1) return; // 处理系统头 - if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1) + if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD) { lock (_initLock) { + // 原子检查:若已存在端口、预览句柄已失效或对象已销毁,则立即拦截 if (_realPlayHandle == -1 || _playPort != -1) return; bool getPortSuccess; @@ -460,19 +465,26 @@ public class HikVideoSource : BaseVideoSource, if (!getPortSuccess) return; + // 配置播放库参数 HikPlayMethods.PlayM4_SetDisplayBuf(_playPort, 1); // 极速模式 HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); if (!HikPlayMethods.PlayM4_OpenStream(_playPort, pBuffer, dwBufSize, 2 * 1024 * 1024)) { - HikPlayMethods.PlayM4_FreePort(_playPort); - _playPort = -1; + // 开启失败需在锁内立即释放端口,防止句柄残留 + lock (_globalPortLock) + { + HikPlayMethods.PlayM4_FreePort(_playPort); + _playPort = -1; + } return; } _decCallBack = new HikPlayMethods.DECCBFUN(SafeOnDecodingCallBack); HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0); HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero); + + _sdkLog.Debug($"[SDK] Hik 播放端口初始化成功, ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放端口:{_playPort}"); } } // 处理流数据 @@ -510,12 +522,56 @@ public class HikVideoSource : BaseVideoSource, // [优化] 维持心跳,防止被哨兵误杀 MarkFrameReceived(0); - // [新增] 捕获并更新分辨率 - // 只有当分辨率发生变化时才写入,减少属性赋值开销 - if (Width != pFrameInfo.nWidth || Height != pFrameInfo.nHeight) + int currentWidth = pFrameInfo.nWidth; + int currentHeight = pFrameInfo.nHeight; + + // 3. [核心修复点] 分辨率动态监测与帧池热重建 + // Modified: [原因] 修复 Bug E:当 SDK 输出的分辨率与当前帧池尺寸不符时,必须阻塞并重建资源。 + // 严禁在分辨率不匹配的情况下调用 OpenCV 转换函数,防止非托管内存越界写入。 + if (!_isPoolReady || Width != currentWidth || Height != currentHeight) { - Width = pFrameInfo.nWidth; - Height = pFrameInfo.nHeight; + bool lockTaken = false; + try + { + // 尝试获取初始化锁,超时 50ms(分辨率变更属于低频关键动作,允许稍长等待) + Monitor.TryEnter(_initLock, 50, ref lockTaken); + + if (lockTaken) + { + // Double Check:防止多个解码回调并发重建 + if (Width != currentWidth || Height != currentHeight || !_isPoolReady) + { + _sdkLog.Warning($"[SDK] 监测到分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight},正在重建帧池..."); + + // 销毁旧池(内部会释放所有 Mat 资源) + _framePool?.Dispose(); + + // 更新基类维护的分辨率属性 + Width = currentWidth; + Height = currentHeight; + + // 重建帧池:initialSize 设为 3 保证高并发缓冲,maxSize 设为 5 严格控制内存总额 + _framePool = new FramePool(Width, Height, MatType.CV_8UC3, initialSize: 3, maxSize: 5); + _isPoolReady = true; + + AddAuditLog($"分辨率热重载完成: {Width}x{Height}"); + } + } + else + { + // 拿不到锁说明主线程正在 Stop 或切换配置,直接丢弃该帧防止死锁 + return; + } + } + catch (Exception ex) + { + _sdkLog.Error(ex, "帧池重建失败"); + return; + } + finally + { + if (lockTaken) Monitor.Exit(_initLock); + } } // 1. [核心流控] 询问基类控制器:这帧要不要? @@ -526,6 +582,8 @@ public class HikVideoSource : BaseVideoSource, // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return; + //Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵处理."); + int width = pFrameInfo.nWidth; int height = pFrameInfo.nHeight;