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;