Files
Ayay/SHH.CameraSdk/Drivers/DaHua/DahuaVideoSource.cs
2026-01-17 19:17:49 +08:00

288 lines
11 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);
}
}
}
}
[HandleProcessCorruptedStateExceptions]
[SecurityCritical]
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref DahuaPlaySDK.FRAME_INFO pFrameInfo, IntPtr nUser, int nReserved2)
{
if (pBuf == IntPtr.Zero || nSize <= 0) return;
MarkFrameReceived(0); // 心跳
int currentWidth = pFrameInfo.nWidth;
int currentHeight = pFrameInfo.nHeight;
// 帧池平滑重建逻辑 (保持与海康版一致,防止死锁)
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
bool lockTaken = false;
try
{
Monitor.TryEnter(_initLock, 50, ref lockTaken);
if (lockTaken)
{
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
_framePool?.Dispose();
var newPool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5);
_framePool = newPool;
Width = currentWidth;
Height = currentHeight;
_isPoolReady = true;
}
}
else return;
}
finally { if (lockTaken) Monitor.Exit(_initLock); }
}
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
if (!decision.IsCaptured) return;
SmartFrame? smartFrame = null;
try
{
smartFrame = _framePool?.Get();
if (smartFrame == null) return;
// 大华 YUV 转换为 BGR (I420)
using (var yuvMat = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
{
Cv2.CvtColor(yuvMat, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_I420);
}
// =========================================================
// 【新增防御】: 检查转换结果是否有效
// 如果转换失败,或者 Mat 为空,绝对不能传给 Router
// =========================================================
if (smartFrame.InternalMat.Empty())
{
_sdkLog.Warning($"[SDK] Dahua 解码帧无效 (Empty Mat), 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}");
// finally 会负责 Dispose这里直接返回
return;
}
foreach (var appId in decision.TargetAppIds)
smartFrame.SubscriberIds.Enqueue(appId);
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
}
catch (Exception ex)
{
_sdkLog.Error($"[SDK] Dahua 解码异常: {ex.Message}");
}
finally
{
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
}