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

399 lines
17 KiB
C#
Raw Normal View History

2026-01-17 19:17:49 +08:00
using Ayay.SerilogLogs;
using Lennox.LibYuvSharp;
2026-01-17 19:17:49 +08:00
using OpenCvSharp;
using Serilog;
using System.Runtime.ExceptionServices;
using System.Security;
using static SHH.CameraSdk.DahuaPlaySDK;
2026-01-17 19:17:49 +08:00
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}");
2026-01-17 19:17:49 +08:00
}
_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);
// 打开流
2026-01-17 19:17:49 +08:00
DahuaPlaySDK.PLAY_OpenStream(_playPort, IntPtr.Zero, 0, 1024 * 1024 * 2);
// =================================================================================
// 🚀 [新增代码] 性能优化:尝试开启大华 GPU 硬解码
// 位置:必须在 PLAY_OpenStream 之后PLAY_Play 之前
// =================================================================================
try
{
_sdkLog.Information($"[Perf] Dahua 尝试开启硬解码. ID:{_config.Id} Port:{_playPort}");
// nDecodeEngine: 1 = 开启硬解码 (Nvidia/Intel)
// 注意:大华 SDK 若不支持会自动降级try-catch 仅为了防止 P/Invoke 签名缺失崩溃
// Optimized: 使用新版接口开启硬件解码,优先尝试 CUDA 以保证 Ayay 的多路并发性能
// nPort 是通过 PLAY_GetFreePort 获取的播放通道号
bool success = PLAY_SetEngine(_playPort, DecodeType.DECODE_HW_NV_CUDA, RenderType.RENDER_D3D11);
if (!success)
{
// 如果显卡不支持 CUDA降级为普通硬解或软解
PLAY_SetEngine(_playPort, DecodeType.DECODE_HW, RenderType.RENDER_D3D9);
}
}
catch (Exception ex)
{
_sdkLog.Warning($"[Perf] Dahua 开启硬解码失败: {ex.Message}");
}
// 设置回调与播放
2026-01-17 19:17:49 +08:00
_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);
}
}
}
}
2026-01-19 07:39:59 +08:00
/// <summary>
/// 解码回调YUV -> SmartFrame -> Pipeline
/// <para>已集成平滑重建、引用交换、Finally释放、[诊断陷阱]、[空帧防御]</para>
/// </summary>
[HandleProcessCorruptedStateExceptions] // 捕获非托管状态损坏异常 (AccessViolation)
2026-01-17 19:17:49 +08:00
[SecurityCritical]
private unsafe void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref DahuaPlaySDK.FRAME_INFO pFrameInfo, IntPtr nUser, int nReserved2)
2026-01-17 19:17:49 +08:00
{
2026-01-19 07:39:59 +08:00
// 1. 基础指针检查
2026-01-17 19:17:49 +08:00
if (pBuf == IntPtr.Zero || nSize <= 0) return;
2026-01-19 07:39:59 +08:00
// 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 我还活着)
2026-01-17 19:17:49 +08:00
int currentWidth = pFrameInfo.nWidth;
int currentHeight = pFrameInfo.nHeight;
2026-01-19 07:39:59 +08:00
// =========================================================================================
// 🛡️ [第二道防线:防死锁重建] 帧池平滑重建逻辑
// 场景:分辨率变化(主辅码流切换/ROI裁剪
// 作用:使用 TryEnter 替代 lock防止主线程 Stop 时此处死等导致死锁。
// =========================================================================================
2026-01-17 19:17:49 +08:00
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
bool lockTaken = false;
try
{
2026-01-19 07:39:59 +08:00
// 等待 50ms拿不到锁就放弃这一帧
2026-01-17 19:17:49 +08:00
Monitor.TryEnter(_initLock, 50, ref lockTaken);
2026-01-19 07:39:59 +08:00
2026-01-17 19:17:49 +08:00
if (lockTaken)
{
2026-01-19 07:39:59 +08:00
// Double Check
2026-01-17 19:17:49 +08:00
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
2026-01-19 07:39:59 +08:00
_sdkLog.Information($"[Res] 大华分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight}");
_framePool?.Dispose(); // 销毁旧池
// 更新基类维护的分辨率属性
2026-01-17 19:17:49 +08:00
Width = currentWidth;
Height = currentHeight;
2026-01-19 07:39:59 +08:00
// 重建帧池initialSize 设为 3 保证高并发缓冲maxSize 设为 5 严格控制内存总额
var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5);
_framePool = newPool;
2026-01-17 19:17:49 +08:00
_isPoolReady = true;
}
}
2026-01-19 07:39:59 +08:00
else return; // 竞争失败(可能正在停止),直接丢弃,防止卡死
}
catch (Exception ex)
{
_sdkLog.Error(ex, "[SDK] 大华帧池重建失败");
return;
2026-01-17 19:17:49 +08:00
}
finally { if (lockTaken) Monitor.Exit(_initLock); }
}
2026-01-19 07:39:59 +08:00
// 1. [核心流控] 询问基类控制器:这帧要不要?
// 之前失效是因为操作的是子类被遮蔽的 Controller现在复用基类 Controller逻辑就通了。
// 传入真实的输入帧率作为参考基准
2026-01-17 19:17:49 +08:00
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
2026-01-19 07:39:59 +08:00
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
2026-01-17 19:17:49 +08:00
if (!decision.IsCaptured) return;
SmartFrame? smartFrame = null;
try
{
smartFrame = _framePool?.Get();
2026-01-19 07:39:59 +08:00
if (smartFrame == null) return; // 池满丢帧
2026-01-17 19:17:49 +08:00
int width = pFrameInfo.nWidth;
int height = pFrameInfo.nHeight;
// 计算 YUV 分量地址
byte* pY = (byte*)pBuf;
byte* pU = pY + (width * height);
byte* pV = pU + (width * height / 4);
// 目标 BGR 地址
byte* pDst = (byte*)smartFrame.InternalMat.Data;
// 调用 LibYuvSharp
// 注意LibYuvSharp 内部通常处理的是 BGR 顺序,
// 如果发现图像发蓝,请将 pU 和 pV 的位置对调
LibYuv.I420ToRGB24(
pY, width,
pU, width / 2,
pV, width / 2,
pDst, width * 3,
width, height
);
//// =========================================================================================
//// ⚡ [核心操作:零拷贝转换]
//// 大华 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);
//}
2026-01-17 19:17:49 +08:00
2026-01-19 07:39:59 +08:00
// =========================================================================================
// 🛡️ [第三道防线:空结果防御]
// 场景:虽然入参合法,但 OpenCV 转换可能失败,或者 Mat 内部状态异常。
// 作用:绝对禁止将 Empty Mat 传给 Worker否则 Worker 里的 Resize 会 100% 崩溃。
// =========================================================================================
2026-01-17 19:17:49 +08:00
if (smartFrame.InternalMat.Empty())
{
2026-01-19 07:39:59 +08:00
_sdkLog.Warning($"[SDK] Dahua 解码后 Mat 为空 (转换失败), 丢弃. ID:{Config.Id}");
return; // finally 会负责 Dispose这里直接安全退出
2026-01-17 19:17:49 +08:00
}
2026-01-19 07:39:59 +08:00
// 填充订阅者
2026-01-17 19:17:49 +08:00
foreach (var appId in decision.TargetAppIds)
smartFrame.SubscriberIds.Enqueue(appId);
2026-01-19 07:39:59 +08:00
// 发送到全局路由 (注意Router 内部必须 AddRef)
2026-01-17 19:17:49 +08:00
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
}
catch (Exception ex)
{
2026-01-19 07:39:59 +08:00
// 捕获所有异常,包括非托管转换错误,防止回调线程崩溃带崩整个程序
2026-01-19 08:13:46 +08:00
_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}");
2026-01-17 19:17:49 +08:00
}
finally
{
2026-01-19 07:39:59 +08:00
// =========================================================================================
// ♻️ [引用闭环]
// 驱动层完成了它的任务,必须释放它持有的那次引用 (Count 1 -> 0, 或 2 -> 1)。
// 只有这样,当 Worker 处理完 Dispose 后,帧才能真正回到池子里。
// =========================================================================================
2026-01-17 19:17:49 +08:00
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
}