修复 Bug

This commit is contained in:
2026-01-17 13:13:17 +08:00
parent a27045e0a0
commit 8482996a94
10 changed files with 177 additions and 59 deletions

View File

@@ -276,12 +276,28 @@ public class CameraManager : IDisposable, IAsyncDisposable
// B. 在线应用策略 // B. 在线应用策略
if (device.IsActived) 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, options.StreamType = dto.StreamType.Value;
RenderHandle = (IntPtr)dto.RenderHandle }
};
device.ApplyOptions(options); // 判定句柄是否真的变了
// Modified: 只有当 DTO 的句柄与旧配置不一致时,才放入 options
if (dto.RenderHandle != oldConfig.RenderHandle)
{
options.RenderHandle = (IntPtr)dto.RenderHandle;
}
// 只有当至少有一个参数需要更新时,才调用底层
// 假设 DynamicStreamOptions 内部有检测是否有值的方法,或者判断其属性
if (options.StreamType.HasValue || options.RenderHandle.HasValue)
{
device.ApplyOptions(options);
}
} }
} }
} }

View File

@@ -86,9 +86,12 @@ public class SmartFrame : IDisposable
/// </summary> /// </summary>
public void Activate() public void Activate()
{ {
// Optimized: [原因] 使用 Exchange 强制重置归还标记,确保该帧在逻辑上完全从池中脱离,防止归还竞态
Interlocked.Exchange(ref _isReturned, 0);
// 激活后引用计数设为 1代表生产者驱动/管道)持有该帧 // 激活后引用计数设为 1代表生产者驱动/管道)持有该帧
_refCount = 1; _refCount = 1;
_isReturned = 0; // 激活时重置归还标记
// 记录帧被取出池的时间,用于后续延迟计算 // 记录帧被取出池的时间,用于后续延迟计算
Timestamp = DateTime.Now; Timestamp = DateTime.Now;
} }

View File

@@ -1,5 +1,6 @@
using Ayay.SerilogLogs; using Ayay.SerilogLogs;
using Serilog; using Serilog;
using System.Threading.Tasks;
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
@@ -94,11 +95,19 @@ public class ProcessingPipeline
// 异步遍历队列:收到取消信号时退出循环 // 异步遍历队列:收到取消信号时退出循环
await foreach (var task in _queue.Reader.ReadAllAsync(_cts.Token)) await foreach (var task in _queue.Reader.ReadAllAsync(_cts.Token))
{ {
// 使用 using 语句:处理完成后自动调用 Frame.Dispose(),引用计数-1 try
using (task.Frame)
{ {
// 执行具体的帧处理逻辑 // 使用 using 语句:处理完成后自动调用 Frame.Dispose(),引用计数-1
ExecuteProcessing(task); using (task.Frame)
{
// 执行具体的帧处理逻辑
ExecuteProcessing(task);
}
}
catch (Exception ex)
{
// Optimized: [原因] 捕获任务级的异常,防止单帧处理失败导致整个后台处理循环终止
_sysLog.Error(ex, "[Pipeline] 关键任务执行异常 (DeviceId: {DeviceId})", task.DeviceId);
} }
} }
} }

View File

@@ -29,7 +29,8 @@ public class CameraCoordinator
private readonly SemaphoreSlim _concurrencyLimiter = new(8); private readonly SemaphoreSlim _concurrencyLimiter = new(8);
/// <summary> 相机流存活判定阈值(秒):超过该时间无帧则判定为流中断 </summary> /// <summary> 相机流存活判定阈值(秒):超过该时间无帧则判定为流中断 </summary>
private const int StreamAliveThresholdSeconds = 5; /// 海康 SDK 建立连接+首帧到达通常需 4-8 秒。阈值低了会导致刚连上就被误判为僵死而强制断开。
private const int StreamAliveThresholdSeconds = 15;
/// <summary> Ping 探测超时时间(毫秒) </summary> /// <summary> Ping 探测超时时间(毫秒) </summary>
private const int PingTimeoutMs = 800; private const int PingTimeoutMs = 800;
@@ -162,7 +163,7 @@ public class CameraCoordinator
double secondsSinceLastFrame = (nowTick - cam.LastFrameTick) / 1000.0; double secondsSinceLastFrame = (nowTick - cam.LastFrameTick) / 1000.0;
// 2. 判定流是否正常:设备在线 + 5秒内有帧 // 2. 判定流是否正常:设备在线 + 15秒内有帧
bool isFlowing = cam.IsActived && secondsSinceLastFrame < StreamAliveThresholdSeconds; bool isFlowing = cam.IsActived && secondsSinceLastFrame < StreamAliveThresholdSeconds;
// 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测 // 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测
@@ -185,6 +186,7 @@ public class CameraCoordinator
// 双重校验:防止等待锁期间状态已变更 // 双重校验:防止等待锁期间状态已变更
if (!cam.IsActived) if (!cam.IsActived)
{ {
// 记录启动时刻elapsed 将重新计时
cam.MarkStartAttempt(); cam.MarkStartAttempt();
try try
@@ -224,8 +226,20 @@ public class CameraCoordinator
// 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位 // 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位
else if (isPhysicalOk && cam.IsActived && !isFlowing && cam.IsRunning) // [cite: 504] else if (isPhysicalOk && cam.IsActived && !isFlowing && cam.IsRunning) // [cite: 504]
{ {
_sysLog.Warning($"[Coordinator] [自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); // Optimized: [修复无限重启] 增加“启动保护期”检查。
await cam.StopAsync().ConfigureAwait(false); // 原问题:相机刚 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),等待出流...");
}
} }
} }

View File

@@ -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<dynamic>(); 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<dynamic>();
} }
// [新增] 专门供审计与管理层调用的强类型方法
// Optimized: 避免匿名类型跨程序集访问失败,提供高性能的实体访问
public IEnumerable<FrameRequirement> GetRequirements()
{
return _requirements.Values;
}
} }

View File

@@ -537,7 +537,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
} }
catch (Exception ex) 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) 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 // 防止重复 Dispose
if (_isDisposed) return; if (_isDisposed) return;
_isDisposed = true;
// 1. 停止业务逻辑 // Optimized: [原因] 获取生命周期锁,防止在 DisposeAsync 执行期间被并发触发 Start/Stop 操作
await StopAsync().ConfigureAwait(false); await _lifecycleLock.WaitAsync().ConfigureAwait(false);
// 2. 优雅关闭状态分发器 try
_statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息
_distributorCts?.Cancel(); // 触发分发器取消
// 3. 等待分发器处理完剩余消息(最多等待 500ms
if (_distributorTask != null)
{ {
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. 切断事件引用,防止内存泄漏 // 6. 抑制垃圾回收器的终结器
FrameReceived = null; GC.SuppressFinalize(this);
StatusChanged = null; }
// 5. 释放基础资源
_lifecycleLock.Dispose();
_distributorCts?.Dispose();
// 6. 抑制垃圾回收器的终结器
GC.SuppressFinalize(this);
} }
#endregion #endregion

View File

@@ -419,15 +419,15 @@ public class HikVideoSource : BaseVideoSource,
#region --- (Decoding) --- #region --- (Decoding) ---
// 必须同时加上 SecurityCritical // 必须同时加上 SecurityCritical
//[HandleProcessCorruptedStateExceptions] [HandleProcessCorruptedStateExceptions]
//[SecurityCritical] [SecurityCritical]
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2) private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
{ {
// 防御性检查,防止传入空指针导致 OpenCV 崩溃CSE异常 //Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")} 帧抵达.");
if (pBuf == IntPtr.Zero || nSize <= 0)
{ // Optimized: [原因] 增加前置防御性检查,若回调入参异常立即退出,防止后续 OpenCV 封装崩溃
return; if (pBuf == IntPtr.Zero || nSize <= 0
} || pFrameInfo.nWidth <= 0 || pFrameInfo.nHeight <= 0) return;
// [优化] 维持心跳,防止被哨兵误杀 // [优化] 维持心跳,防止被哨兵误杀
MarkFrameReceived(0); MarkFrameReceived(0);
@@ -463,8 +463,8 @@ public class HikVideoSource : BaseVideoSource,
bool lockTaken = false; bool lockTaken = false;
try try
{ {
// 尝试获取锁,超时时间 0ms (拿不到立即返回 false) // 尝试获取锁,超时时间 20ms (拿不到立即返回 false)
Monitor.TryEnter(_initLock, 0, ref lockTaken); Monitor.TryEnter(_initLock, 20, ref lockTaken);
if (lockTaken) if (lockTaken)
{ {
@@ -478,7 +478,7 @@ public class HikVideoSource : BaseVideoSource,
} }
else else
{ {
// 【关键逻辑】没拿到锁,说明主线程正在操作 (通常是正在 Stop) // 【关键逻辑】如果 20ms 没拿到锁,说明主线程正在操作 (通常是正在 Stop)
// 既然都要停止了,这一帧直接丢弃,立即返回,防止死锁 // 既然都要停止了,这一帧直接丢弃,立即返回,防止死锁
return; return;
} }
@@ -491,16 +491,15 @@ public class HikVideoSource : BaseVideoSource,
if (_framePool == null) return; if (_framePool == null) return;
// 3. 转换与分发 // Optimized: [原因] 将 smartFrame 定义在 try 外部,确保 finally 块能够可靠执行 Dispose 归还逻辑
SmartFrame smartFrame = _framePool.Get(); SmartFrame? smartFrame = null;
// 【标志位】用于判断所有权是否成功移交
bool handoverSuccess = false;
try try
{ { // 3. 转换与分发
smartFrame = _framePool.Get();
if (smartFrame == null) return; // 池满丢帧 if (smartFrame == null) return; // 池满丢帧
// Optimized: [原因] 使用局部作用域封装 YUV 转换,确保原生指针尽快脱离
using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf)) using (var rawYuvWrapper = Mat.FromPixelData(height + height / 2, width, MatType.CV_8UC1, pBuf))
{ {
Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12); Cv2.CvtColor(rawYuvWrapper, smartFrame.InternalMat, ColorConversionCodes.YUV2BGR_YV12);
@@ -519,9 +518,6 @@ public class HikVideoSource : BaseVideoSource,
// decision.TargetAppIds 包含了 "谁需要这一帧" 的信息 // decision.TargetAppIds 包含了 "谁需要这一帧" 的信息
//GlobalProcessingCenter.Submit(this.Id, smartFrame, decision); //GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision); GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
// 标记成功,禁止 finally 块销毁对象
handoverSuccess = true;
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -44,8 +44,61 @@ public class DeviceConfigHandler : ICommandHandler
var device = _cameraManager.GetDevice(dto.Id); var device = _cameraManager.GetDevice(dto.Id);
string op = device != null ? "更新" : "新增"; 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}"); string changeSummary = string.Empty;
_sysLog.Debug($"[Sync] 即将{op}设备配置 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} 详情:" + "{@dto}", dto, dto.AutoSubscriptions); 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) if (device != null)
{ {

View File

@@ -103,9 +103,11 @@ public class ImageMonitorController : BackgroundService
CameraId = deviceId.ToString(), CameraId = deviceId.ToString(),
CaptureTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), CaptureTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
OriginalImageBytes = jpgBytes, // 引用赋值 OriginalImageBytes = jpgBytes, // 引用赋值
OriginalWidth = frame.InternalWidth,
OriginalHeight = frame.InnernalHeight,
TargetImageBytes = targetBytes, // 引用赋值 TargetImageBytes = targetBytes, // 引用赋值
OriginalWidth = frame.TargetWidth, TargetWidth = frame.TargetWidth,
OriginalHeight = frame.TargetHeight, TargetHeight = frame.TargetHeight,
DispatchTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() DispatchTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
}; };

View File

@@ -61,6 +61,8 @@ public class GrpcSenderWorker : BackgroundService
CaptureTimestamp = payload.CaptureTimestamp, CaptureTimestamp = payload.CaptureTimestamp,
OriginalWidth = payload.OriginalWidth, OriginalWidth = payload.OriginalWidth,
OriginalHeight = payload.OriginalHeight, OriginalHeight = payload.OriginalHeight,
TargetWidth = payload.TargetWidth,
TargetHeight = payload.TargetHeight,
HasOriginalImage = payload.HasOriginalImage, HasOriginalImage = payload.HasOriginalImage,
HasTargetImage = payload.HasTargetImage, HasTargetImage = payload.HasTargetImage,