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

386 lines
16 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 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
}