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

386 lines
16 KiB
C#
Raw Normal View History

using OpenCvSharp;
namespace SHH.CameraSdk;
/// <summary>
/// [海康驱动] 工业级视频源实现 V3.3.1 (极高并发修正版)
/// 核心职责:负责海康威视设备 (SDK) 的物理连接、取流、解码与资源管理。
/// 核心修复记录:
/// 1. [Bug X] 异步竞争:引入 Epoch 世代验证,防止超时取消后的幽灵任务覆盖新连接。
/// 2. [Bug Y] 内存踩踏:解码回调加锁,防止多核环境下共享 Mat 被并发读写引发 AV 异常。
/// 3. [Bug α] 端口抢占PlayM4_GetPort 全局加锁,防止高并发启动时的播放端口串位。
/// 4. [Bug H/W/T/E] 继承之前的路由分发、幽灵句柄、零 GC、异步启动等修复。
/// </summary>
public class HikVideoSource : BaseVideoSource
{
#region --- (Global Resources) ---
// 静态路由表 (Fix Bug H: 友军误伤)
// 作用:将海康 SDK 的全局回调(仅带 UserID精准路由到具体的 HikVideoSource 实例
private static readonly ConcurrentDictionary<int, HikVideoSource> _instances = new();
// 全局异常回调委托(防止 GC 回收)
private static readonly HikNativeMethods.EXCEPTION_CALLBACK _globalExceptionCallback = StaticOnSdkException;
// [Fix Bug α: 端口抢占]
// 背景:海康播放库 PlayCtrl.dll 的 PlayM4_GetPort 函数内部使用了非线程安全的全局计数器。
// 作用:使用全局静态锁强制串行化端口申请操作,防止高并发启动时分配到相同的 Port。
private static readonly object _globalPortLock = new();
#endregion
#region --- (Instance Members) ---
private int _userId = -1; // SDK 登录句柄(-1 表示未登录)
private int _realPlayHandle = -1; // 预览句柄 (网络层,-1 表示未开启预览)
private int _playPort = -1; // 播放端口 (解码层,-1 表示未分配端口)
private readonly object _initLock = new(); // 初始化/清理互斥锁:保护启动/停止流程的原子性
private readonly object _bufferLock = new(); // 解码缓冲区锁 (Fix Bug Y: 防止多线程并发读写内存)
// [Fix Bug X: 异步状态竞争]
// 作用:连接世代计数器,每次 StartAsync 调用时自增
// 原理:异步任务执行过程中验证是否为最新请求,避免幽灵任务覆盖状态
private volatile int _connectionEpoch = 0;
// 回调委托引用:必须持有以防止 P/Invoke 过程中被 GC 回收,导致回调崩溃
private HikNativeMethods.REALDATACALLBACK? _realDataCallBack;
private HikPlayMethods.DECCBFUN? _decCallBack;
// 内存复用对象 (Fix Bug T):复用非托管内存块,减少 LOH (大对象堆) 分配压力
private Mat? _sharedYuvMat;
private Mat? _sharedBgrMat;
// 帧对象池:实现零 GC 分配,避免频繁创建/销毁 Mat 导致的性能抖动
private FramePool? _framePool;
private bool _isPoolReady = false; // 帧池初始化状态标记
// 帧需求控制器管理不同订阅者UI/AI的帧率需求实现按需分发
public FrameController Controller { get; } = new();
#endregion
#region --- (Constructor) ---
/// <summary>
/// 初始化海康视频源实例
/// </summary>
/// <param name="config">视频源基础配置含设备IP、账号、码流类型等</param>
public HikVideoSource(VideoSourceConfig config) : base(config) { }
#endregion
#region --- (Core Lifecycle) ---
/// <summary>
/// [异步启动核心] (含 Bug E/X 修复)
/// 流程SDK环境初始化 → 注册全局回调 → 物理登录设备 → 路由注册 → 开启网络预览
/// </summary>
/// <param name="token">取消令牌:用于终止超时或中断的启动流程</param>
protected override async Task OnStartAsync(CancellationToken token)
{
// [Fix Bug X] 记录当前启动世代,标记一次新的启动请求
int currentEpoch = Interlocked.Increment(ref _connectionEpoch);
// [Fix Bug E] 切换到后台线程执行避免阻塞UI/上下文线程(登录为同步阻塞操作)
await Task.Run(() =>
{
// [Fix Bug X] 世代验证:若已存在新的启动请求,直接放弃当前任务
if (currentEpoch != _connectionEpoch) return;
// 初始化海康 SDK 环境(引用计数管理,确保资源不重复加载)
if (!HikSdkManager.Initialize())
throw new CameraException(CameraErrorCode.SdkNotInitialized, "SDK初始化失败", DeviceBrand.HikVision);
try
{
// 注册全局异常回调捕获断线、重连等SDK层面异常
HikNativeMethods.NET_DVR_SetExceptionCallBack_V30(0, IntPtr.Zero, _globalExceptionCallback, IntPtr.Zero);
// 执行设备物理登录(阻塞调用,网络异常时可能耗时数秒)
var devInfo = new HikNativeMethods.NET_DEVICEINFO_V30();
int newUserId = HikNativeMethods.NET_DVR_Login_V30(
_config.IpAddress, _config.Port, _config.Username, _config.Password, ref devInfo);
// [Fix Bug X] 登录后再次验证世代:避免超时后产生的幽灵句柄
if (currentEpoch != _connectionEpoch)
{
if (newUserId >= 0) HikNativeMethods.NET_DVR_Logout(newUserId); // 释放僵尸句柄
throw new OperationCanceledException("启动任务已过期(被新的请求抢占)");
}
_userId = newUserId;
if (_userId < 0)
{
uint err = HikNativeMethods.NET_DVR_GetLastError();
throw new CameraException(HikErrorMapper.Map(err), $"登录失败: {err}", DeviceBrand.HikVision, (int)err);
}
// [Bug H] 路由注册:将 UserID 与当前实例绑定,支持全局回调路由
_instances.TryAdd(_userId, this);
// 开启网络预览(取流):失败则抛出异常,触发资源清理
if (!StartRealPlay())
{
uint err = HikNativeMethods.NET_DVR_GetLastError();
throw new CameraException(HikErrorMapper.Map(err), $"预览失败: {err}", DeviceBrand.HikVision, (int)err);
}
}
catch
{
// [Fix Bug W] 异常清理:启动失败时执行完整资源释放,防止句柄泄漏
CleanupSync();
throw;
}
}, token);
}
/// <summary>
/// 异步停止设备:终止取流、解码,释放所有资源
/// </summary>
protected override async Task OnStopAsync()
{
// [Fix Bug X] 停止时递增世代:立即使所有正在进行的启动任务失效
Interlocked.Increment(ref _connectionEpoch);
// 在后台线程执行同步清理逻辑,避免阻塞调用线程
await Task.Run(() => CleanupSync());
}
/// <summary>
/// [同步清理核心] (含 Bug Y 锁保护)
/// 职责:按“停止取流→释放解码资源→释放内存→注销登录”顺序销毁,防止非托管崩溃
/// </summary>
private void CleanupSync()
{
lock (_initLock)
{
// 1. 停止网络取流:释放预览句柄
if (_realPlayHandle >= 0)
{
HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle);
_realPlayHandle = -1;
}
// 2. 停止解码并释放播放端口:避免端口资源泄漏
if (_playPort >= 0)
{
HikPlayMethods.PlayM4_Stop(_playPort);
HikPlayMethods.PlayM4_CloseStream(_playPort);
HikPlayMethods.PlayM4_FreePort(_playPort);
_playPort = -1;
}
// [Fix Bug Y] 内存释放保护:确保解码回调未在使用内存
lock (_bufferLock)
{
_sharedYuvMat?.Dispose(); _sharedYuvMat = null;
_sharedBgrMat?.Dispose(); _sharedBgrMat = null;
}
// 3. 注销登录:先移除路由映射,再释放登录句柄
if (_userId >= 0)
{
_instances.TryRemove(_userId, out _);
HikNativeMethods.NET_DVR_Logout(_userId);
_userId = -1;
}
// 4. 释放帧对象池:清理复用内存
_framePool?.Dispose();
_framePool = null;
_isPoolReady = false;
}
// 5. 减少SDK全局引用计数确保最后一个实例销毁时卸载SDK
HikSdkManager.Uninitialize();
}
#endregion
#region --- (Network Streaming) ---
/// <summary>
/// 开启网络取流:配置预览参数,绑定流数据回调
/// </summary>
/// <returns>取流开启成功返回 true失败返回 false</returns>
private bool StartRealPlay()
{
var previewInfo = new HikNativeMethods.NET_DVR_PREVIEWINFO
{
hPlayWnd = IntPtr.Zero, // 句柄为空SDK不直接渲染通过回调获取原始流数据
lChannel = _config.ChannelIndex, // 设备通道号(从配置读取)
dwStreamType = (uint)_config.StreamType, // 码流类型(主码流/子码流,从配置读取)
bBlocked = false // 非阻塞取流:避免长时间阻塞线程
};
// 绑定网络流回调接收SDK推送的原始流数据
_realDataCallBack = new HikNativeMethods.REALDATACALLBACK(SafeOnRealDataReceived);
_realPlayHandle = HikNativeMethods.NET_DVR_RealPlay_V40(_userId, ref previewInfo, _realDataCallBack, IntPtr.Zero);
return _realPlayHandle >= 0;
}
/// <summary>
/// 网络流数据回调 (RealDataCallBack)
/// 职责:接收 SDK 原始流数据,系统头用于初始化播放库,流数据用于解码
/// </summary>
private void SafeOnRealDataReceived(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser)
{
try
{
// 预览句柄无效时直接返回,避免无效处理
if (_realPlayHandle == -1) return;
// 处理系统头:初始化播放库(仅首次接收系统头时执行)
if (dwDataType == HikNativeMethods.NET_DVR_SYSHEAD && _playPort == -1)
{
lock (_initLock)
{
// 双重校验:防止多线程下重复初始化
if (_realPlayHandle == -1 || _playPort != -1) return;
// [Fix Bug α: 端口抢占] 全局锁保护端口申请,避免并发冲突
DateTime timeStart = DateTime.Now;
bool getPortSuccess;
lock (_globalPortLock)
{
getPortSuccess = HikPlayMethods.PlayM4_GetPort(ref _playPort);
}
var useTime = Math.Round((DateTime.Now - timeStart).TotalSeconds, 1);
if (!getPortSuccess) return;
// 关键配置设置播放缓冲区为最小值1减少延时禁止播放库积压数据
HikPlayMethods.PlayM4_SetDisplayBuf(_playPort, 1);
// 初始化播放库:设置流模式→打开流→绑定解码回调→开始解码
HikPlayMethods.PlayM4_SetStreamOpenMode(_playPort, 0); // 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 { /* 吞没回调异常防止回调崩溃导致整个SDK进程退出 */ }
}
#endregion
#region --- (Decoding & Frame Distribution) ---
/// <summary>
/// 解码回调 (DecCallBack)
/// 职责:接收解码后的 YUV 数据,转码为 BGR 格式,通过帧池复用内存并分发
/// </summary>
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
{
// 汇报心跳:更新帧接收时间,防止哨兵判定设备僵死
MarkFrameReceived();
// 1. 帧分发决策:根据订阅者需求判断是否需要保留当前帧(耗时<0.01ms
var decision = Controller.MakeDecision(Environment.TickCount64);
if (!decision.IsCaptured) return;
int width = pFrameInfo.nWidth;
int height = pFrameInfo.nHeight;
// 2. 初始化帧池:首次解码时创建,按实际分辨率分配内存
if (!_isPoolReady)
{
lock (_initLock)
{
if (!_isPoolReady)
{
_framePool?.Dispose();
// 帧池配置CV_8UC3=BGR格式初始3帧最大5帧平衡内存与性能
_framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5);
_isPoolReady = true;
}
}
}
if (_framePool == null) return;
// 3. 从帧池获取内存零GC分配池满时返回null直接丢帧避免积压
SmartFrame smartFrame = _framePool.Get();
try
{
if (smartFrame == null) return; // 帧池满,丢弃当前帧
try
{
// 4. YUV转BGR直接写入帧池内存无中间对象分配
using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
{
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
}
// 5. 对外分发帧数据:通过基类事件通知订阅者(零拷贝)
RaiseFrameReceived(smartFrame);
}
catch (Exception ex)
{
// 异常时释放帧:避免内存泄漏
smartFrame.Dispose();
Debug.WriteLine($"[DecodingError] {ex.Message}");
}
// 6. 提交到全局处理中心:后续由管道处理二次加工与分发
GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
}
finally
{
// 释放驱动层引用:驱动职责结束,引用计数-1由消费者/管道管理后续生命周期)
smartFrame.Dispose();
}
}
#endregion
#region --- (Exception Handling) ---
/// <summary>
/// 全局异常回调处理
/// 职责:将 SDK 全局异常(仅带 UserID路由到对应的 HikVideoSource 实例
/// </summary>
private static void StaticOnSdkException(uint dwType, int lUserID, int lHandle, IntPtr pUser)
{
try
{
// 通过 UserID 查找实例,触发实例内异常处理逻辑
if (_instances.TryGetValue(lUserID, out var instance))
{
instance.ReportError(new CameraException(
CameraErrorCode.NetworkUnreachable,
$"SDK全局报警异常: 0x{dwType:X}",
DeviceBrand.HikVision));
}
}
catch { /* 吞没异常:避免全局回调崩溃 */ }
}
#endregion
#region --- (Metadata Fetching) ---
/// <summary>
/// 占位实现:暂未实现设备元数据获取逻辑
/// 注:实际场景需补充,用于获取设备型号、通道能力等信息
/// </summary>
protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
#endregion
}