using Ayay.SerilogLogs; using OpenCvSharp; using Serilog; using SHH.CameraSdk.HikFeatures; using System.Runtime.ExceptionServices; using System.Security; namespace SHH.CameraSdk; /// /// [海康驱动] 工业级视频源实现 V3.4.0 (运维增强版) /// 修复记录: /// 1. [Fix Bug Z] 控制器遮蔽:移除子类 Controller 定义,复用基类实例,修复 FPS 控制失效问题。 /// 2. [Feat A] 热更新支持:实现 OnApplyOptions,支持码流/句柄不亦断线热切换。 /// 3. [Feat B] 审计集成:全面接入 AddAuditLog,对接 Web 运维仪表盘。 /// public class HikVideoSource : BaseVideoSource, IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature { #region --- 静态资源 (Global Resources) --- // 日志实例 protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.HikVisionSdk); // 静态路由表 private static readonly ConcurrentDictionary _instances = new(); // 全局异常回调 private static readonly HikNativeMethods.EXCEPTION_CALLBACK _globalExceptionCallback = StaticOnSdkException; // 端口抢占锁 private static readonly object _globalPortLock = new(); #endregion // 声明组件 private readonly HikTimeSyncProvider _timeProvider; private readonly HikRebootProvider _rebootProvider; private readonly HikPtzProvider _ptzProvider; // ========================================== // 实现 IHikContext (核心数据暴露) // ========================================== public int GetUserId() => _userId; // 暴露父类或私有的 _userId public string GetDeviceIp() => Config.IpAddress; // ========================================== // 实现 ITimeSyncFeature (路由转发) // ========================================== // 核心逻辑:全部委托给 _timeProvider 处理,自己不写一行逻辑 public Task GetTimeAsync() => _timeProvider.GetTimeAsync(); public Task SetTimeAsync(DateTime time) => _timeProvider.SetTimeAsync(time); public Task RebootAsync() => _rebootProvider.RebootAsync(); public Task PtzControlAsync(PtzAction action, bool stop, int speed = 4) => _ptzProvider.PtzControlAsync(action, stop, speed); public Task PtzStepAsync(PtzAction action, int durationMs, int speed = 4) => _ptzProvider.PtzStepAsync(action, durationMs, speed); #region --- 实例成员 (Instance Members) --- private int _userId = -1; // SDK 登录句柄 private int _realPlayHandle = -1; // 预览句柄 private int _playPort = -1; // 播放端口 private readonly object _initLock = new(); private readonly object _bufferLock = new(); private volatile int _connectionEpoch = 0; // 回调委托引用 (防止GC) private HikNativeMethods.REALDATACALLBACK? _realDataCallBack; private HikPlayMethods.DECCBFUN? _decCallBack; // 内存复用对象 private Mat? _sharedYuvMat; private Mat? _sharedBgrMat; // (如有需要可复用,当前逻辑直接用FramePool) private FramePool? _framePool; private bool _isPoolReady = false; // 【关键修复 Bug Z】: 删除了这里原本的 "public FrameController Controller..." // 直接使用 BaseVideoSource.Controller #endregion #region --- 构造函数 (Constructor) --- public HikVideoSource(VideoSourceConfig config) : base(config) { // 初始化组件,将 "this" 作为上下文传进去 _timeProvider = new HikTimeSyncProvider(this); _rebootProvider = new HikRebootProvider(this); _ptzProvider = new HikPtzProvider(this); } #endregion #region --- 核心生命周期 (Core Lifecycle) --- protected override async Task OnStartAsync(CancellationToken token) { int currentEpoch = Interlocked.Increment(ref _connectionEpoch); await Task.Run(() => { if (currentEpoch != _connectionEpoch) return; if (!HikSdkManager.Initialize()) { _sdkLog.Error("[SDK] HikVision Sdk 初始化失败."); AddAuditLog($"[SDK] HikVision Sdk 初始化失败."); throw new CameraException(CameraErrorCode.SdkNotInitialized, "HikVision Sdk 初始化失败.", DeviceBrand.HikVision); } try { HikNativeMethods.NET_DVR_SetExceptionCallBack_V30(0, IntPtr.Zero, _globalExceptionCallback, IntPtr.Zero); // [审计] 记录登录动作 _sdkLog.Information($"[SDK] Hik 正在执行登录 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}"); AddAuditLog($"[SDK] Hik 正在执行登录 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}"); var devInfo = new HikNativeMethods.NET_DEVICEINFO_V30(); int newUserId = HikNativeMethods.NET_DVR_Login_V30( _config.IpAddress, _config.Port, _config.Username, _config.Password, ref devInfo); if (currentEpoch != _connectionEpoch) { if (newUserId >= 0) HikNativeMethods.NET_DVR_Logout(newUserId); _sdkLog.Information($"[SDK] Hik 启动任务已过期 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}"); AddAuditLog($"[SDK] Hik 启动任务已过期 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}"); throw new OperationCanceledException("启动任务已过期"); } if (newUserId < 0) { uint err = HikNativeMethods.NET_DVR_GetLastError(); _sdkLog.Warning($"[SDK] Hik 登录失败 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}"); AddAuditLog($"[SDK] Hik 登录失败 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}"); throw new CameraException(HikErrorMapper.Map(err), $"登录失败: {err}", DeviceBrand.HikVision, (int)err); } _userId = newUserId; _instances.TryAdd(_userId, this); _sdkLog.Information($"[SDK] Hik 登录成功 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); AddAuditLog($"[SDK] Hik 登录成功 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); // 开启取流 if (!StartRealPlay()) { uint err = HikNativeMethods.NET_DVR_GetLastError(); throw new CameraException(HikErrorMapper.Map(err), $"预览失败: {err}", DeviceBrand.HikVision, (int)err); } _sdkLog.Debug($"[SDK] Hik 网络取流成功 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); AddAuditLog($"[SDK] Hik 网络取流成功 => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); } catch (Exception ex) { _sdkLog.Error($"[SDK] Hik 启动异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, " + "Exception:{Exp}", ex); AddAuditLog($"[SDK] Hik 启动异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, Exception:{ex.Message}"); CleanupSync(); throw; } }, token); } protected override async Task OnStopAsync() { Interlocked.Increment(ref _connectionEpoch); _sdkLog.Debug($"[SDK] Hik 正在执行停止流程. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); AddAuditLog($"[SDK] Hik 正在执行停止流程. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); await Task.Run(() => CleanupSync()); _sdkLog.Information($"[SDK] Hik 设备已停止. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); AddAuditLog($"[SDK] Hik 设备已停止. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}"); } private void CleanupSync() { lock (_initLock) { // 1. 停止预览 try { if (_realPlayHandle >= 0) { HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle); _realPlayHandle = -1; } } catch(Exception ex) { _sdkLog.Debug($"[SDK] Hik 停止预览失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik 停止预览失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } // 2. 停止解码 if (_playPort >= 0) { try { HikPlayMethods.PlayM4_Stop(_playPort); HikPlayMethods.PlayM4_CloseStream(_playPort); } catch (Exception ex) { _sdkLog.Debug($"[SDK] Hik 停止解码失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik 停止解码失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } finally { try { HikPlayMethods.PlayM4_FreePort(_playPort); } catch (Exception ex) { _sdkLog.Warning($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik 端口资源释放失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } } _playPort = -1; } lock (_bufferLock) { _sharedYuvMat?.Dispose(); _sharedYuvMat = null; _sharedBgrMat?.Dispose(); _sharedBgrMat = null; } // 3. 注销登录 try { if (_userId >= 0) { _instances.TryRemove(_userId, out _); HikNativeMethods.NET_DVR_Logout(_userId); _userId = -1; } } catch (Exception ex) { _sdkLog.Warning($"[SDK] Hik 注销登录失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik 注销登录失败. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } _framePool?.Dispose(); _framePool = null; _isPoolReady = false; } try { HikSdkManager.Uninitialize(); } catch { } } #endregion #region --- [新功能] 动态参数热应用 (Hot Swap) --- // 【关键修复 Feat A】实现基类的抽象方法,处理码流切换 protected override void OnApplyOptions(DynamicStreamOptions options) { // 1. 码流热切换逻辑 if (options.StreamType.HasValue) { int targetStream = options.StreamType.Value; _sdkLog.Debug($"[SDK] Hik 收到码流切换请求. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); AddAuditLog($"收到码流切换请求: {targetStream},开始执行热切换..."); lock (_initLock) { // 【修复点】双重检查在线状态 // 如果在拿锁的过程中,外部已经调用了 StopAsync,这里必须停止,否则会创建"僵尸句柄" if (!IsActived || !IsPhysicalOnline || _userId < 0) { _sdkLog.Warning($"[SDK] 码流切换被取消,设备已离线."); AddAuditLog($"[SDK] 码流切换被取消,设备已离线."); return; } // A. 停止预览 (Keep Login) if (_realPlayHandle >= 0) { HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle); _realPlayHandle = -1; } // B. 清理播放库 (防止旧流数据残留) if (_playPort >= 0) { HikPlayMethods.PlayM4_Stop(_playPort); HikPlayMethods.PlayM4_CloseStream(_playPort); HikPlayMethods.PlayM4_FreePort(_playPort); _playPort = -1; } // C. 更新内部配置状态 _config.StreamType = targetStream; // D. 重新开启预览 if (StartRealPlay()) { _sdkLog.Information($"[SDK] Hik 码流热切换成功. => 当前: {(_config.StreamType == 0 ? "主" : "子")}码流), ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); AddAuditLog($"[SDK] Hik 码流热切换成功. => 当前: {(_config.StreamType == 0 ? "主" : "子")}码流), ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); } else { uint err = HikNativeMethods.NET_DVR_GetLastError(); _sdkLog.Information($"[SDK] Hik 码流切换失败. => Err:{err}, ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); AddAuditLog($"[SDK] Hik 码流切换失败. => Err:{err}, ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); // 【修复点】主动上报错误,让基类感知到当前已经断流了 // 这会将状态置为 Reconnecting,并可能触发自动重连 ReportError(new CameraException(HikErrorMapper.Map(err), "Hik 码流切换失败.", DeviceBrand.HikVision)); } } } // 2. 句柄动态更新逻辑 (如有需要) if (options.RenderHandle.HasValue) { // 如果是硬解码模式,可以在这里调用 PlayM4_Play(port, newHandle) _sdkLog.Information($"[SDK] Hik 收到新句柄绑定请求, 新句柄:{options.RenderHandle}. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); AddAuditLog($"[SDK] Hik 收到新句柄绑定请求, 新句柄:{options.RenderHandle}. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放句柄:{_realPlayHandle}"); } } #endregion #region --- 网络取流 (Network Streaming) --- private bool StartRealPlay() { var previewInfo = new HikNativeMethods.NET_DVR_PREVIEWINFO { hPlayWnd = (IntPtr)_config.RenderHandle, lChannel = _config.ChannelIndex, dwStreamType = (uint)_config.StreamType, bBlocked = false }; _realDataCallBack = new HikNativeMethods.REALDATACALLBACK(SafeOnRealDataReceived); _realPlayHandle = HikNativeMethods.NET_DVR_RealPlay_V40(_userId, ref previewInfo, _realDataCallBack, IntPtr.Zero); return _realPlayHandle >= 0; } private void SafeOnRealDataReceived(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser) { try { // 【关键位置】:在此处调用,统计网络层收到的每一字节数据 // 因为 dwBufSize > 0,MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数 MarkFrameReceived(dwBufSize); if (_realPlayHandle == -1) return; // 处理系统头 if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1) { lock (_initLock) { if (_realPlayHandle == -1 || _playPort != -1) return; bool getPortSuccess; lock (_globalPortLock) { getPortSuccess = HikPlayMethods.PlayM4_GetPort(ref _playPort); } if (!getPortSuccess) return; HikPlayMethods.PlayM4_SetDisplayBuf(_playPort, 1); // 极速模式 HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); if (!HikPlayMethods.PlayM4_OpenStream(_playPort, pBuffer, dwBufSize, 2 * 1024 * 1024)) { HikPlayMethods.PlayM4_FreePort(_playPort); _playPort = -1; return; } _decCallBack = new HikPlayMethods.DECCBFUN(SafeOnDecodingCallBack); HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0); HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero); } } // 处理流数据 else if (dwDataType == HikNativeMethods.NET_DVR_STREAMDATA && _playPort != -1) { HikPlayMethods.PlayM4_InputData(_playPort, pBuffer, dwBufSize); } } catch(Exception ex) { _sdkLog.Debug($"[SDK] Hik SafeOnRealDataReceived 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik SafeOnRealDataReceived 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } } #endregion #region --- 解码与帧分发 (Decoding) --- // 必须同时加上 SecurityCritical //[HandleProcessCorruptedStateExceptions] //[SecurityCritical] private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2) { // 防御性检查,防止传入空指针导致 OpenCV 崩溃(CSE异常) if (pBuf == IntPtr.Zero || nSize <= 0) { return; } // [优化] 维持心跳,防止被哨兵误杀 MarkFrameReceived(0); // [新增] 捕获并更新分辨率 // 只有当分辨率发生变化时才写入,减少属性赋值开销 if (Width != pFrameInfo.nWidth || Height != pFrameInfo.nHeight) { Width = pFrameInfo.nWidth; Height = pFrameInfo.nHeight; } // 1. [核心流控] 询问基类控制器:这帧要不要? // 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。 // 传入真实的输入帧率作为参考基准 var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps); // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return; int width = pFrameInfo.nWidth; int height = pFrameInfo.nHeight; // 2. 初始化帧池 if (!_isPoolReady) { // ==================================================================================== // 【修改点 Start】: 使用 Monitor.TryEnter 替换 lock // 原因:防止死锁。如果主线程 CleanupSync 持有 _initLock 正在 Stop, // 这里如果用 lock 会死等,导致 StopRealPlay 无法返回。 // 改用 TryEnter,如果拿不到锁(说明正在停止),直接放弃这一帧并退出。 // ==================================================================================== bool lockTaken = false; try { // 尝试获取锁,超时时间 0ms (拿不到立即返回 false) Monitor.TryEnter(_initLock, 0, ref lockTaken); if (lockTaken) { // 拿到锁了,执行原有的初始化逻辑 (Double Check) if (!_isPoolReady) { _framePool?.Dispose(); _framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5); _isPoolReady = true; } } else { // 【关键逻辑】没拿到锁,说明主线程正在操作 (通常是正在 Stop) // 既然都要停止了,这一帧直接丢弃,立即返回,防止死锁 return; } } finally { if (lockTaken) Monitor.Exit(_initLock); } } if (_framePool == null) return; // 3. 转换与分发 SmartFrame smartFrame = _framePool.Get(); // 【标志位】用于判断所有权是否成功移交 bool handoverSuccess = false; try { if (smartFrame == null) return; // 池满丢帧 using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf)) { Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12); } smartFrame.SubscriberIds.AddRange(decision.TargetAppIds); // ========================================================================= // 【修正】删除这里的 GlobalStreamDispatcher.Dispatch! // 严禁在这里分发,因为这时的图是“生的”,还没经过 Pipeline 处理。 // ========================================================================= //GlobalStreamDispatcher.Dispatch(Id, smartFrame); // 4. [分发] 将决策结果传递给处理中心 // decision.TargetAppIds 包含了 "谁需要这一帧" 的信息 //GlobalProcessingCenter.Submit(this.Id, smartFrame, decision); GlobalPipelineRouter.Enqueue(Id, smartFrame, decision); // 标记成功,禁止 finally 块销毁对象 handoverSuccess = true; } catch (Exception ex) { // 这里为了性能不频繁写日志,仅在调试时开启 _sdkLog.Debug($"[SDK] Hik SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}" + "Exception: {Exp}", ex); AddAuditLog($"[SDK] Hik SafeOnDecodingCallBack 异常. => ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId} Exception: {ex.Message}"); } finally { // 【核心修复】 // 只有当分发失败(异常)时,驱动层才负责回收。 // 一旦分发成功,所有权属于 GlobalProcessingCenter,驱动层严禁 Dispose。 if (!handoverSuccess && smartFrame != null) { smartFrame.Dispose(); } } } #endregion #region --- 异常处理 --- private static void StaticOnSdkException(uint dwType, int lUserID, int lHandle, IntPtr pUser) { try { if (_instances.TryGetValue(lUserID, out var instance)) { Log.ForContext("SourceContext", LogModules.HikVisionSdk) .Error($"Hik SDK 报警异常: 0x{dwType:X}, UserId: {lUserID} "); instance.AddAuditLog($"SDK报警 User:{pUser} 异常: 0x{dwType:X}, UserId: {lUserID}"); // 写入审计 instance.ReportError(new CameraException( CameraErrorCode.NetworkUnreachable, $"SDK全局异常: 0x{dwType:X}", DeviceBrand.HikVision)); } } catch { } } #endregion protected override Task OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata()); }