Files
Ayay/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs
2026-01-17 13:13:17 +08:00

563 lines
25 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 OpenCvSharp;
using Serilog;
using SHH.CameraSdk.HikFeatures;
using System.Runtime.ExceptionServices;
using System.Security;
namespace SHH.CameraSdk;
/// <summary>
/// [海康驱动] 工业级视频源实现 V3.4.0 (运维增强版)
/// 修复记录:
/// 1. [Fix Bug Z] 控制器遮蔽:移除子类 Controller 定义,复用基类实例,修复 FPS 控制失效问题。
/// 2. [Feat A] 热更新支持:实现 OnApplyOptions支持码流/句柄不亦断线热切换。
/// 3. [Feat B] 审计集成:全面接入 AddAuditLog对接 Web 运维仪表盘。
/// </summary>
public class HikVideoSource : BaseVideoSource,
IHikContext, ITimeSyncFeature, IRebootFeature, IPtzFeature
{
#region --- (Global Resources) ---
// 日志实例
protected override ILogger _sdkLog => Log.ForContext("SourceContext", LogModules.HikVisionSdk);
// 静态路由表
private static readonly ConcurrentDictionary<int, HikVideoSource> _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<DateTime> 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.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);
}
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 > 0MarkFrameReceived 内部只会累加码流,不会增加 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)
{
//Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达.");
// Optimized: [原因] 增加前置防御性检查,若回调入参异常立即退出,防止后续 OpenCV 封装崩溃
if (pBuf == IntPtr.Zero || nSize <= 0
|| pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 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
{
// 尝试获取锁,超时时间 20ms (拿不到立即返回 false)
Monitor.TryEnter(_initLock, 20, 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
{
// 【关键逻辑】如果 20ms 没拿到锁,说明主线程正在操作 (通常是正在 Stop)
// 既然都要停止了,这一帧直接丢弃,立即返回,防止死锁
return;
}
}
finally
{
if (lockTaken) Monitor.Exit(_initLock);
}
}
if (_framePool == null) return;
// Optimized: [原因] 将 smartFrame 定义在 try 外部,确保 finally 块能够可靠执行 Dispose 归还逻辑
SmartFrame? smartFrame = null;
try
{ // 3. 转换与分发
smartFrame = _framePool.Get();
if (smartFrame == null) return; // 池满丢帧
// Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离
using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
{
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
}
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.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
{
// Optimized: [原因] 驱动层必须释放它持有的初始引用。
// 如果 Dispatch 内部已经 AddRef此处 Dispose 只会让计数从 2 降到 1帧不会回池。
// 如果没有其他人持有,此处 Dispose 会让计数从 1 降到 0帧安全回池。
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<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
}