增加 SmartFrame 强制收回逻辑

This commit is contained in:
2026-01-17 15:41:55 +08:00
parent e06c60968d
commit 2992306056
7 changed files with 205 additions and 48 deletions

View File

@@ -45,18 +45,11 @@ public class DynamicStreamOptions
#region --- 2. (Frame Rate Control) --- #region --- 2. (Frame Rate Control) ---
/// <summary> /// <summary>
/// 目标渲染/显示帧率fps /// 目标帧率fps
/// <para>Nullable 规则null = 不修改0 = 跟随原始流速度;非 0 = 强制限定显示帧率</para>
/// <para>作用域:仅影响 UI 预览层,不会改变底层码流的采集帧率</para>
/// </summary>
public int? TargetDisplayFps { get; set; }
/// <summary>
/// 目标 AI 分析帧率fps
/// <para>Nullable 规则null = 不修改;非 null = 限定算法处理的输入帧率</para> /// <para>Nullable 规则null = 不修改;非 null = 限定算法处理的输入帧率</para>
/// <para>性能优化:降低此值可显著减少高分辨率下的 GPU/CPU 负荷(如 4K 从 30fps 降到 5fps</para> /// <para>性能优化:降低此值可显著减少高分辨率下的 GPU/CPU 负荷(如 4K 从 30fps 降到 5fps</para>
/// </summary> /// </summary>
public int? TargetAnalyzeFps { get; set; } public int? TargetFps { get; set; }
#endregion #endregion
@@ -109,8 +102,7 @@ public class DynamicStreamOptions
TargetHeight is null && TargetHeight is null &&
AllowEnlarge is null && AllowEnlarge is null &&
AllowShrink is null && AllowShrink is null &&
TargetDisplayFps is null && TargetFps is null &&
TargetAnalyzeFps is null &&
EnableStreamOutput is null && EnableStreamOutput is null &&
RenderHandle is null && RenderHandle is null &&
StreamType is null && StreamType is null &&

View File

@@ -1,4 +1,6 @@
using OpenCvSharp; using Ayay.SerilogLogs;
using OpenCvSharp;
using Serilog;
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
@@ -12,6 +14,8 @@ namespace SHH.CameraSdk;
/// </summary> /// </summary>
public class FramePool : IDisposable public class FramePool : IDisposable
{ {
private ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.HikVisionSdk);
#region --- (Private Resources & Configurations) --- #region --- (Private Resources & Configurations) ---
/// <summary> 可用帧队列(线程安全):存储待借出的空闲智能帧 </summary> /// <summary> 可用帧队列(线程安全):存储待借出的空闲智能帧 </summary>
@@ -79,12 +83,13 @@ public class FramePool : IDisposable
/// 从池借出一个智能帧O(1) 时间复杂度) /// 从池借出一个智能帧O(1) 时间复杂度)
/// </summary> /// </summary>
/// <returns>可用智能帧 / 池空且达上限时返回 null触发背压丢帧</returns> /// <returns>可用智能帧 / 池空且达上限时返回 null触发背压丢帧</returns>
public SmartFrame Get() public SmartFrame? Get()
{ {
// 1. 优先从可用队列取帧,无锁快速路径 // 1. 优先从可用队列取帧,无锁快速路径
if (_availableFrames.TryDequeue(out var frame)) if (_availableFrames.TryDequeue(out var frame))
{ {
frame.Activate(); frame.Activate();
frame.MarkBorrowed(); // 记录起始时间
return frame; return frame;
} }
@@ -104,11 +109,57 @@ public class FramePool : IDisposable
return Get(); return Get();
} }
// ============================================================
// 3. [自愈触发] 如果走到这里,说明池子满了且所有帧都在外借中。
// 可能存在“僵尸帧”死锁。执行强制回收哨兵。
// ============================================================
if (ForceRecycleZombies())
{
// 如果哨兵成功救回了至少一帧,递归重试就能拿到帧
return Get();
}
// 3. 背压策略:池空且达上限,返回 null 强制丢帧,保证生产端不阻塞 // 3. 背压策略:池空且达上限,返回 null 强制丢帧,保证生产端不阻塞
// 适用场景:消费端处理过慢导致帧堆积,丢帧保实时性 // 适用场景:消费端处理过慢导致帧堆积,丢帧保实时性
return null; return null;
} }
/// <summary>
/// 哨兵巡检:强制回收占用超过 5 秒不还的僵尸帧
/// </summary>
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;
}
/// <summary> /// <summary>
/// [系统内部调用] 将帧归还至池(由 SmartFrame.Dispose 自动触发) /// [系统内部调用] 将帧归还至池(由 SmartFrame.Dispose 自动触发)
/// </summary> /// </summary>

View File

@@ -181,4 +181,34 @@ public class SmartFrame : IDisposable
} }
#endregion #endregion
// 在 SmartFrame 类中添加此方法
/// <summary>
/// [哨兵专用] 强制重置帧状态
/// 用于自愈机制:当引用计数由于逻辑 Bug 永久无法归零时,由 FramePool 强行回收
/// </summary>
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;
}
} }

View File

@@ -22,6 +22,11 @@ public static class GlobalPipelineRouter
/// </summary> /// </summary>
public static void Enqueue(long deviceId, SmartFrame frame, FrameDecision decision) public static void Enqueue(long deviceId, SmartFrame frame, FrameDecision decision)
{ {
// Optimized: [原因] 撤回冗余的 AddRef。实测发现 ProcessingPipeline.TrySubmit
// 内部已包含 AddRef 逻辑,此处若再加会导致引用计数无法归零,进而撑爆帧池导致卡死。
frame.AddRef();
if (_currentProcessor != null) if (_currentProcessor != null)
{ {
// 场景 A: 有处理器 (如缩放服务) -> 改道进入处理器 // 场景 A: 有处理器 (如缩放服务) -> 改道进入处理器

View File

@@ -257,6 +257,10 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
/// </summary> /// </summary>
public async Task StartAsync() public async Task StartAsync()
{ {
// Optimized: [原因] 增加销毁前置检查,配合 Bug 修复方案
if (_isDisposed || _lifecycleLock == null)
throw new ObjectDisposedException(nameof(BaseVideoSource), "设备实例已销毁");
// 死锁免疫:不捕获当前同步上下文 // 死锁免疫:不捕获当前同步上下文
await _lifecycleLock.WaitAsync().ConfigureAwait(false); await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try try
@@ -443,7 +447,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
var changeLog = new List<string>(); var changeLog = new List<string>();
if (options.StreamType.HasValue) changeLog.Add($"码流类型={options.StreamType}"); if (options.StreamType.HasValue) changeLog.Add($"码流类型={options.StreamType}");
if (options.RenderHandle.HasValue) changeLog.Add($"渲染句柄已更新"); 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)}"); AddAuditLog($"动态参数应用: {string.Join(" | ", changeLog)}");
} }
@@ -533,13 +537,13 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
// --- B. 结算网络带宽 (Mbps) --- // --- B. 结算网络带宽 (Mbps) ---
// 公式: (字节数 * 8位) / 1024 / 1024 / 秒数 // 公式: (字节数 * 8位) / 1024 / 1024 / 秒数
long bytes = Interlocked.Exchange(ref _tempByteCounter, 0); 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 else
{ {
// 初始化重置 // 初始化重置:确保原子性
_tempFrameCounter = 0; Interlocked.Exchange(ref _tempFrameCounter, 0);
_tempByteCounter = 0; Interlocked.Exchange(ref _tempByteCounter, 0);
} }
// 更新结算时间锚点 // 更新结算时间锚点
@@ -635,6 +639,14 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
{ {
_sdkLog.Fatal(ex, "设备 {Id} 状态分发器致命异常", Id); _sdkLog.Fatal(ex, "设备 {Id} 状态分发器致命异常", Id);
} }
finally
{
// Optimized: [原因] 确保在分发器关闭后,缓冲区内残余的状态消息能被强制消费完。
while (_statusQueue.Reader.TryRead(out var args))
{
try { StatusChanged?.Invoke(this, args); } catch { /* 忽略销毁期的回调异常 */ }
}
}
} }
/// <summary> /// <summary>
@@ -744,6 +756,10 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
// 防止重复 Dispose // 防止重复 Dispose
if (_isDisposed) return; if (_isDisposed) return;
// 提前锁定并获取引用
var semaphore = _lifecycleLock;
if (semaphore == null) return;
// Optimized: [原因] 获取生命周期锁,防止在 DisposeAsync 执行期间被并发触发 Start/Stop 操作 // Optimized: [原因] 获取生命周期锁,防止在 DisposeAsync 执行期间被并发触发 Start/Stop 操作
await _lifecycleLock.WaitAsync().ConfigureAwait(false); await _lifecycleLock.WaitAsync().ConfigureAwait(false);
@@ -751,11 +767,12 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
{ {
// 防止重复 Dispose // 防止重复 Dispose
if (_isDisposed) return; if (_isDisposed) return;
_isDisposed = true;
// 1. 停止业务逻辑 // 1. 停止业务逻辑
await StopAsync().ConfigureAwait(false); await StopAsync().ConfigureAwait(false);
_isDisposed = true;
// 2. 优雅关闭状态分发器 // 2. 优雅关闭状态分发器
_statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息 _statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息
_distributorCts?.Cancel(); // 触发分发器取消 _distributorCts?.Cancel(); // 触发分发器取消
@@ -776,9 +793,13 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
} }
finally finally
{ {
// Modified: [原因] 保证计数锁在任何情况下都能释放 // Optimized: [原因] 修复信号量销毁顺序。
if (!_isDisposed) // 先释放锁,让可能存在的阻塞线程(虽然被 _isDisposed 阻断)能正常通过,
_lifecycleLock.Release(); // 然后检查是否为销毁流程的最后一步。
semaphore.Release();
// 彻底销毁信号量并清空引用,确保后续调用不再访问已释放的对象
_lifecycleLock?.Dispose();
// 6. 抑制垃圾回收器的终结器 // 6. 抑制垃圾回收器的终结器
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -85,8 +85,8 @@ public static class HikSdkManager
// 引用计数归 0 时执行物理卸载,关闭 SDK 所有隐形线程与资源 // 引用计数归 0 时执行物理卸载,关闭 SDK 所有隐形线程与资源
if (_referenceCount == 0) if (_referenceCount == 0)
{ {
// [物理卸载] 释放 SDK 占用的非托管资源(如网络连接、内存缓冲区) //// [物理卸载] 释放 SDK 占用的非托管资源(如网络连接、内存缓冲区)
HikNativeMethods.NET_DVR_Cleanup(); //HikNativeMethods.NET_DVR_Cleanup();
} }
} }
} }

View File

@@ -259,30 +259,33 @@ public class HikVideoSource : BaseVideoSource,
// 2. 停止解码 // 2. 停止解码
if (_playPort >= 0) if (_playPort >= 0)
{ {
try lock(_globalPortLock)
{
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
{ {
try try
{ {
HikPlayMethods.PlayM4_FreePort(_playPort); HikPlayMethods.PlayM4_Stop(_playPort);
HikPlayMethods.PlayM4_CloseStream(_playPort);
} }
catch (Exception ex) catch (Exception ex)
{ {
_sdkLog.Warning($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", 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}"); 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) lock (_bufferLock)
@@ -443,13 +446,15 @@ public class HikVideoSource : BaseVideoSource,
// 因为 dwBufSize > 0MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数 // 因为 dwBufSize > 0MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数
MarkFrameReceived(dwBufSize); MarkFrameReceived(dwBufSize);
// Optimized: [原因] 增加前置失效判定,若当前句柄已释放则不再处理后续流数据
if (_realPlayHandle == -1) return; if (_realPlayHandle == -1) return;
// 处理系统头 // 处理系统头
if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1) if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD)
{ {
lock (_initLock) lock (_initLock)
{ {
// 原子检查:若已存在端口、预览句柄已失效或对象已销毁,则立即拦截
if (_realPlayHandle == -1 || _playPort != -1) return; if (_realPlayHandle == -1 || _playPort != -1) return;
bool getPortSuccess; bool getPortSuccess;
@@ -460,19 +465,26 @@ public class HikVideoSource : BaseVideoSource,
if (!getPortSuccess) return; if (!getPortSuccess) return;
// 配置播放库参数
HikPlayMethods.PlayM4_SetDisplayBuf(_playPort, 1); // 极速模式 HikPlayMethods.PlayM4_SetDisplayBuf(_playPort, 1); // 极速模式
HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0);
if (!HikPlayMethods.PlayM4_OpenStream(_playPort, pBuffer, dwBufSize, 2 * 1024 * 1024)) 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; return;
} }
_decCallBack = new HikPlayMethods.DECCBFUN(SafeOnDecodingCallBack); _decCallBack = new HikPlayMethods.DECCBFUN(SafeOnDecodingCallBack);
HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0); HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0);
HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero); 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); MarkFrameReceived(0);
// [新增] 捕获并更新分辨率 int currentWidth = pFrameInfo.nWidth;
// 只有当分辨率发生变化时才写入,减少属性赋值开销 int currentHeight = pFrameInfo.nHeight;
if (Width != pFrameInfo.nWidth || Height != pFrameInfo.nHeight)
// 3. [核心修复点] 分辨率动态监测与帧池热重建
// Modified: [原因] 修复 Bug E当 SDK 输出的分辨率与当前帧池尺寸不符时,必须阻塞并重建资源。
// 严禁在分辨率不匹配的情况下调用 OpenCV 转换函数,防止非托管内存越界写入。
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{ {
Width = pFrameInfo.nWidth; bool lockTaken = false;
Height = pFrameInfo.nHeight; 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. [核心流控] 询问基类控制器:这帧要不要? // 1. [核心流控] 询问基类控制器:这帧要不要?
@@ -526,6 +582,8 @@ public class HikVideoSource : BaseVideoSource,
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
if (!decision.IsCaptured) return; if (!decision.IsCaptured) return;
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵处理.");
int width = pFrameInfo.nWidth; int width = pFrameInfo.nWidth;
int height = pFrameInfo.nHeight; int height = pFrameInfo.nHeight;