Files
Ayay/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs
2026-03-03 13:55:37 +08:00

477 lines
20 KiB
C#
Raw Permalink 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 Lennox.LibYuvSharp;
using OpenCvSharp;
using Serilog;
using SHH.CameraSdk.DahuaFeatures;
using SHH.CameraSdk.HikFeatures;
using System;
using System.Runtime.ExceptionServices;
using System.Security;
using static SHH.CameraSdk.DahuaPlaySDK;
namespace SHH.CameraSdk;
/// <summary>
/// [大华驱动] 工业级视频源实现 (依照官方 Demo 逻辑重构版)
/// <para>当前模块: AiVideo | 核心原则: 低耦合、高并发、零拷贝</para>
/// </summary>
public class DahuaVideoSource : BaseVideoSource,
IDahuaContext, ITimeSyncFeature, IRebootFeature, IPtzFeature, IPresetFeature
{
#region --- 1. (Static Resources) ---
/// <summary> 大华 SDK 专用日志实例 </summary>
protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.DaHuaSdk);
/// <summary> 全局句柄映射表:用于静态异常回调分发至具体实例 </summary>
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 readonly DahuaRebootProvider _rebootProvider;
private readonly DahuaTimeSyncProvider _timeProvider;
private readonly DahuaPtzProvider _ptzProvider;
private readonly DahuaPresetProvider _presetProvider;
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
#region --- 3. (Constructor) ---
/// <summary>大华视频源实现</summary>
/// <param name="config"></param>
public DahuaVideoSource(VideoSourceConfig config) : base(config)
{
_rebootProvider = new DahuaRebootProvider(this);
_timeProvider = new DahuaTimeSyncProvider(this);
_ptzProvider = new DahuaPtzProvider(this);
_presetProvider = new DahuaPresetProvider(this);
}
#endregion
#region --- 4. IHikContext & Features (Interface Impls) ---
/// <summary>获取登录句柄</summary>
/// <returns></returns>
public IntPtr GetUserId() => _loginId; // 暴露父类或私有的 _loginId
/// <summary>获取设备IP</summary>
/// <returns></returns>
public string GetDeviceIp() => Config.IpAddress;
/// <summary>
/// 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑
/// </summary>
/// <returns></returns>
public Task<DateTime> GetTimeAsync() => _timeProvider.GetTimeAsync();
/// <summary>设置设备时间</summary>
/// <param name="time"></param>
/// <returns></returns>
public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time);
/// <summary>重启设备</summary>
/// <returns></returns>
public Task RebootAsync() => _rebootProvider.RebootAsync();
/// <summary>PTZ 控制</summary>
/// <param name="action"></param>
/// <param name="stop"></param>
/// <param name="speed"></param>
/// <returns></returns>
public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4)
=> _ptzProvider.PtzControlAsync(action, stop, speed);
/// <summary>PTZ 步长</summary>
/// <param name="action"></param>
/// <param name="durationMs"></param>
/// <param name="speed"></param>
/// <returns></returns>
public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4)
=> _ptzProvider.PtzStepAsync(action, durationMs, speed);
/// <summary>跳转到预置点</summary>
public Task GotoPresetAsync(int presetIndex)
=> _presetProvider.GotoPresetAsync(presetIndex);
/// <summary>设置/保存当前位置为预置点</summary>
public Task SetPresetAsync(int presetIndex)
=> _presetProvider.SetPresetAsync(presetIndex);
/// <summary>删除预置点</summary>
public Task RemovePresetAsync(int presetIndex)
=> _presetProvider.RemovePresetAsync(presetIndex);
#endregion
#region --- 5. (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 --- 6. (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);
// =================================================================================
// 🚀 [新增代码] 性能优化:尝试开启大华 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}");
}
// 设置回调与播放
_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 unsafe 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; // 池满丢帧
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,
pV, width / 2,
pU, 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);
//}
// =========================================================================================
// 🛡️ [第三道防线:空结果防御]
// 场景:虽然入参合法,但 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 --- 7. (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
}