完善海康 SDK 日志

This commit is contained in:
2026-01-16 17:45:27 +08:00
parent 0b374121f3
commit 97a322960a
6 changed files with 167 additions and 37 deletions

View File

@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />

View File

@@ -59,7 +59,8 @@ namespace Ayay.SerilogLogs
// 强制覆盖微软自带的啰嗦日志
builder.MinimumLevel.Override("Microsoft", LogEventLevel.Warning);
builder.MinimumLevel.Override("System", LogEventLevel.Warning);
builder.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Warning);
// 2.3 注入全套元数据 (Enrichers) - 让日志更聪明
builder
// 注入全套元数据 (Enrichers) - 让日志更聪明

View File

@@ -652,16 +652,26 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
/// </summary>
public void Dispose()
{
// 异步销毁在后台执行,避免阻塞 UI 线程
Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
// 触发异步销毁,但设定一个超时兜底,防止永久卡死 UI
// 这里等待 2 秒,如果还没销毁完也强行返回,避免界面冻结
Task.Run(async () => await DisposeAsync().ConfigureAwait(false))
.Wait(TimeSpan.FromSeconds(2));
GC.SuppressFinalize(this);
}
private volatile bool _isDisposed = false;
/// <summary>
/// 异步销毁资源(优雅关闭)
/// </summary>
/// <returns>ValueTask</returns>
public virtual async ValueTask DisposeAsync()
{
// 防止重复 Dispose
if (_isDisposed) return;
_isDisposed = true;
// 1. 停止业务逻辑
await StopAsync().ConfigureAwait(false);

View File

@@ -110,6 +110,8 @@ public class HikVideoSource : BaseVideoSource,
if (!HikSdkManager.Initialize())
{
_sdkLog.Error("[SDK] HikVision Sdk 初始化失败.");
AddAuditLog($"[SDK] HikVision Sdk 初始化失败.");
throw new CameraException(CameraErrorCode.SdkNotInitialized, "HikVision Sdk 初始化失败.", DeviceBrand.HikVision);
}
@@ -128,16 +130,22 @@ public class HikVideoSource : BaseVideoSource,
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("启动任务已过期");
}
_userId = newUserId;
if (_userId < 0)
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}");
@@ -155,8 +163,8 @@ public class HikVideoSource : BaseVideoSource,
}
catch (Exception ex)
{
_sdkLog.Error($"[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}");
_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;
}
@@ -181,18 +189,46 @@ public class HikVideoSource : BaseVideoSource,
lock (_initLock)
{
// 1. 停止预览
if (_realPlayHandle >= 0)
try
{
HikNativeMethods.NET_DVR_StopRealPlay(_realPlayHandle);
_realPlayHandle = -1;
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)
{
HikPlayMethods.PlayM4_Stop(_playPort);
HikPlayMethods.PlayM4_CloseStream(_playPort);
HikPlayMethods.PlayM4_FreePort(_playPort);
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;
}
@@ -203,18 +239,33 @@ public class HikVideoSource : BaseVideoSource,
}
// 3. 注销登录
if (_userId >= 0)
try
{
_instances.TryRemove(_userId, out _);
HikNativeMethods.NET_DVR_Logout(_userId);
_userId = -1;
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;
}
HikSdkManager.Uninitialize();
try
{
HikSdkManager.Uninitialize();
}
catch
{
}
}
#endregion
@@ -234,6 +285,15 @@ public class HikVideoSource : BaseVideoSource,
lock (_initLock)
{
// 【修复点】双重检查在线状态
// 如果在拿锁的过程中,外部已经调用了 StopAsync这里必须停止否则会创建"僵尸句柄"
if (!IsOnline || !IsPhysicalOnline || _userId < 0)
{
_sdkLog.Warning($"[SDK] 码流切换被取消,设备已离线.");
AddAuditLog($"[SDK] 码流切换被取消,设备已离线.");
return;
}
// A. 停止预览 (Keep Login)
if (_realPlayHandle >= 0)
{
@@ -264,6 +324,10 @@ public class HikVideoSource : BaseVideoSource,
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));
}
}
}
@@ -343,7 +407,11 @@ public class HikVideoSource : BaseVideoSource,
HikPlayMethods.PlayM4_InputData(_playPort, pBuffer, dwBufSize);
}
}
catch { }
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
@@ -351,10 +419,16 @@ public class HikVideoSource : BaseVideoSource,
#region --- (Decoding) ---
// 必须同时加上 SecurityCritical
[HandleProcessCorruptedStateExceptions]
[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);
@@ -380,14 +454,38 @@ public class HikVideoSource : BaseVideoSource,
// 2. 初始化帧池
if (!_isPoolReady)
{
lock (_initLock)
// ====================================================================================
// 【修改点 Start】: 使用 Monitor.TryEnter 替换 lock
// 原因:防止死锁。如果主线程 CleanupSync 持有 _initLock 正在 Stop
// 这里如果用 lock 会死等,导致 StopRealPlay 无法返回。
// 改用 TryEnter如果拿不到锁说明正在停止直接放弃这一帧并退出。
// ====================================================================================
bool lockTaken = false;
try
{
if (!_isPoolReady)
// 尝试获取锁,超时时间 0ms (拿不到立即返回 false)
Monitor.TryEnter(_initLock, 0, ref lockTaken);
if (lockTaken)
{
_framePool?.Dispose();
_framePool = new FramePool(width, height, MatType.CV_8UC3, initialSize: 3, maxSize: 5);
_isPoolReady = true;
// 拿到锁了,执行原有的初始化逻辑 (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);
}
}
@@ -395,6 +493,10 @@ public class HikVideoSource : BaseVideoSource,
// 3. 转换与分发
SmartFrame smartFrame = _framePool.Get();
// 【标志位】用于判断所有权是否成功移交
bool handoverSuccess = false;
try
{
if (smartFrame == null) return; // 池满丢帧
@@ -416,17 +518,25 @@ public class HikVideoSource : BaseVideoSource,
// decision.TargetAppIds 包含了 "谁需要这一帧" 的信息
//GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
// 标记成功,禁止 finally 块销毁对象
handoverSuccess = true;
}
catch (Exception ex)
{
smartFrame.Dispose();
// 这里为了性能不频繁写日志,仅在调试时开启
// Debug.WriteLine(ex.Message);
_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
{
if (smartFrame != null)
// 【核心修复】
// 只有当分发失败(异常)时,驱动层才负责回收。
// 一旦分发成功,所有权属于 GlobalProcessingCenter驱动层严禁 Dispose。
if (!handoverSuccess && smartFrame != null)
{
smartFrame.Dispose();
}
}
}

View File

@@ -62,14 +62,6 @@ public static class Bootstrapper
MaxRetentionDays = 10,
FileSizeLimitBytes = 1024L * 1024 * 1024,
// 动态设置日志级别
ModuleLevels = new Dictionary<string, Serilog.Events.LogEventLevel>
{
// 确保 Core 模块在调试时能输出 Debug在生产时输出 Info
{ LogModules.Core, isDebugArgs ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information },
{ LogModules.Network, Serilog.Events.LogEventLevel.Warning }
}
};
LogBootstrapper.Init(ops);

View File

@@ -1,6 +1,8 @@
using Ayay.SerilogLogs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using SHH.CameraSdk;
@@ -53,6 +55,20 @@ public class Program
// =============================================================
var builder = WebApplication.CreateBuilder(args);
// 👇👇👇 核心修复开始 👇👇👇
// ★ 1. 接管日志系统:告诉 Host 使用我们刚才配置好的 Serilog
// dispose: true 表示程序结束时自动刷新日志
builder.Host.UseSerilog(dispose: true);
// ★ 2. 斩草除根:清除 .NET 默认注入的 Console/Debug 日志提供程序
// 这一步是解决 "info: Microsoft.Hosting.Lifetime..." 重复输出的关键
builder.Logging.ClearProviders();
// ★ 3. (可选) 彻底静音:禁止 Kestrel 打印 "Now listening on..." 这种启动横幅
// 如果你只想看你自己的 "[WebApi] 🚀 服务启动...",就把这行加上
builder.WebHost.SuppressStatusMessages(true);
// ★ 核心改动:一行代码注册所有业务 (SDK, Workers, gRpc, 视频流)
builder.Services.AddCameraBusinessServices(config, sysLog);