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,