Files
Ayay/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs

745 lines
33 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 Lennox.LibYuvSharp;
using OpenCvSharp;
using Serilog;
using SHH.CameraSdk.HikFeatures;
using System.Runtime.ExceptionServices;
using System.Security;
namespace SHH.CameraSdk;
/// <summary>
/// [海康驱动] 工业级视频源实现 V3.5.0 (运维增强版)
/// <para>技术支撑:基于 Hikvision CH-NetSDK V6.1.x 开发</para>
/// <para>核心职责:深度封装海康私有协议,实现从原始私有码流到 BGR 零拷贝帧池的高性能转换与分发</para>
/// 关键修复与增强记录:
/// <para>✅ 1. [Fix Bug Z] 架构修正:移除子类冗余的 Controller 定义,强制复用基类 <see cref="BaseVideoSource.Controller"/>,彻底修复多路并发下的 FPS 流控失效问题</para>
/// <para>✅ 2. [Feat A] 热更新支持:重写 <see cref="OnApplyOptions"/>,实现码流类型(Main/Sub)与渲染句柄的动态热切换,无需重启设备链路即可生效</para>
/// <para>✅ 3. [Feat B] 运维集成:全链路接入 <see cref="BaseVideoSource.AddAuditLog"/>,将登录、取流、重连及 SDK 报警实时推送至 Web 运维仪表盘</para>
/// <para>✅ 4. [Feat C] 性能优化:在解码回调中使用 <see cref="Monitor.TryEnter"/> 竞争锁,有效规避在设备断开瞬间可能产生的驱动层死锁</para>
/// </summary>
public class HikVideoSource : BaseVideoSource,
IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature
{
#region --- 1. (Static Resources) ---
/// <summary> 海康 SDK 专用日志实例 </summary>
protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.HikVisionSdk);
/// <summary> 全局句柄映射表:用于静态异常回调分发至具体实例 </summary>
private static readonly ConcurrentDictionary<int, HikVideoSource> _instances = new();
/// <summary> 静态异常回调委托引用 </summary>
private static readonly HikNativeMethods.EXCEPTION_CALLBACK _globalExceptionCallback = StaticOnSdkException;
/// <summary> 全局播放端口抢占锁 </summary>
private static readonly object _globalPortLock = new();
#endregion
#region --- 2. (Instance Members) ---
// 协议功能组件
private readonly HikTimeSyncProvider _timeProvider;
private readonly HikRebootProvider _rebootProvider;
private readonly HikPtzProvider _ptzProvider;
// SDK 句柄与资源
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 readonly HikNativeMethods.REALDATACALLBACK _realDataCallBack;
private readonly HikPlayMethods.DECCBFUN _decCallBack;
// 图像处理资源, 内存复用对象
private Mat? _sharedYuvMat;
private Mat? _sharedBgrMat;
private FramePool? _framePool;
private bool _isPoolReady = false;
#endregion
#region --- 3. (Constructor) ---
/// <summary>
/// 海康视频源实现
/// </summary>
/// <param name="config"></param>
public HikVideoSource(VideoSourceConfig config) : base(config)
{
// 初始化组件,将 "this" 作为上下文传进去
_timeProvider = new HikTimeSyncProvider(this);
_rebootProvider = new HikRebootProvider(this);
_ptzProvider = new HikPtzProvider(this);
// Modified: [Fix GC Crash] 移除此处的 new REALDATACALLBACK
// 直接使用构造函数初始化的 _realDataCallBack保证委托地址在整个对象生命周期内不变
_realDataCallBack = new HikNativeMethods.REALDATACALLBACK(SafeOnRealDataReceived);
_decCallBack = new HikPlayMethods.DECCBFUN(SafeOnDecodingCallBack);
}
#endregion
#region --- 4. IHikContext & Features (Interface Impls) ---
/// <summary>
/// 获取登录句柄
/// </summary>
/// <returns></returns>
public int GetUserId() => _userId; // 暴露父类或私有的 _userId
/// <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);
#endregion
#region --- 5. (Lifecycle Overrides) ---
/// <summary>
/// 启动逻辑
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="CameraException"></exception>
/// <exception cref="OperationCanceledException"></exception>
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.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}");
}
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);
}
/// <summary>
/// 停止逻辑
/// </summary>
/// <returns></returns>
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}");
}
/// <summary>
/// 同步清理所有 SDK 资源
/// </summary>
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)
{
lock(_globalPortLock)
{
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
{
}
}
/// <summary>
/// 获取设备元数据
/// </summary>
/// <returns></returns>
protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
#endregion
#region --- 6. (OnApplyOptions) ---
/// <summary>
/// 配置更新
/// </summary>
/// <param name="options"></param>
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 --- 7. (Streaming & Decoding) ---
/// <summary>
/// 开始预览
/// </summary>
/// <returns></returns>
private bool StartRealPlay()
{
var previewInfo = new HikNativeMethods.NET_DVR_PREVIEWINFO
{
hPlayWnd = (IntPtr)_config.RenderHandle,
lChannel = _config.ChannelIndex,
dwStreamType = (uint)_config.StreamType,
bBlocked = false
};
_realPlayHandle = HikNativeMethods.NET_DVR_RealPlay_V40(_userId, ref previewInfo, _realDataCallBack, IntPtr.Zero);
// Optimized: [Fix GC Crash] 显式保活,防止 JIT 在 P/Invoke 过程中激进回收(双重保险)
GC.KeepAlive(_realDataCallBack);
return _realPlayHandle >= 0;
}
/// <summary>
/// 预览数据回调
/// </summary>
/// <param name="lRealHandle"></param>
/// <param name="dwDataType"></param>
/// <param name="pBuffer"></param>
/// <param name="dwBufSize"></param>
/// <param name="pUser"></param>
private void SafeOnRealDataReceived(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser)
{
try
{
// 【关键位置】:在此处调用,统计网络层收到的每一字节数据
// 因为 dwBufSize > 0MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数
MarkFrameReceived(dwBufSize);
// Optimized: [原因] 增加前置失效判定,若当前句柄已释放则不再处理后续流数据
if (_realPlayHandle == -1) return;
// 处理系统头
if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD)
{
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))
{
// 开启失败需在锁内立即释放端口,防止句柄残留
lock (_globalPortLock)
{
HikPlayMethods.PlayM4_FreePort(_playPort);
_playPort = -1;
}
return;
}
//// =================================================================================
//// 🚀 [新增代码] 性能优化:适配新版 SDK 开启硬解码
//// =================================================================================
//try
//{
// // 尝试调用 Ex 版本的接口 (参数 2 表示 D3D11 硬解)
// if (HikPlayMethods.PlayM4_SetDecodeEngineEx(_playPort, 1))
// {
// _sdkLog.Information($"[Perf] Hik 强制硬解码(SetDecodeEngineEx)已开启. ID:{_config.Id}");
// }
// else
// {
// // 如果返回 false打印一下错误码
// uint err = HikPlayMethods.PlayM4_GetLastError(_playPort);
// _sdkLog.Warning($"[Perf] Hik 硬解码开启失败 Err={err}.");
// }
//}
//catch (EntryPointNotFoundException)
//{
// _sdkLog.Warning($"[Perf] PlayM4_SetDecodeEngineEx 也没找到,这太奇怪了。");
//}
HikPlayMethods.PlayM4_SetDecCallBackEx(_playPort, _decCallBack, IntPtr.Zero, 0);
// Optimized: [Fix GC Crash] 显式保活
GC.KeepAlive(_decCallBack);
HikPlayMethods.PlayM4_Play(_playPort, IntPtr.Zero);
_sdkLog.Debug($"[SDK] Hik 播放端口初始化成功, ID:{_config.Id} IP:{_config.IpAddress} Port:{_config.Port} Name:{_config.Name}, UserID: {_userId}, 播放端口:{_playPort}");
}
}
// 处理流数据
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}");
}
}
/// <summary>
/// 必须同时加上 SecurityCritical
/// </summary>
/// <param name="nPort"></param>
/// <param name="pBuf"></param>
/// <param name="nSize"></param>
/// <param name="pFrameInfo"></param>
/// <param name="nReserved1"></param>
/// <param name="nReserved2"></param>
[HandleProcessCorruptedStateExceptions]
[SecurityCritical]
private unsafe void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
{
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达.");
// 1. 基础指针检查
if (pBuf == IntPtr.Zero || nSize <= 0) return;
// 2. 视频有效性检查
// 如果不是音频,但宽高依然为 0说明是异常数据或非图像私有头
if (pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0)
{
// 如果你想调试音频,可以在这里处理 pBuf
// 但对于视频分析业务,这里直接 return不要写 Log否则磁盘会爆
return;
}
// [优化] 维持心跳,防止被哨兵误杀
MarkFrameReceived(0);
int currentWidth = pFrameInfo.nWidth;
int currentHeight = pFrameInfo.nHeight;
// 3. [核心修复点] 分辨率动态监测与帧池热重建
// Modified: [原因] 修复 Bug E当 SDK 输出的分辨率与当前帧池尺寸不符时,必须阻塞并重建资源。
// 严禁在分辨率不匹配的情况下调用 OpenCV 转换函数,防止非托管内存越界写入。
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
bool lockTaken = false;
try
{
// 尝试获取初始化锁,超时 50ms分辨率变更属于低频关键动作允许稍长等待
Monitor.TryEnter(_initLock, 50, ref lockTaken);
if (lockTaken)
{
// Double Check防止多个解码回调并发重建
if (Width != currentWidth || Height != currentHeight || !_isPoolReady)
{
_sdkLog.Warning($"[SDK] 监测到分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight},正在重建帧池...");
// 销毁旧池(内部会释放所有 Mat 资源)
_framePool?.Dispose();
// 更新基类维护的分辨率属性
Width = currentWidth;
Height = currentHeight;
// 重建帧池initialSize 设为 3 保证高并发缓冲maxSize 设为 5 严格控制内存总额
_framePool = new FramePool(Width, Height, MatType.CV_8UC3, initialSize: 3, maxSize: 5);
_isPoolReady = true;
AddAuditLog($"分辨率热重载完成: {Width}x{Height}");
}
}
else
{
// 拿不到锁说明主线程正在 Stop 或切换配置,直接丢弃该帧防止死锁
return;
}
}
catch (Exception ex)
{
_sdkLog.Error(ex, "帧池重建失败");
return;
}
finally
{
if (lockTaken) Monitor.Exit(_initLock);
}
}
// 1. [核心流控] 询问基类控制器:这帧要不要?
// 之前失效是因为操作的是子类被遮蔽的 Controller现在复用基类 Controller逻辑就通了。
// 传入真实的输入帧率作为参考基准
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
if (!decision.IsCaptured) return;
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵处理.");
// Optimized: [原因] 将 smartFrame 定义在 try 外部,确保 finally 块能够可靠执行 Dispose 归还逻辑
SmartFrame? smartFrame = null;
try
{
if (_framePool == null)
{
_sdkLog.Warning($"[SDK] Hik framePool 为空, 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}");
return;
}
// 3. 转换与分发
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,
pU, width / 2,
pV, width / 2,
pDst, width * 3,
width, height
);
//// Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离
//using (var rawYuvWrapper = Mat.FromPixelData(currentHeight + currentHeight / 2, currentWidth, MatType.CV_8UC1, pBuf))
//{
// Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
//}
// =========================================================
// 【新增防御】: 检查转换结果是否有效
// 如果转换失败,或者 Mat 为空,绝对不能传给 Router
// =========================================================
if (smartFrame.InternalMat.Empty())
{
_sdkLog.Warning($"[SDK] Hik 解码帧无效 (Empty Mat), 丢弃. 设备ID: {Config.Id} IP:{Config.IpAddress} Name:{Config.Name}");
// finally 会负责 Dispose这里直接返回
return;
}
foreach (var targetAppId in decision.TargetAppIds)
smartFrame.SubscriberIds.Enqueue(targetAppId);
// =========================================================================
// 【修正】删除这里的 GlobalStreamDispatcher.Dispatch
// 严禁在这里分发,因为这时的图是“生的”,还没经过 Pipeline 处理。
// =========================================================================
//GlobalStreamDispatcher.Dispatch(Id, smartFrame);
// 4. [分发] 将决策结果传递给处理中心
// decision.TargetAppIds 包含了 "谁需要这一帧" 的信息
//GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
}
catch (Exception ex)
{
// 这里为了性能不频繁写日志,仅在调试时开启
_sdkLog.Warning($"[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
{
// Optimized: [原因] 驱动层必须释放它持有的初始引用。
// 如果 Dispatch 内部已经 AddRef此处 Dispose 只会让计数从 2 降到 1帧不会回池。
// 如果没有其他人持有,此处 Dispose 会让计数从 1 降到 0帧安全回池。
smartFrame?.Dispose();
}
}
#endregion
#region --- 8. ---
/// <summary>
/// SDK 报警回调
/// </summary>
/// <param name="dwType"></param>
/// <param name="lUserID"></param>
/// <param name="lHandle"></param>
/// <param name="pUser"></param>
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
}