SDK 的 Bug 修复
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SHH.CameraSdk;
|
||||
namespace SHH.CameraSdk;
|
||||
|
||||
/// <summary>
|
||||
/// 视频 SDK 统一异常类 (V3.3.1 修复版)
|
||||
|
||||
@@ -22,7 +22,7 @@ public interface IVideoSource : IDisposable, IAsyncDisposable
|
||||
bool IsRunning { get; set; }
|
||||
|
||||
/// <summary> 设备物理在线状态(基于心跳/探测的实时感知结果) </summary>
|
||||
bool IsOnline { get; }
|
||||
bool IsActived { get; }
|
||||
|
||||
/// <summary> 设备能力元数据(只读,如分辨率、码流类型、支持的功能集) </summary>
|
||||
DeviceMetadata Metadata { get; }
|
||||
|
||||
@@ -42,7 +42,7 @@ public class CamerasController : ControllerBase
|
||||
d.RealFps,
|
||||
d.TotalFrames,
|
||||
d.IsPhysicalOnline,
|
||||
d.IsOnline,
|
||||
d.IsActived,
|
||||
d.IsRunning,
|
||||
d.Width,
|
||||
d.Height,
|
||||
@@ -532,7 +532,7 @@ public class CamerasController : ControllerBase
|
||||
if (device == null) return NotFound(new { error = "Device not found" });
|
||||
|
||||
// 依然是两道防线:先检查在线,再检查能力
|
||||
if (!device.IsOnline) return BadRequest(new { error = "Device is offline" });
|
||||
if (!device.IsActived) return BadRequest(new { error = "Device is offline" });
|
||||
|
||||
if (device is IRebootFeature rebootFeature)
|
||||
{
|
||||
@@ -559,7 +559,7 @@ public class CamerasController : ControllerBase
|
||||
{
|
||||
var device = _manager.GetDevice(id);
|
||||
if (device == null) return NotFound();
|
||||
if (!device.IsOnline) return BadRequest("Device offline");
|
||||
if (!device.IsActived) return BadRequest("Device offline");
|
||||
|
||||
if (device is IPtzFeature ptz)
|
||||
{
|
||||
|
||||
@@ -76,7 +76,7 @@ public class MonitorController : ControllerBase
|
||||
{
|
||||
d.Id,
|
||||
Status = d.Status.ToString(),
|
||||
d.IsOnline,
|
||||
d.IsActived,
|
||||
d.IsPhysicalOnline,
|
||||
d.RealFps,
|
||||
d.Width,
|
||||
|
||||
@@ -66,6 +66,8 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
|
||||
}
|
||||
|
||||
_coordinator.Register(device);
|
||||
|
||||
// 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
|
||||
if (_isEngineStarted)
|
||||
device.IsRunning = true;
|
||||
@@ -211,6 +213,14 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
// 1. 审计
|
||||
_sysLog.Debug($"[Core] 响应设备配置更新请求, ID:{deviceId}.");
|
||||
|
||||
// ============================================================
|
||||
// 【核心修复:手动解除冷冻】
|
||||
// [原因] 用户已干预配置,无论之前是否认证失败,都应立即重置标记
|
||||
// 这样下一次 Coordinator (5秒内) 扫描时,会因为 IsAuthFailed == false
|
||||
// 且经过了 NormalRetryMs (30s) 而立即尝试拉起。
|
||||
// ============================================================
|
||||
device.ResetResilience();
|
||||
|
||||
// 2. 创建副本进行对比
|
||||
var oldConfig = device.Config;
|
||||
var newConfig = oldConfig.DeepCopy();
|
||||
@@ -247,13 +257,14 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
bool wasRunning = device.IsRunning;
|
||||
|
||||
// A. 彻底停止
|
||||
if (device.IsOnline) await device.StopAsync();
|
||||
if (device.IsActived) await device.StopAsync();
|
||||
|
||||
// B. 写入新配置
|
||||
device.UpdateConfig(newConfig);
|
||||
|
||||
// C. 自动重启
|
||||
if (wasRunning) await device.StartAsync();
|
||||
if (wasRunning)
|
||||
await device.StartAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -263,7 +274,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
device.UpdateConfig(newConfig);
|
||||
|
||||
// B. 在线应用策略
|
||||
if (device.IsOnline)
|
||||
if (device.IsActived)
|
||||
{
|
||||
var options = new DynamicStreamOptions
|
||||
{
|
||||
|
||||
@@ -78,6 +78,8 @@ public class SmartFrame : IDisposable
|
||||
ResetDerivatives();
|
||||
}
|
||||
|
||||
private int _isReturned = 0; // 0: 激活中, 1: 已归还池
|
||||
|
||||
/// <summary>
|
||||
/// [生产者调用] 从帧池取出时激活帧
|
||||
/// 功能:初始化引用计数,标记激活时间戳
|
||||
@@ -86,6 +88,7 @@ public class SmartFrame : IDisposable
|
||||
{
|
||||
// 激活后引用计数设为 1,代表生产者(驱动/管道)持有该帧
|
||||
_refCount = 1;
|
||||
_isReturned = 0; // 激活时重置归还标记
|
||||
// 记录帧被取出池的时间,用于后续延迟计算
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
@@ -155,11 +158,15 @@ public class SmartFrame : IDisposable
|
||||
// 原子递减:线程安全,确保计数准确
|
||||
if (Interlocked.Decrement(ref _refCount) <= 0)
|
||||
{
|
||||
// 1. 彻底清理衍生数据(TargetMat 通常是 new 出来的,必须 Dispose)
|
||||
ResetDerivatives();
|
||||
// 2. 关键:原子抢占归还权。只有成功将 _isReturned 从 0 变为 1 的线程才能执行归还逻辑。
|
||||
if (Interlocked.CompareExchange(ref _isReturned, 1, 0) == 0)
|
||||
{
|
||||
// 3. 彻底清理衍生数据(TargetMat 必须释放)
|
||||
ResetDerivatives();
|
||||
|
||||
// 2. 归还到池中复用 (InternalMat 不释放,继续保留在内存池中)
|
||||
_pool.Return(this);
|
||||
// 4. 安全归还到池中
|
||||
_pool.Return(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,10 @@ public class CameraCoordinator
|
||||
|
||||
#region --- 状态调和逻辑 (Reconciliation Logic) ---
|
||||
|
||||
private const int NormalRetryMs = 30000; // 普通网络故障:30秒后重试
|
||||
|
||||
private const int FatalRetryMs = 900000; // 认证类致命故障:15分钟后重试 (或保持 0,直到手动重置)
|
||||
|
||||
/// <summary>
|
||||
/// 相机状态调和(核心自愈逻辑 - 修复版)
|
||||
/// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性
|
||||
@@ -146,10 +150,20 @@ public class CameraCoordinator
|
||||
{
|
||||
// 1. 计算距离上次收到帧的时间(秒)
|
||||
long nowTick = Environment.TickCount64;
|
||||
|
||||
// --- [新增] 分级冷冻判定逻辑 ---
|
||||
long elapsed = nowTick - cam.LastStartAttemptTick;
|
||||
|
||||
// 如果是认证失败(致命),15分钟内不准动
|
||||
if (cam.IsAuthFailed && elapsed < FatalRetryMs) return;
|
||||
|
||||
// 如果是普通网络问题,30秒内不准动
|
||||
if (!cam.IsAuthFailed && elapsed < NormalRetryMs) return;
|
||||
|
||||
double secondsSinceLastFrame = (nowTick - cam.LastFrameTick) / 1000.0;
|
||||
|
||||
// 2. 判定流是否正常:设备在线 + 5秒内有帧
|
||||
bool isFlowing = cam.IsOnline && secondsSinceLastFrame < StreamAliveThresholdSeconds;
|
||||
bool isFlowing = cam.IsActived && secondsSinceLastFrame < StreamAliveThresholdSeconds;
|
||||
|
||||
// 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测
|
||||
// (注意:如果哨兵已经更新了 Ping 状态,ProbeHardwareAsync 内部也可以优化为直接读取,
|
||||
@@ -159,7 +173,7 @@ public class CameraCoordinator
|
||||
// 4. 状态调和决策:根据物理状态与设备状态的差异执行对应操作
|
||||
|
||||
// 场景 A: 物理在线 + 设备离线 + 用户要求运行 -> 执行启动
|
||||
if (isPhysicalOk && !cam.IsOnline && cam.IsRunning)
|
||||
if (isPhysicalOk && !cam.IsActived && cam.IsRunning)
|
||||
{
|
||||
// 加登录锁防止冲突
|
||||
bool lockTaken = false;
|
||||
@@ -167,10 +181,30 @@ public class CameraCoordinator
|
||||
{
|
||||
await _sdkLoginLock.WaitAsync(token).ConfigureAwait(false);
|
||||
lockTaken = true;
|
||||
|
||||
// 双重校验:防止等待锁期间状态已变更
|
||||
if (!cam.IsOnline)
|
||||
if (!cam.IsActived)
|
||||
{
|
||||
await cam.StartAsync().ConfigureAwait(false);
|
||||
cam.MarkStartAttempt();
|
||||
|
||||
try
|
||||
{
|
||||
await cam.StartAsync().ConfigureAwait(false);
|
||||
|
||||
// 成功后复位致命标记
|
||||
cam.IsAuthFailed = false;
|
||||
}
|
||||
catch (CameraException ex) when (ex.ErrorCode == CameraErrorCode.InvalidCredentials)
|
||||
{
|
||||
// [新增] 识别到致命密码错误,打上标记,触发 15 分钟长冷冻
|
||||
cam.IsAuthFailed = true;
|
||||
_sysLog.Fatal($"[Coordinator] 设备 {cam.Id} 认证失败(密码错),已进入 15 分钟保护性冷冻以防封 IP。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 普通异常,维持普通冷冻(30秒)
|
||||
_sysLog.Warning($"[Coordinator] 设备 {cam.Id} 启动失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -182,13 +216,13 @@ public class CameraCoordinator
|
||||
}
|
||||
}
|
||||
// 场景 B: 物理离线 + 设备在线 -> 执行强制停止
|
||||
else if (!isPhysicalOk && cam.IsOnline)
|
||||
else if (!isPhysicalOk && cam.IsActived)
|
||||
{
|
||||
await cam.StopAsync().ConfigureAwait(false);
|
||||
}
|
||||
// 场景 C: 物理在线 + 设备在线 + 流中断 + 【用户要求运行】 -> 判定为僵死
|
||||
// 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位
|
||||
else if (isPhysicalOk && cam.IsOnline && !isFlowing && cam.IsRunning) // [cite: 504]
|
||||
else if (isPhysicalOk && cam.IsActived && !isFlowing && cam.IsRunning) // [cite: 504]
|
||||
{
|
||||
_sysLog.Warning($"[Coordinator] [自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中...");
|
||||
await cam.StopAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -69,7 +69,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
#region --- 2. 内部状态与基础设施 (Internal States & Infrastructure) ---
|
||||
|
||||
/// <summary> 设备在线状态标志(volatile 确保多线程可见性) </summary>
|
||||
private volatile bool _isOnline;
|
||||
private volatile bool _isActived;
|
||||
|
||||
/// <summary> 视频源核心状态(受 _stateSyncRoot 保护) </summary>
|
||||
private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
|
||||
@@ -119,7 +119,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
/// <summary> 设备在线状态 </summary>
|
||||
public bool IsOnline => _isOnline;
|
||||
public bool IsActived => _isActived;
|
||||
|
||||
/// <summary> 设备元数据(能力集、通道信息等) </summary>
|
||||
public DeviceMetadata Metadata { get; protected set; } = new();
|
||||
@@ -223,7 +223,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
_config = newConfig.DeepCopy();
|
||||
|
||||
// 写入审计日志
|
||||
AddAuditLog($"配置已更新 [IP:{_config.IpAddress}],生效时机:{(_isOnline ? "下次重连" : "下次启动")}");
|
||||
AddAuditLog($"配置已更新 [IP:{_config.IpAddress}],生效时机:{(_isActived ? "下次重连" : "下次启动")}");
|
||||
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置落地完成");
|
||||
}
|
||||
finally
|
||||
@@ -280,7 +280,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
await _pendingLifecycleTask.ConfigureAwait(false);
|
||||
|
||||
// 幂等性检查:已在线则直接返回
|
||||
if (_isOnline) return;
|
||||
if (_isActived) return;
|
||||
|
||||
// 更新状态为连接中
|
||||
UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand} 设备...");
|
||||
@@ -297,7 +297,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 5000);
|
||||
|
||||
// 标记运行状态
|
||||
_isOnline = true;
|
||||
_isActived = true;
|
||||
IsRunning = true;
|
||||
|
||||
// 更新状态为播放中,并刷新元数据
|
||||
@@ -307,7 +307,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 异常回滚:标记离线并更新状态
|
||||
_isOnline = false;
|
||||
_isActived = false;
|
||||
UpdateStatus(VideoSourceStatus.Disconnected, $"启动失败: {ex.Message}");
|
||||
throw; // 向上抛出异常,由上层处理
|
||||
}
|
||||
@@ -326,7 +326,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
try
|
||||
{
|
||||
// 标记离线,阻断后续数据处理
|
||||
_isOnline = false;
|
||||
_isActived = false;
|
||||
|
||||
// 执行驱动层停止逻辑
|
||||
await OnStopAsync().ConfigureAwait(false);
|
||||
@@ -346,7 +346,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
public async Task<MetadataDiff> RefreshMetadataAsync()
|
||||
{
|
||||
// 离线状态不刷新元数据
|
||||
if (!_isOnline) return MetadataDiff.None;
|
||||
if (!_isActived) return MetadataDiff.None;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -379,7 +379,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
public void ApplyOptions(DynamicStreamOptions options)
|
||||
{
|
||||
// 离线或参数为空时,忽略请求
|
||||
if (options == null || !_isOnline)
|
||||
if (options == null || !_isActived)
|
||||
{
|
||||
AddAuditLog("动态参数应用失败:设备离线或参数为空");
|
||||
return;
|
||||
@@ -437,10 +437,10 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
/// <param name="ex">相机统一异常</param>
|
||||
protected void ReportError(CameraException ex)
|
||||
{
|
||||
if (!_isOnline) return;
|
||||
if (!_isActived) return;
|
||||
|
||||
// 标记离线并更新状态为重连中
|
||||
_isOnline = false;
|
||||
_isActived = false;
|
||||
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK异常: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
@@ -709,6 +709,46 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 14. 自愈辅助字段 (Resilience Helpers) ---
|
||||
|
||||
/// <summary>
|
||||
/// 认证类致命错误标记(如密码错、用户锁定)
|
||||
/// 作用:触发 15 分钟长冷冻期,防止 IP 被相机锁定
|
||||
/// </summary>
|
||||
public bool IsAuthFailed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上次尝试执行 StartAsync 的系统 Tick 时间 (单调时钟)
|
||||
/// </summary>
|
||||
private long _lastStartAttemptTick = 0;
|
||||
public long LastStartAttemptTick => Interlocked.Read(ref _lastStartAttemptTick);
|
||||
|
||||
/// <summary>
|
||||
/// 更新最后一次启动尝试的时间戳为当前时间
|
||||
/// </summary>
|
||||
public void MarkStartAttempt()
|
||||
{
|
||||
Interlocked.Exchange(ref _lastStartAttemptTick, Environment.TickCount64);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制重置自愈相关的冷却与错误标记
|
||||
/// 用于用户手动干预(如修改密码)后,使协调器能立即触发下一次尝试
|
||||
/// </summary>
|
||||
public void ResetResilience()
|
||||
{
|
||||
// 1. 清除认证失败标记
|
||||
IsAuthFailed = false;
|
||||
|
||||
// 2. 将尝试时间戳归零
|
||||
// 这样在 Coordinator 中计算 elapsed = now - 0,结果会远大于 30s
|
||||
Interlocked.Exchange(ref _lastStartAttemptTick, 0);
|
||||
|
||||
_sdkLog.Debug($"[Sdk] 设备 {Id} 自愈状态已人工重置");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// 自动从 SmartFrame 中提取
|
||||
public int Width { get; protected set; }
|
||||
public int Height { get; protected set; }
|
||||
|
||||
@@ -287,7 +287,7 @@ public class HikVideoSource : BaseVideoSource,
|
||||
{
|
||||
// 【修复点】双重检查在线状态
|
||||
// 如果在拿锁的过程中,外部已经调用了 StopAsync,这里必须停止,否则会创建"僵尸句柄"
|
||||
if (!IsOnline || !IsPhysicalOnline || _userId < 0)
|
||||
if (!IsActived || !IsPhysicalOnline || _userId < 0)
|
||||
{
|
||||
_sdkLog.Warning($"[SDK] 码流切换被取消,设备已离线.");
|
||||
AddAuditLog($"[SDK] 码流切换被取消,设备已离线.");
|
||||
|
||||
@@ -127,16 +127,20 @@ public class DeviceConfigHandler : ICommandHandler
|
||||
if (dto.ImmediateExecution)
|
||||
{
|
||||
// 情况 1: 收到“启动”指令
|
||||
if (!device.IsOnline) // 只有没在线时才点火
|
||||
if (!device.IsActived) // 只有没在线时才点火
|
||||
{
|
||||
_sysLog.Warning($"[Sync] 设备立即启动 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}");
|
||||
_ = device.StartAsync();
|
||||
// 必须在线再执行
|
||||
if (device.IsPhysicalOnline)
|
||||
{
|
||||
_sysLog.Warning($"[Sync] 设备立即启动 => ID:{dto.Id} Name:{dto.Name} IP:{dto.IpAddress} Port:{dto.Port} Brand:{(DeviceBrand)dto.Brand} Rtsp:{dto.RtspPath}");
|
||||
_ = device.StartAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 情况 2: 收到“停止”指令 (即 ImmediateExecution = false)
|
||||
if (device.IsOnline) // 只有在线时才熄火
|
||||
if (device.IsActived) // 只有在线时才熄火
|
||||
{
|
||||
_sysLog.Warning($"[Sync] 设备立即停止 {dto.Id}");
|
||||
_ = device.StopAsync();
|
||||
|
||||
Reference in New Issue
Block a user