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; /// /// [大华驱动] 工业级视频源实现 (依照官方 Demo 逻辑重构版) /// 当前模块: AiVideo | 核心原则: 低耦合、高并发、零拷贝 /// public class DahuaVideoSource : BaseVideoSource, IDahuaContext, ITimeSyncFeature, IRebootFeature, IPtzFeature, IPresetFeature { #region --- 1. 静态资源与回调持有 (Static Resources) --- /// 大华 SDK 专用日志实例 protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.DaHuaSdk); /// 全局句柄映射表:用于静态异常回调分发至具体实例 private static readonly ConcurrentDictionary _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) --- /// 大华视频源实现 /// 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) --- /// 获取登录句柄 /// public IntPtr GetUserId() => _loginId; // 暴露父类或私有的 _loginId /// 获取设备IP /// public string GetDeviceIp() => Config.IpAddress; /// /// 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑 /// /// public Task GetTimeAsync() => _timeProvider.GetTimeAsync(); /// 设置设备时间 /// /// public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time); /// 重启设备 /// public Task RebootAsync() => _rebootProvider.RebootAsync(); /// PTZ 控制 /// /// /// /// public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4) => _ptzProvider.PtzControlAsync(action, stop, speed); /// PTZ 步长 /// /// /// /// public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4) => _ptzProvider.PtzStepAsync(action, durationMs, speed); /// 跳转到预置点 public Task GotoPresetAsync(int presetIndex) => _presetProvider.GotoPresetAsync(presetIndex); /// 设置/保存当前位置为预置点 public Task SetPresetAsync(int presetIndex) => _presetProvider.SetPresetAsync(presetIndex); /// 删除预置点 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) --- /// /// 静态回调:分发数据至具体实例 /// 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); } } } } /// /// 解码回调:YUV -> SmartFrame -> Pipeline /// 已集成:平滑重建、引用交换、Finally释放、[诊断陷阱]、[空帧防御] /// [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 OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata()); #endregion }