Files
Ayay/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs

347 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Ayay.SerilogLogs;
using OpenCvSharp;
using Serilog;
using System.Runtime.ExceptionServices;
using System.Security;
namespace SHH.CameraSdk;
/// <summary>
/// [大华驱动] 工业级视频源实现 (依照官方 Demo 逻辑重构版)
/// <para>当前模块: AiVideo | 核心原则: 低耦合、高并发、零拷贝</para>
/// </summary>
public class DahuaVideoSource : BaseVideoSource
{
protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.DaHuaSdk);
#region --- 1. (Static Resources) ---
private static readonly ConcurrentDictionary<IntPtr, DahuaVideoSource> _instances = new();
// 必须保持静态引用,防止被 GC 回收导致回调崩溃
private static fDisConnectCallBack m_DisConnectCallBack;
private static fHaveReConnectCallBack m_ReConnectCallBack;
private static fRealDataCallBackEx2 m_RealDataCallBackEx2;
#endregion
#region --- 2. (Instance Members) ---
private IntPtr _loginId = IntPtr.Zero;
private IntPtr _realPlayId = IntPtr.Zero;
private int _playPort = -1;
private FramePool? _framePool;
private volatile bool _isPoolReady = false;
private readonly object _initLock = new();
// 强引用实例的回调委托
private DahuaPlaySDK.DECCBFUN? _decCallBack;
#endregion
public DahuaVideoSource(VideoSourceConfig config) : base(config) { }
#region --- 3. (Lifecycle Overrides) ---
protected override async Task OnStartAsync(CancellationToken token)
{
await Task.Run(() =>
{
// 1. 初始化 SDK (只需一次)
InitSdkGlobal();
_sdkLog.Information($"[SDK] Dahua 正在执行登录 => ID:{_config.Id} IP:{_config.IpAddress}");
AddAuditLog($"[SDK] Dahua 正在执行登录 => IP:{_config.IpAddress}");
// 2. 登录设备
NET_DEVICEINFO_Ex deviceInfo = new NET_DEVICEINFO_Ex();
_loginId = NETClient.LoginWithHighLevelSecurity(_config.IpAddress, (ushort)_config.Port, _config.Username, _config.Password, EM_LOGIN_SPAC_CAP_TYPE.TCP, IntPtr.Zero, ref deviceInfo);
if (_loginId == IntPtr.Zero)
{
string err = NETClient.GetLastError();
throw new Exception($"大华登录失败: {err}");
}
_instances.TryAdd(_loginId, this);
_sdkLog.Information($"[SDK] Dahua 登录成功 => LoginID:{_loginId}, 通道数:{deviceInfo.nChanNum}");
// 3. 开启实时预览 (参考 Demo Button1_Click 逻辑)
// Modified: [原因] 使用 RealPlayByDataType 以支持获取原始码流并指定回调
NET_IN_REALPLAY_BY_DATA_TYPE stuIn = new NET_IN_REALPLAY_BY_DATA_TYPE()
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_IN_REALPLAY_BY_DATA_TYPE)),
nChannelID = _config.ChannelIndex,
hWnd = IntPtr.Zero, // 不直接渲染到窗口,我们拿原始数据
// EM_A_RType_Realplay = 0 (主码流), EM_A_RType_Realplay_1 = 1 (辅码流)
rType = _config.StreamType == 0 ? EM_RealPlayType.EM_A_RType_Realplay : EM_RealPlayType.EM_A_RType_Realplay_1,
emDataType = EM_REAL_DATA_TYPE.PRIVATE, // 私有码流
cbRealDataEx = m_RealDataCallBackEx2 // 挂载静态回调
};
NET_OUT_REALPLAY_BY_DATA_TYPE stuOut = new NET_OUT_REALPLAY_BY_DATA_TYPE()
{
dwSize = (uint)Marshal.SizeOf(typeof(NET_OUT_REALPLAY_BY_DATA_TYPE))
};
_realPlayId = NETClient.RealPlayByDataType(_loginId, stuIn, ref stuOut, 5000);
if (_realPlayId == IntPtr.Zero)
{
string err = NETClient.GetLastError();
NETClient.Logout(_loginId);
throw new Exception($"大华预览失败: {err}");
}
_sdkLog.Information($"[SDK] Dahua 取流成功 => RealPlayID:{_realPlayId}");
AddAuditLog($"[SDK] Dahua 取流成功");
}, token);
}
protected override async Task OnStopAsync()
{
await Task.Run(() =>
{
lock (_initLock)
{
if (_realPlayId != IntPtr.Zero)
{
NETClient.StopRealPlay(_realPlayId);
_realPlayId = IntPtr.Zero;
}
if (_playPort != -1)
{
DahuaPlaySDK.PLAY_Stop(_playPort);
DahuaPlaySDK.PLAY_CloseStream(_playPort);
DahuaPlaySDK.PLAY_ReleasePort(_playPort);
_playPort = -1;
}
if (_loginId != IntPtr.Zero)
{
_instances.TryRemove(_loginId, out _);
NETClient.Logout(_loginId);
_loginId = IntPtr.Zero;
}
_framePool?.Dispose();
_isPoolReady = false;
}
});
}
#endregion
#region --- 4. (Core Logic) ---
/// <summary>
/// 静态回调:分发数据至具体实例
/// </summary>
private static void OnRealDataReceived(IntPtr lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr param, IntPtr dwUser)
{
// 大华 SDK 无法直接在回调中给实例,需要通过句柄查找或全局状态
// 简化处理:由于 Ayay 目前追求单机性能,这里假设通过全局查找或在 RealPlayByDataType 时传入的 dwUser 识别
// 这里基于实例管理的简单逻辑:
foreach (var instance in _instances.Values)
{
if (instance._realPlayId == lRealHandle)
{
instance.ProcessRealData(dwDataType, pBuffer, dwBufSize);
break;
}
}
}
private void ProcessRealData(uint dwDataType, IntPtr pBuffer, uint dwBufSize)
{
MarkFrameReceived(dwBufSize); // 统计网络带宽
// dwDataType: 0 = 私有码流
if (dwDataType == 0 && dwBufSize > 0)
{
lock (_initLock)
{
if (_realPlayId == IntPtr.Zero) return;
if (_playPort == -1)
{
int port = 0;
if (DahuaPlaySDK.PLAY_GetFreePort(ref port))
{
_playPort = port;
DahuaPlaySDK.PLAY_SetStreamOpenMode(_playPort, 0);
DahuaPlaySDK.PLAY_OpenStream(_playPort, IntPtr.Zero, 0, 1024 * 1024 * 2);
_decCallBack = new DahuaPlaySDK.DECCBFUN(SafeOnDecodingCallBack);
DahuaPlaySDK.PLAY_SetDecCallBack(_playPort, _decCallBack);
DahuaPlaySDK.PLAY_Play(_playPort, IntPtr.Zero);
}
}
if (_playPort != -1)
{
DahuaPlaySDK.PLAY_InputData(_playPort, pBuffer, dwBufSize);
}
}
}
}
/// <summary>
/// 解码回调YUV -> SmartFrame -> Pipeline
/// <para>已集成平滑重建、引用交换、Finally释放、[诊断陷阱]、[空帧防御]</para>
/// </summary>
[HandleProcessCorruptedStateExceptions] // 捕获非托管状态损坏异常 (AccessViolation)
[SecurityCritical]
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;
// 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 currentHeight = pFrameInfo.nHeight;
// =========================================================================================
// 🛡️ [第二道防线:防死锁重建] 帧池平滑重建逻辑
// 场景:分辨率变化(主辅码流切换/ROI裁剪
// 作用:使用 TryEnter 替代 lock防止主线程 Stop 时此处死等导致死锁。
// =========================================================================================
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
bool lockTaken = false;
try
{
// 等待 50ms拿不到锁就放弃这一帧
Monitor.TryEnter(_initLock, 50, ref lockTaken);
if (lockTaken)
{
// Double Check
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
_sdkLog.Information($"[Res] 大华分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight}");
_framePool?.Dispose(); // 销毁旧池
// 更新基类维护的分辨率属性
Width = currentWidth;
Height = currentHeight;
// 重建帧池initialSize 设为 3 保证高并发缓冲maxSize 设为 5 严格控制内存总额
var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5);
_framePool = newPool;
_isPoolReady = true;
}
}
else return; // 竞争失败(可能正在停止),直接丢弃,防止卡死
}
catch (Exception ex)
{
_sdkLog.Error(ex, "[SDK] 大华帧池重建失败");
return;
}
finally { if (lockTaken) Monitor.Exit(_initLock); }
}
// 1. [核心流控] 询问基类控制器:这帧要不要?
// 之前失效是因为操作的是子类被遮蔽的 Controller现在复用基类 Controller逻辑就通了。
// 传入真实的输入帧率作为参考基准
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
if (!decision.IsCaptured) return;
SmartFrame? smartFrame = null;
try
{
smartFrame = _framePool?.Get();
if (smartFrame == null) return; // 池满丢帧
// =========================================================================================
// ⚡ [核心操作:零拷贝转换]
// 大华 PlaySDK 默认输出 I420 (YUV420P)。
// 使用 Mat.FromPixelData 封装指针,避免内存拷贝。
// =========================================================================================
using (var yuvMat = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
{
Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420);
}
// =========================================================================================
// 🛡️ [第三道防线:空结果防御]
// 场景:虽然入参合法,但 OpenCV 转换可能失败,或者 Mat 内部状态异常。
// 作用:绝对禁止将 Empty Mat 传给 Worker否则 Worker 里的 Resize 会 100% 崩溃。
// =========================================================================================
if (smartFrame.InternalMat.Empty())
{
_sdkLog.Warning($"[SDK] Dahua 解码后 Mat 为空 (转换失败), 丢弃. ID:{Config.Id}");
return; // finally 会负责 Dispose这里直接安全退出
}
// 填充订阅者
foreach (var appId in decision.TargetAppIds)
smartFrame.SubscriberIds.Enqueue(appId);
// 发送到全局路由 (注意Router 内部必须 AddRef)
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
}
catch (Exception ex)
{
// 捕获所有异常,包括非托管转换错误,防止回调线程崩溃带崩整个程序
_sdkLog.Error(ex, $"[SDK] Dahua 解码/转换流程异常. ID:{Config.Id} Size:{currentWidth}x{currentHeight}");
AddAuditLog($"[SDK] Dahua SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, Exception: {ex.Message}");
}
finally
{
// =========================================================================================
// ♻️ [引用闭环]
// 驱动层完成了它的任务,必须释放它持有的那次引用 (Count 1 -> 0, 或 2 -> 1)。
// 只有这样,当 Worker 处理完 Dispose 后,帧才能真正回到池子里。
// =========================================================================================
smartFrame?.Dispose();
}
}
#endregion
#region --- 5. (Statics) ---
private static void InitSdkGlobal()
{
if (m_RealDataCallBackEx2 != null) return;
m_DisConnectCallBack = (lLoginID, pchDVRIP, nDVRPort, dwUser) => { };
m_ReConnectCallBack = (lLoginID, pchDVRIP, nDVRPort, dwUser) => { };
m_RealDataCallBackEx2 = new fRealDataCallBackEx2(OnRealDataReceived);
NETClient.Init(m_DisConnectCallBack, IntPtr.Zero, null);
NETClient.SetAutoReconnect(m_ReConnectCallBack, IntPtr.Zero);
}
protected override void OnApplyOptions(DynamicStreamOptions options) { }
protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
#endregion
}