SDK核心 Bug 修复
This commit is contained in:
@@ -189,78 +189,137 @@ public class DahuaVideoSource : BaseVideoSource
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HandleProcessCorruptedStateExceptions]
|
/// <summary>
|
||||||
|
/// 解码回调:YUV -> SmartFrame -> Pipeline
|
||||||
|
/// <para>已集成:平滑重建、引用交换、Finally释放、[诊断陷阱]、[空帧防御]</para>
|
||||||
|
/// </summary>
|
||||||
|
[HandleProcessCorruptedStateExceptions] // 捕获非托管状态损坏异常 (AccessViolation)
|
||||||
[SecurityCritical]
|
[SecurityCritical]
|
||||||
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref DahuaPlaySDK.FRAME_INFO pFrameInfo, IntPtr nUser, int nReserved2)
|
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;
|
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 currentWidth = pFrameInfo.nWidth;
|
||||||
int currentHeight = pFrameInfo.nHeight;
|
int currentHeight = pFrameInfo.nHeight;
|
||||||
|
|
||||||
// 帧池平滑重建逻辑 (保持与海康版一致,防止死锁)
|
// =========================================================================================
|
||||||
|
// 🛡️ [第二道防线:防死锁重建] 帧池平滑重建逻辑
|
||||||
|
// 场景:分辨率变化(主辅码流切换/ROI裁剪)。
|
||||||
|
// 作用:使用 TryEnter 替代 lock,防止主线程 Stop 时此处死等导致死锁。
|
||||||
|
// =========================================================================================
|
||||||
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
|
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
|
||||||
{
|
{
|
||||||
bool lockTaken = false;
|
bool lockTaken = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 等待 50ms,拿不到锁就放弃这一帧
|
||||||
Monitor.TryEnter(_initLock, 50, ref lockTaken);
|
Monitor.TryEnter(_initLock, 50, ref lockTaken);
|
||||||
|
|
||||||
if (lockTaken)
|
if (lockTaken)
|
||||||
{
|
{
|
||||||
|
// Double Check
|
||||||
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
|
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
|
||||||
{
|
{
|
||||||
_framePool?.Dispose();
|
_sdkLog.Information($"[Res] 大华分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight}");
|
||||||
var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5);
|
|
||||||
_framePool = newPool;
|
_framePool?.Dispose(); // 销毁旧池
|
||||||
|
|
||||||
|
// 更新基类维护的分辨率属性
|
||||||
Width = currentWidth;
|
Width = currentWidth;
|
||||||
Height = currentHeight;
|
Height = currentHeight;
|
||||||
|
|
||||||
|
// 重建帧池:initialSize 设为 3 保证高并发缓冲,maxSize 设为 5 严格控制内存总额
|
||||||
|
var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5);
|
||||||
|
_framePool = newPool;
|
||||||
|
|
||||||
_isPoolReady = true;
|
_isPoolReady = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else return;
|
else return; // 竞争失败(可能正在停止),直接丢弃,防止卡死
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_sdkLog.Error(ex, "[SDK] 大华帧池重建失败");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
finally { if (lockTaken) Monitor.Exit(_initLock); }
|
finally { if (lockTaken) Monitor.Exit(_initLock); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. [核心流控] 询问基类控制器:这帧要不要?
|
||||||
|
// 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。
|
||||||
|
// 传入真实的输入帧率作为参考基准
|
||||||
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
|
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
|
||||||
|
|
||||||
|
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
|
||||||
if (!decision.IsCaptured) return;
|
if (!decision.IsCaptured) return;
|
||||||
|
|
||||||
SmartFrame? smartFrame = null;
|
SmartFrame? smartFrame = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
smartFrame = _framePool?.Get();
|
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))
|
using (var yuvMat = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
|
||||||
{
|
{
|
||||||
Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420);
|
Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================================================
|
||||||
// 【新增防御】: 检查转换结果是否有效
|
// 🛡️ [第三道防线:空结果防御]
|
||||||
// 如果转换失败,或者 Mat 为空,绝对不能传给 Router
|
// 场景:虽然入参合法,但 OpenCV 转换可能失败,或者 Mat 内部状态异常。
|
||||||
// =========================================================
|
// 作用:绝对禁止将 Empty Mat 传给 Worker,否则 Worker 里的 Resize 会 100% 崩溃。
|
||||||
|
// =========================================================================================
|
||||||
if (smartFrame.InternalMat.Empty())
|
if (smartFrame.InternalMat.Empty())
|
||||||
{
|
{
|
||||||
_sdkLog.Warning($"[SDK] Dahua 解码帧无效 (Empty Mat), 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}");
|
_sdkLog.Warning($"[SDK] Dahua 解码后 Mat 为空 (转换失败), 丢弃. ID:{Config.Id}");
|
||||||
// finally 会负责 Dispose,这里直接返回
|
return; // finally 会负责 Dispose,这里直接安全退出
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 填充订阅者
|
||||||
foreach (var appId in decision.TargetAppIds)
|
foreach (var appId in decision.TargetAppIds)
|
||||||
smartFrame.SubscriberIds.Enqueue(appId);
|
smartFrame.SubscriberIds.Enqueue(appId);
|
||||||
|
|
||||||
|
// 发送到全局路由 (注意:Router 内部必须 AddRef)
|
||||||
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
|
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
// =========================================================================================
|
||||||
|
// ♻️ [引用闭环]
|
||||||
|
// 驱动层完成了它的任务,必须释放它持有的那次引用 (Count 1 -> 0, 或 2 -> 1)。
|
||||||
|
// 只有这样,当 Worker 处理完 Dispose 后,帧才能真正回到池子里。
|
||||||
|
// =========================================================================================
|
||||||
smartFrame?.Dispose();
|
smartFrame?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,9 +515,18 @@ public class HikVideoSource : BaseVideoSource,
|
|||||||
{
|
{
|
||||||
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达.");
|
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达.");
|
||||||
|
|
||||||
// Optimized: [原因] 增加前置防御性检查,若回调入参异常立即退出,防止后续 OpenCV 封装崩溃
|
// 1. 基础指针检查
|
||||||
if (pBuf == IntPtr.Zero || nSize <= 0
|
if (pBuf == IntPtr.Zero || nSize <= 0) return;
|
||||||
|| pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0) return;
|
|
||||||
|
// 2. 视频有效性检查
|
||||||
|
// 如果不是音频,但宽高依然为 0,说明是异常数据或非图像私有头
|
||||||
|
if (pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0)
|
||||||
|
{
|
||||||
|
// 如果你想调试音频,可以在这里处理 pBuf
|
||||||
|
// 但对于视频分析业务,这里直接 return,不要写 Log,否则磁盘会爆
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// [优化] 维持心跳,防止被哨兵误杀
|
// [优化] 维持心跳,防止被哨兵误杀
|
||||||
MarkFrameReceived(0);
|
MarkFrameReceived(0);
|
||||||
@@ -584,59 +593,23 @@ public class HikVideoSource : BaseVideoSource,
|
|||||||
|
|
||||||
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵处理.");
|
//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 归还逻辑
|
// Optimized: [原因] 将 smartFrame 定义在 try 外部,确保 finally 块能够可靠执行 Dispose 归还逻辑
|
||||||
SmartFrame? smartFrame = null;
|
SmartFrame? smartFrame = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{ // 3. 转换与分发
|
{
|
||||||
|
if (_framePool == null)
|
||||||
|
{
|
||||||
|
_sdkLog.Warning($"[SDK] Hik framePool 为空, 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换与分发
|
||||||
smartFrame = _framePool.Get();
|
smartFrame = _framePool.Get();
|
||||||
if (smartFrame == null) return; // 池满丢帧
|
if (smartFrame == null) return; // 池满丢帧
|
||||||
|
|
||||||
// Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离
|
// 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);
|
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
|
||||||
}
|
}
|
||||||
@@ -647,7 +620,7 @@ public class HikVideoSource : BaseVideoSource,
|
|||||||
// =========================================================
|
// =========================================================
|
||||||
if (smartFrame.InternalMat.Empty())
|
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,这里直接返回
|
// finally 会负责 Dispose,这里直接返回
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -669,7 +642,7 @@ public class HikVideoSource : BaseVideoSource,
|
|||||||
catch (Exception ex)
|
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}");
|
AddAuditLog($"[SDK] Hik SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public static class Bootstrapper
|
|||||||
|
|
||||||
MaxRetentionDays = 10,
|
MaxRetentionDays = 10,
|
||||||
FileSizeLimitBytes = 1024L * 1024 * 1024,
|
FileSizeLimitBytes = 1024L * 1024 * 1024,
|
||||||
|
RollOnFileSizeLimit = true,
|
||||||
};
|
};
|
||||||
LogBootstrapper.Init(ops);
|
LogBootstrapper.Init(ops);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user