From 97a322960a7df8d7a3df842f95b7362922b6825b Mon Sep 17 00:00:00 2001 From: wilson Date: Fri, 16 Jan 2026 17:45:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B5=B7=E5=BA=B7=20SDK=20?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ayay.SerilogLogs/Ayay.SerilogLogs.csproj | 1 + Ayay.SerilogLogs/LogBootstrapper.cs | 3 +- SHH.CameraSdk/Drivers/BaseVideoSource.cs | 14 +- .../Drivers/HikVision/HikVideoSource.cs | 162 +++++++++++++++--- SHH.CameraService/Bootstrapper.cs | 8 - SHH.CameraService/Program.cs | 16 ++ 6 files changed, 167 insertions(+), 37 deletions(-) diff --git a/Ayay.SerilogLogs/Ayay.SerilogLogs.csproj b/Ayay.SerilogLogs/Ayay.SerilogLogs.csproj index 0594d1f..bd6f411 100644 --- a/Ayay.SerilogLogs/Ayay.SerilogLogs.csproj +++ b/Ayay.SerilogLogs/Ayay.SerilogLogs.csproj @@ -6,6 +6,7 @@ + diff --git a/Ayay.SerilogLogs/LogBootstrapper.cs b/Ayay.SerilogLogs/LogBootstrapper.cs index 6500d3f..6413075 100644 --- a/Ayay.SerilogLogs/LogBootstrapper.cs +++ b/Ayay.SerilogLogs/LogBootstrapper.cs @@ -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) - 让日志更聪明 diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index 256a2bf..2177780 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -652,16 +652,26 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC /// 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; + /// /// 异步销毁资源(优雅关闭) /// /// ValueTask public virtual async ValueTask DisposeAsync() { + // 防止重复 Dispose + if (_isDisposed) return; + _isDisposed = true; + // 1. 停止业务逻辑 await StopAsync().ConfigureAwait(false); diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index ad91d83..29aa24c 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -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(); + } } } diff --git a/SHH.CameraService/Bootstrapper.cs b/SHH.CameraService/Bootstrapper.cs index 1f24a74..2a1226e 100644 --- a/SHH.CameraService/Bootstrapper.cs +++ b/SHH.CameraService/Bootstrapper.cs @@ -62,14 +62,6 @@ public static class Bootstrapper MaxRetentionDays = 10, FileSizeLimitBytes = 1024L * 1024 * 1024, - - // 动态设置日志级别 - ModuleLevels = new Dictionary - { - // 确保 Core 模块在调试时能输出 Debug,在生产时输出 Info - { LogModules.Core, isDebugArgs ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information }, - { LogModules.Network, Serilog.Events.LogEventLevel.Warning } - } }; LogBootstrapper.Init(ops); diff --git a/SHH.CameraService/Program.cs b/SHH.CameraService/Program.cs index f907384..15539ea 100644 --- a/SHH.CameraService/Program.cs +++ b/SHH.CameraService/Program.cs @@ -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);