diff --git a/SHH.CameraSdk/Abstractions/Errors/CameraException.cs b/SHH.CameraSdk/Abstractions/Errors/CameraException.cs index 318aa17..48c1ad3 100644 --- a/SHH.CameraSdk/Abstractions/Errors/CameraException.cs +++ b/SHH.CameraSdk/Abstractions/Errors/CameraException.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace SHH.CameraSdk; +namespace SHH.CameraSdk; /// /// 视频 SDK 统一异常类 (V3.3.1 修复版) diff --git a/SHH.CameraSdk/Abstractions/IVideoSource.cs b/SHH.CameraSdk/Abstractions/IVideoSource.cs index 39b26c8..2dc6c2f 100644 --- a/SHH.CameraSdk/Abstractions/IVideoSource.cs +++ b/SHH.CameraSdk/Abstractions/IVideoSource.cs @@ -22,7 +22,7 @@ public interface IVideoSource : IDisposable, IAsyncDisposable bool IsRunning { get; set; } /// 设备物理在线状态(基于心跳/探测的实时感知结果) - bool IsOnline { get; } + bool IsActived { get; } /// 设备能力元数据(只读,如分辨率、码流类型、支持的功能集) DeviceMetadata Metadata { get; } diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs index 4dcc7df..c8efaab 100644 --- a/SHH.CameraSdk/Controllers/CamerasController.cs +++ b/SHH.CameraSdk/Controllers/CamerasController.cs @@ -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) { diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs index 2306e07..01e4b36 100644 --- a/SHH.CameraSdk/Controllers/MonitorController.cs +++ b/SHH.CameraSdk/Controllers/MonitorController.cs @@ -76,7 +76,7 @@ public class MonitorController : ControllerBase { d.Id, Status = d.Status.ToString(), - d.IsOnline, + d.IsActived, d.IsPhysicalOnline, d.RealFps, d.Width, diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index d05773c..a7290e8 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -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 { diff --git a/SHH.CameraSdk/Core/Memory/SmartFrame.cs b/SHH.CameraSdk/Core/Memory/SmartFrame.cs index 40c790f..81c50ee 100644 --- a/SHH.CameraSdk/Core/Memory/SmartFrame.cs +++ b/SHH.CameraSdk/Core/Memory/SmartFrame.cs @@ -78,6 +78,8 @@ public class SmartFrame : IDisposable ResetDerivatives(); } + private int _isReturned = 0; // 0: 激活中, 1: 已归还池 + /// /// [生产者调用] 从帧池取出时激活帧 /// 功能:初始化引用计数,标记激活时间戳 @@ -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); + } } } diff --git a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs index 5c51340..56af4e0 100644 --- a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs +++ b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs @@ -136,6 +136,10 @@ public class CameraCoordinator #region --- 状态调和逻辑 (Reconciliation Logic) --- + private const int NormalRetryMs = 30000; // 普通网络故障:30秒后重试 + + private const int FatalRetryMs = 900000; // 认证类致命故障:15分钟后重试 (或保持 0,直到手动重置) + /// /// 相机状态调和(核心自愈逻辑 - 修复版) /// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性 @@ -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); diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index 2177780..c37ec20 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -69,7 +69,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC #region --- 2. 内部状态与基础设施 (Internal States & Infrastructure) --- /// 设备在线状态标志(volatile 确保多线程可见性) - private volatile bool _isOnline; + private volatile bool _isActived; /// 视频源核心状态(受 _stateSyncRoot 保护) private VideoSourceStatus _status = VideoSourceStatus.Disconnected; @@ -119,7 +119,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC public bool IsRunning { get; set; } /// 设备在线状态 - public bool IsOnline => _isOnline; + public bool IsActived => _isActived; /// 设备元数据(能力集、通道信息等) 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 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 /// 相机统一异常 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) --- + + /// + /// 认证类致命错误标记(如密码错、用户锁定) + /// 作用:触发 15 分钟长冷冻期,防止 IP 被相机锁定 + /// + public bool IsAuthFailed { get; set; } + + /// + /// 上次尝试执行 StartAsync 的系统 Tick 时间 (单调时钟) + /// + private long _lastStartAttemptTick = 0; + public long LastStartAttemptTick => Interlocked.Read(ref _lastStartAttemptTick); + + /// + /// 更新最后一次启动尝试的时间戳为当前时间 + /// + public void MarkStartAttempt() + { + Interlocked.Exchange(ref _lastStartAttemptTick, Environment.TickCount64); + } + + /// + /// 强制重置自愈相关的冷却与错误标记 + /// 用于用户手动干预(如修改密码)后,使协调器能立即触发下一次尝试 + /// + 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; } diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 29aa24c..73bb336 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -287,7 +287,7 @@ public class HikVideoSource : BaseVideoSource, { // 【修复点】双重检查在线状态 // 如果在拿锁的过程中,外部已经调用了 StopAsync,这里必须停止,否则会创建"僵尸句柄" - if (!IsOnline || !IsPhysicalOnline || _userId < 0) + if (!IsActived || !IsPhysicalOnline || _userId < 0) { _sdkLog.Warning($"[SDK] 码流切换被取消,设备已离线."); AddAuditLog($"[SDK] 码流切换被取消,设备已离线."); diff --git a/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs b/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs index c3eea7a..b6af265 100644 --- a/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs +++ b/SHH.CameraService/GrpcImpls/Handlers/DeviceConfigHandler.cs @@ -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();