From 83ad6221a473590030560f9a82c9e945272d20c6 Mon Sep 17 00:00:00 2001 From: twice109 <3518499@qq.com> Date: Fri, 26 Dec 2025 16:58:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9C=A8=E7=BA=BF=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E4=B8=80=E4=B8=AA=20Bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abstractions/IDeviceConnectivity.cs | 23 ++++ SHH.CameraSdk/Core/Manager/CameraManager.cs | 2 +- .../Core/Resilience/CameraCoordinator.cs | 15 ++- .../Core/Services/ConnectivitySentinel.cs | 103 ++++++++++++++++++ SHH.CameraSdk/Drivers/BaseVideoSource.cs | 30 ++++- SHH.CameraSdk/Program.cs | 27 +++++ 6 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 SHH.CameraSdk/Abstractions/IDeviceConnectivity.cs create mode 100644 SHH.CameraSdk/Core/Services/ConnectivitySentinel.cs diff --git a/SHH.CameraSdk/Abstractions/IDeviceConnectivity.cs b/SHH.CameraSdk/Abstractions/IDeviceConnectivity.cs new file mode 100644 index 0000000..35eea15 --- /dev/null +++ b/SHH.CameraSdk/Abstractions/IDeviceConnectivity.cs @@ -0,0 +1,23 @@ +namespace SHH.CameraSdk; + +/// +/// [状态代理契约] 设备连通性接口 +/// 职责:仅暴露网络探测所需的最小数据集,屏蔽驱动层的复杂逻辑 +/// +public interface IDeviceConnectivity +{ + // 设备的 ID (用于日志) + long Id { get; } + + // 目标 IP 地址 + string IpAddress { get; } + + // 当前业务状态 (用于判断是否需要降级探测策略) + VideoSourceStatus Status { get; } + + // 最后一次收到视频帧的时间 (用于帧心跳判定) + long LastFrameTick { get; } + + // [核心] 代理入口:允许外部哨兵更新设备的在线状态 + void SetNetworkStatus(bool isOnline); +} \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index 6599c7d..1e21eda 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -195,7 +195,7 @@ public class CameraManager : IDisposable, IAsyncDisposable Name = cam.Config.Name, IpAddress = cam.Config.IpAddress, Status = cam.Status.ToString(), - IsOnline = cam.IsOnline, + IsOnline = cam.IsPhysicalOnline, Fps = cam.RealFps, Bitrate = cam.RealBitrate, // [新增] 映射基类属性 TotalFrames = cam.TotalFrames, diff --git a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs index bc30274..55d7fa1 100644 --- a/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs +++ b/SHH.CameraSdk/Core/Resilience/CameraCoordinator.cs @@ -133,7 +133,7 @@ public class CameraCoordinator #region --- 状态调和逻辑 (Reconciliation Logic) --- /// - /// 相机状态调和(核心自愈逻辑) + /// 相机状态调和(核心自愈逻辑 - 修复版) /// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性 /// /// 待调和的相机设备 @@ -148,12 +148,16 @@ public class CameraCoordinator bool isFlowing = cam.IsOnline && secondsSinceLastFrame < StreamAliveThresholdSeconds; // 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测 + // (注意:如果哨兵已经更新了 Ping 状态,ProbeHardwareAsync 内部也可以优化为直接读取, + // 但在 Coordinator 里保留主动探测作为双重保险也是合理的) bool isPhysicalOk = isFlowing ? true : await ProbeHardwareAsync(cam).ConfigureAwait(false); // 4. 状态调和决策:根据物理状态与设备状态的差异执行对应操作 + + // 场景 A: 物理在线 + 设备离线 + 用户要求运行 -> 执行启动 if (isPhysicalOk && !cam.IsOnline && cam.IsRunning) { - // 物理在线 + 设备离线 + 需运行 → 执行启动(加登录锁防止冲突) + // 加登录锁防止冲突 bool lockTaken = false; try { @@ -173,14 +177,15 @@ public class CameraCoordinator } } } + // 场景 B: 物理离线 + 设备在线 -> 执行强制停止 else if (!isPhysicalOk && cam.IsOnline) { - // 物理离线 + 设备在线 → 执行停止 await cam.StopAsync().ConfigureAwait(false); } - else if (isPhysicalOk && cam.IsOnline && !isFlowing) + // 场景 C: 物理在线 + 设备在线 + 流中断 + 【用户要求运行】 -> 判定为僵死 + // 【关键修复】:增加了 && cam.IsRunning 判定,防止待机状态下被误复位 + else if (isPhysicalOk && cam.IsOnline && !isFlowing && cam.IsRunning) // [cite: 504] { - // 物理在线 + 设备在线 + 流中断 → 判定为僵死,执行复位 Console.WriteLine($"[自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中..."); await cam.StopAsync().ConfigureAwait(false); } diff --git a/SHH.CameraSdk/Core/Services/ConnectivitySentinel.cs b/SHH.CameraSdk/Core/Services/ConnectivitySentinel.cs new file mode 100644 index 0000000..25e7165 --- /dev/null +++ b/SHH.CameraSdk/Core/Services/ConnectivitySentinel.cs @@ -0,0 +1,103 @@ +using System.Net.NetworkInformation; + +namespace SHH.CameraSdk; + +/// +/// [状态代理] 网络连通性哨兵 +/// 特性: +/// 1. 低耦合:不依赖具体驱动,只依赖接口 +/// 2. 高性能:使用 Parallel.ForEachAsync 实现受控并行 +/// 3. 智能策略:播放中不Ping,空闲时才Ping +/// +public class ConnectivitySentinel +{ + private readonly CameraManager _manager; // [cite: 329] + private readonly PeriodicTimer _timer; + private readonly CancellationTokenSource _cts = new(); + + // [关键配置] 最大并发度 + // 建议值:CPU 核心数 * 4,或者固定 16-32 + // 50 个摄像头,设为 16,意味着分 4 批完成,总耗时极短 + private const int MAX_PARALLELISM = 16; + + public ConnectivitySentinel(CameraManager manager) + { + _manager = manager; + // 每 3 秒执行一轮全量巡检 + _timer = new PeriodicTimer(TimeSpan.FromSeconds(3)); + + // 启动后台任务(不阻塞主线程) + _ = RunLoopAsync(); + } + + private async Task RunLoopAsync() + { + try + { + // 等待下一个 3秒 周期 + while (await _timer.WaitForNextTickAsync(_cts.Token)) + { + // 1. 获取当前所有设备的快照 + // CameraManager.GetAllDevices() 返回的是 BaseVideoSource,它实现了 IDeviceConnectivity + var devices = _manager.GetAllDevices().Cast(); + + // 2. [核心回答] 受控并行执行 + // .NET 6+ 提供的超级 API,专门解决“一下子 50 个”的问题 + await Parallel.ForEachAsync(devices, new ParallelOptions + { + MaxDegreeOfParallelism = MAX_PARALLELISM, + CancellationToken = _cts.Token + }, + async (device, token) => + { + // 对每个设备执行独立检查 + await CheckSingleDeviceAsync(device); + }); + } + } + catch (OperationCanceledException) { /* 正常停止 */ } + } + + private async Task CheckSingleDeviceAsync(IDeviceConnectivity device) + { + bool isAlive = false; + + // [智能策略]:如果设备正在取流,直接检查帧心跳(省流模式) + if (device.Status == VideoSourceStatus.Playing || device.Status == VideoSourceStatus.Streaming) + { + long now = Environment.TickCount64; + // 5秒内有帧,就算在线 + isAlive = (now - device.LastFrameTick) < 5000; + } + else + { + // [主动探测]:空闲或离线时,发射 ICMP Ping + isAlive = await PingAsync(device.IpAddress); + } + + // [状态注入]:将探测结果“注入”回设备 + device.SetNetworkStatus(isAlive); + } + + // 纯粹的 Ping 逻辑 + private async Task PingAsync(string ip) + { + try + { + using var ping = new Ping(); + // 超时设为 800ms,快速失败,避免拖慢整体批次 + var reply = await ping.SendPingAsync(ip, 800); + return reply.Status == IPStatus.Success; + } + catch + { + return false; + } + } + + public void Stop() + { + _cts.Cancel(); + _timer.Dispose(); + } +} \ No newline at end of file diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index 0661809..aa652a0 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -12,8 +12,26 @@ /// ✅ [Bug π] 管道安全:Dispose 采用优雅关闭策略,确保剩余状态消息被消费 /// ✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑 /// -public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable +public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceConnectivity { + // [新增] 物理在线状态(专门给 Ping 使用) + private volatile bool _isPhysicalOnline; + public bool IsPhysicalOnline => _isPhysicalOnline; + + string IDeviceConnectivity.IpAddress => _config.IpAddress; + + // 允许哨兵从外部更新 _isOnline 字段 + void IDeviceConnectivity.SetNetworkStatus(bool isOnline) + { + if (_isPhysicalOnline != isOnline) + { + _isPhysicalOnline = isOnline; + // 触发状态变更是为了通知 UI 更新绿色小圆点,但不改变 Status + // 注意:这里传 _status 保持原样,只变消息 + StatusChanged?.Invoke(this, new StatusChangedEventArgs(_status, isOnline ? "物理网络恢复" : "物理网络中断")); + } + } + #region --- 1. 核心配置与锁机制 (Core Config & Locks) --- /// @@ -256,13 +274,17 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); await OnStartAsync(cts.Token).ConfigureAwait(false); + // ============================================================ + // 【核心修复位置】 + // 赋予心跳 5 秒的宽限期 (Grace Period) + // 这样 Coordinator 在未来 5 秒内计算出来的无帧时间将是负数或极小值,不会触发复位 + // ============================================================ + Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 5000); + // 标记运行状态 _isOnline = true; IsRunning = true; - // 初始化心跳:给予 2 秒宽限期,防止刚启动被判定为僵死 - Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000); - // 更新状态为播放中,并刷新元数据 UpdateStatus(VideoSourceStatus.Playing, "流传输正常运行"); await RefreshMetadataAsync().ConfigureAwait(false); diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs index 9e4be59..6d547ed 100644 --- a/SHH.CameraSdk/Program.cs +++ b/SHH.CameraSdk/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using OpenCvSharp; using SHH.CameraSdk; @@ -32,6 +33,10 @@ namespace SHH.CameraSdk // ============================================================================== var app = await StartWebMonitoring(cameraManager); + // [新增] 启动网络哨兵 (它会自动在后台跑) + // 就像保安一样,你不需要管它,它每3秒会把所有摄像头的 IsOnline 状态刷一遍 + var sentinel = new ConnectivitySentinel(cameraManager); + // ============================================================================== // 3. 业务编排:配置设备与流控策略 (8+2 演示) // ============================================================================== @@ -75,6 +80,11 @@ namespace SHH.CameraSdk { var builder = WebApplication.CreateBuilder(); + // [新增] 屏蔽日志配置 + builder.Logging.AddFilter("Microsoft", Microsoft.Extensions.Logging.LogLevel.Warning); + builder.Logging.AddFilter("System", Microsoft.Extensions.Logging.LogLevel.Warning); + builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", Microsoft.Extensions.Logging.LogLevel.Warning); + // 注入服务 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); @@ -134,6 +144,23 @@ namespace SHH.CameraSdk renderer.Enqueue(frame); }); } + + var config2 = new VideoSourceConfig + { + Id = 102, + Brand = DeviceBrand.HikVision, + IpAddress = "172.16.41.20", + Port = 8000, + Username = "admin", + Password = "abcd1234", + StreamType = 0 // 主码流 + }; + manager.AddDevice(config2); + + //if (manager.GetDevice(102) is HikVideoSource hikCamera2) + //{ + + //} } } } \ No newline at end of file