288 lines
11 KiB
C#
288 lines
11 KiB
C#
|
|
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
|
|||
|
|
}
|