增加 SmartFrame 强制收回逻辑
This commit is contained in:
@@ -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 &&
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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: 有处理器 (如缩放服务) -> 改道进入处理器
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 > 0,MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数
|
// 因为 dwBufSize > 0,MarkFrameReceived 内部只会累加码流,不会增加 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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user