From 8482996a94df4d572c48a7b3a9346e400fd77666 Mon Sep 17 00:00:00 2001 From: wilson Date: Sat, 17 Jan 2026 13:13:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SHH.CameraSdk/Core/Manager/CameraManager.cs | 26 ++++++-- SHH.CameraSdk/Core/Memory/SmartFrame.cs | 5 +- .../Core/Pipeline/ProcessingPipeline.cs | 17 ++++-- .../Core/Resilience/CameraCoordinator.cs | 22 +++++-- .../Core/Scheduling/FrameController.cs | 7 +++ SHH.CameraSdk/Drivers/BaseVideoSource.cs | 60 ++++++++++++------- .../Drivers/HikVision/HikVideoSource.cs | 34 +++++------ .../GrpcImpls/Handlers/DeviceConfigHandler.cs | 57 +++++++++++++++++- .../ImageFactory/ImageMonitorController.cs | 6 +- .../GrpcImpls/ImageProcs/GrpcSenderWorker.cs | 2 + 10 files changed, 177 insertions(+), 59 deletions(-) diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index a7290e8..2228b8c 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -276,12 +276,28 @@ public class CameraManager : IDisposable, IAsyncDisposable // B. 在线应用策略 if (device.IsActived) { - var options = new DynamicStreamOptions + // Optimized: 仅构造真正发生变化的参数 + var options = new DynamicStreamOptions(); + + // 判定码流是否真的变了(或者 DTO 明确传了新值) + if (dto.StreamType.HasValue && dto.StreamType != oldConfig.StreamType) { - StreamType = dto.StreamType ?? newConfig.StreamType, - RenderHandle = (IntPtr)dto.RenderHandle - }; - device.ApplyOptions(options); + options.StreamType = dto.StreamType.Value; + } + + // 判定句柄是否真的变了 + // Modified: 只有当 DTO 的句柄与旧配置不一致时,才放入 options + if (dto.RenderHandle != oldConfig.RenderHandle) + { + options.RenderHandle = (IntPtr)dto.RenderHandle; + } + + // 只有当至少有一个参数需要更新时,才调用底层 + // 假设 DynamicStreamOptions 内部有检测是否有值的方法,或者判断其属性 + if (options.StreamType.HasValue || options.RenderHandle.HasValue) + { + device.ApplyOptions(options); + } } } } diff --git a/SHH.CameraSdk/Core/Memory/SmartFrame.cs b/SHH.CameraSdk/Core/Memory/SmartFrame.cs index 06730cd..e7e0955 100644 --- a/SHH.CameraSdk/Core/Memory/SmartFrame.cs +++ b/SHH.CameraSdk/Core/Memory/SmartFrame.cs @@ -86,9 +86,12 @@ public class SmartFrame : IDisposable /// public void Activate() { + // Optimized: [原因] 使用 Exchange 强制重置归还标记,确保该帧在逻辑上完全从池中脱离,防止归还竞态 + Interlocked.Exchange(ref _isReturned, 0); + // 激活后引用计数设为 1,代表生产者(驱动/管道)持有该帧 _refCount = 1; - _isReturned = 0; // 激活时重置归还标记 + // 记录帧被取出池的时间,用于后续延迟计算 Timestamp = DateTime.Now; } diff --git a/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs b/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs index 5fcc485..71c8729 100644 --- a/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs +++ b/SHH.CameraSdk/Core/Pipeline/ProcessingPipeline.cs @@ -1,5 +1,6 @@ using Ayay.SerilogLogs; using Serilog; +using System.Threading.Tasks; namespace SHH.CameraSdk; @@ -94,11 +95,19 @@ public class ProcessingPipeline // 异步遍历队列:收到取消信号时退出循环 await foreach (var task in _queue.Reader.ReadAllAsync(_cts.Token)) { - // 使用 using 语句:处理完成后自动调用 Frame.Dispose(),引用计数-1 - using (task.Frame) + try { - // 执行具体的帧处理逻辑 - ExecuteProcessing(task); + // 使用 using 语句:处理完成后自动调用 Frame.Dispose(),引用计数-1 + using (task.Frame) + { + // 执行具体的帧处理逻辑 + ExecuteProcessing(task); + } + } + catch (Exception ex) + { + // Optimized: [原因] 捕获任务级的异常,防止单帧处理失败导致整个后台处理循环终止 + _sysLog.Error(ex, "[Pipeline] 关键任务执行异常 (DeviceId: {DeviceId})", task.DeviceId); } } } diff --git a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs index 56af4e0..13c414e 100644 --- a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs +++ b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs @@ -29,7 +29,8 @@ public class CameraCoordinator private readonly SemaphoreSlim _concurrencyLimiter = new(8); /// 相机流存活判定阈值(秒):超过该时间无帧则判定为流中断 - private const int StreamAliveThresholdSeconds = 5; + /// 海康 SDK 建立连接+首帧到达通常需 4-8 秒。阈值低了会导致刚连上就被误判为僵死而强制断开。 + private const int StreamAliveThresholdSeconds = 15; /// Ping 探测超时时间(毫秒) private const int PingTimeoutMs = 800; @@ -162,7 +163,7 @@ public class CameraCoordinator double secondsSinceLastFrame = (nowTick - cam.LastFrameTick) / 1000.0; - // 2. 判定流是否正常:设备在线 + 5秒内有帧 + // 2. 判定流是否正常:设备在线 + 15秒内有帧 bool isFlowing = cam.IsActived && secondsSinceLastFrame < StreamAliveThresholdSeconds; // 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测 @@ -185,6 +186,7 @@ public class CameraCoordinator // 双重校验:防止等待锁期间状态已变更 if (!cam.IsActived) { + // 记录启动时刻,elapsed 将重新计时 cam.MarkStartAttempt(); try @@ -224,8 +226,20 @@ public class CameraCoordinator // 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位 else if (isPhysicalOk && cam.IsActived && !isFlowing && cam.IsRunning) // [cite: 504] { - _sysLog.Warning($"[Coordinator] [自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); - await cam.StopAsync().ConfigureAwait(false); + // Optimized: [修复无限重启] 增加“启动保护期”检查。 + // 原问题:相机刚 StartAsync 还在握手(例如第3秒),isFlowing 为 false,会导致立即被 Stop。 + // 新逻辑:只有当“启动已超过 15秒”且“依然没流”时,才判定为真正的僵死。 + + // elapsed 是毫秒,StreamAliveThresholdSeconds 是秒,需要换算 + if (elapsed > StreamAliveThresholdSeconds * 1000) + { + _sysLog.Warning($"[Coordinator] [自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); + await cam.StopAsync().ConfigureAwait(false); + } + else + { + _sysLog.Debug($"[Coordinator] 设备 {cam.Id} 启动握手中 ({elapsed}ms),等待出流..."); + } } } diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs index e1bf959..1893b18 100644 --- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs +++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs @@ -154,4 +154,11 @@ public class FrameController { return _requirements.Values.Select(r => new { r.AppId, r.TargetFps, r.RealFps, LastActive = r.LastCaptureTick, r.Memo, r.SavePath, r.Handle, r.TargetIp, r.TargetPort, r.Protocol, r.Type }).ToList(); } + + // [新增] 专门供审计与管理层调用的强类型方法 + // Optimized: 避免匿名类型跨程序集访问失败,提供高性能的实体访问 + public IEnumerable GetRequirements() + { + return _requirements.Values; + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index c37ec20..297a7d6 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -537,7 +537,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC } catch (Exception ex) { - Debug.WriteLine($"[UIEventError] 设备 {Id} 状态回调异常: {ex.Message}"); + _sdkLog.Error(ex, "设备 {Id} 状态变更回调异常", Id); } // 退出条件:取消令牌已触发 且 队列为空 @@ -554,9 +554,10 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC } } } + catch (OperationCanceledException) { /* 正常退出 */ } catch (Exception ex) { - Debug.WriteLine($"[DistributorFatal] 设备 {Id} 状态分发器崩溃: {ex.Message}"); + _sdkLog.Fatal(ex, "设备 {Id} 状态分发器致命异常", Id); } } @@ -670,31 +671,46 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC { // 防止重复 Dispose if (_isDisposed) return; - _isDisposed = true; - // 1. 停止业务逻辑 - await StopAsync().ConfigureAwait(false); + // Optimized: [原因] 获取生命周期锁,防止在 DisposeAsync 执行期间被并发触发 Start/Stop 操作 + await _lifecycleLock.WaitAsync().ConfigureAwait(false); - // 2. 优雅关闭状态分发器 - _statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息 - _distributorCts?.Cancel(); // 触发分发器取消 - - // 3. 等待分发器处理完剩余消息(最多等待 500ms) - if (_distributorTask != null) + try { - await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false); + // 防止重复 Dispose + if (_isDisposed) return; + _isDisposed = true; + + // 1. 停止业务逻辑 + await StopAsync().ConfigureAwait(false); + + // 2. 优雅关闭状态分发器 + _statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息 + _distributorCts?.Cancel(); // 触发分发器取消 + + // 3. 等待分发器处理完剩余消息(最多等待 500ms) + if (_distributorTask != null) + { + await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false); + } + + // 4. 切断事件引用,防止内存泄漏 + FrameReceived = null; + StatusChanged = null; + + // 5. 释放基础资源 + _lifecycleLock.Dispose(); + _distributorCts?.Dispose(); } + finally + { + // Modified: [原因] 保证计数锁在任何情况下都能释放 + if (!_isDisposed) + _lifecycleLock.Release(); - // 4. 切断事件引用,防止内存泄漏 - FrameReceived = null; - StatusChanged = null; - - // 5. 释放基础资源 - _lifecycleLock.Dispose(); - _distributorCts?.Dispose(); - - // 6. 抑制垃圾回收器的终结器 - GC.SuppressFinalize(this); + // 6. 抑制垃圾回收器的终结器 + GC.SuppressFinalize(this); + } } #endregion diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 986bbf9..89ac54b 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -419,15 +419,15 @@ 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; - } + //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); @@ -463,8 +463,8 @@ public class HikVideoSource : BaseVideoSource, bool lockTaken = false; try { - // 尝试获取锁,超时时间 0ms (拿不到立即返回 false) - Monitor.TryEnter(_initLock, 0, ref lockTaken); + // 尝试获取锁,超时时间 20ms (拿不到立即返回 false) + Monitor.TryEnter(_initLock, 20, ref lockTaken); if (lockTaken) { @@ -478,7 +478,7 @@ public class HikVideoSource : BaseVideoSource, } else { - // 【关键逻辑】没拿到锁,说明主线程正在操作 (通常是正在 Stop) + // 【关键逻辑】如果 20ms 没拿到锁,说明主线程正在操作 (通常是正在 Stop) // 既然都要停止了,这一帧直接丢弃,立即返回,防止死锁 return; } @@ -491,16 +491,15 @@ public class HikVideoSource : BaseVideoSource, if (_framePool == null) return; - // 3. 转换与分发 - SmartFrame smartFrame = _framePool.Get(); - - // 【标志位】用于判断所有权是否成功移交 - bool handoverSuccess = false; + // 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); @@ -519,9 +518,6 @@ public class HikVideoSource : BaseVideoSource, // decision.TargetAppIds 包含了 "谁需要这一帧" 的信息 //GlobalProcessingCenter.Submit(this.Id, smartFrame, decision); GlobalPipelineRouter.Enqueue(Id, smartFrame, decision); - - // 标记成功,禁止 finally 块销毁对象 - handoverSuccess = true; } catch (Exception ex) { diff --git a/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs index b6af265..c357eb1 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs @@ -44,8 +44,61 @@ public class DeviceConfigHandler : ICommandHandler var device = _cameraManager.GetDevice(dto.Id); string op = device != null ? "更新" : "新增"; - _sysLog.Warning($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}"); - _sysLog.Debug($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} 详情:" + "{@dto}", dto, dto.AutoSubscriptions); + string changeSummary = string.Empty; + if (device != null) + { + var old = device.Config; + var sb = new System.Text.StringBuilder(); + + // 1. 物理参数审计 (冷更新判定) + if (dto.IpAddress != old.IpAddress) sb.Append($"IP:{old.IpAddress}->{dto.IpAddress}; "); + if (dto.Port != old.Port) sb.Append($"Port:{old.Port}->{dto.Port}; "); + if (dto.Username != old.Username) sb.Append($"User:{old.Username}->{dto.Username}; "); + if (dto.Password != old.Password) sb.Append("密码:[已变更]; "); + if (dto.ChannelIndex != old.ChannelIndex) sb.Append($"通道:{old.ChannelIndex}->{dto.ChannelIndex}; "); + + // 2. 运行意图审计 (播放/停止) + // Modified: 明确呈现播放状态的切换 + if (dto.ImmediateExecution != device.IsRunning) + sb.Append($"运行状态:{(device.IsRunning ? "播放" : "停止")}->{(dto.ImmediateExecution ? "播放" : "停止")}; "); + + // 3. 图像参数审计 + if (dto.StreamType != old.StreamType) sb.Append($"码流:{old.StreamType}->{dto.StreamType}; "); + if (dto.UseGrayscale) sb.Append("灰度模式:开启; "); + + // 4. 订阅策略深度审计 (使用新增的强类型方法) + // Optimized: 通过 AppId 匹配,找出 FPS 变动 + if (dto.AutoSubscriptions != null) + { + var currentReqs = device.Controller?.GetRequirements(); + if (currentReqs != null) + { + foreach (var newSub in dto.AutoSubscriptions) + { + var matched = currentReqs.FirstOrDefault(x => x.AppId == newSub.AppId); + if (matched != null) + { + if (matched.TargetFps != newSub.TargetFps || (int)matched.Type != newSub.Type) + { + sb.Append($"[订阅变动:{newSub.AppId}] FPS:{matched.TargetFps}->{newSub.TargetFps}; "); + } + } + else + { + sb.Append($"[新增订阅:{newSub.AppId}] FPS:{newSub.TargetFps}; "); + } + } + } + } + + changeSummary = sb.Length > 0 ? $" | 变更明目: {sb.ToString().TrimEnd(' ', ';')}" : " | 配置一致"; + } + + _sysLog.Information($"[Sync] 即将{op}设备配置, 新配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}"); + _sysLog.Debug($"[Sync] 即将{op}设备配置, 新配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} 详情:" + "{@dto}", dto, dto.AutoSubscriptions); + + if (!string.IsNullOrEmpty(changeSummary)) + _sysLog.Warning($"[Sync] 即将{op}设备配置, ID:{dto.Id} 变更项 => {changeSummary}"); if (device != null) { diff --git a/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs b/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs index 2b1c3bc..65024f2 100644 --- a/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs +++ b/SHH.CameraService/GrpcImpls/ImageFactory/ImageMonitorController.cs @@ -103,9 +103,11 @@ public class ImageMonitorController : BackgroundService CameraId = deviceId.ToString(), CaptureTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), OriginalImageBytes = jpgBytes, // 引用赋值 + OriginalWidth = frame.InternalWidth, + OriginalHeight = frame.InnernalHeight, TargetImageBytes = targetBytes, // 引用赋值 - OriginalWidth = frame.TargetWidth, - OriginalHeight = frame.TargetHeight, + TargetWidth = frame.TargetWidth, + TargetHeight = frame.TargetHeight, DispatchTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; diff --git a/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs b/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs index 055df53..9569f8f 100644 --- a/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs +++ b/SHH.CameraService/GrpcImpls/ImageProcs/GrpcSenderWorker.cs @@ -61,6 +61,8 @@ public class GrpcSenderWorker : BackgroundService CaptureTimestamp = payload.CaptureTimestamp, OriginalWidth = payload.OriginalWidth, OriginalHeight = payload.OriginalHeight, + TargetWidth = payload.TargetWidth, + TargetHeight = payload.TargetHeight, HasOriginalImage = payload.HasOriginalImage, HasTargetImage = payload.HasTargetImage,