修复在线导致的一个 Bug
This commit is contained in:
23
SHH.CameraSdk/Abstractions/IDeviceConnectivity.cs
Normal file
23
SHH.CameraSdk/Abstractions/IDeviceConnectivity.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace SHH.CameraSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [状态代理契约] 设备连通性接口
|
||||||
|
/// 职责:仅暴露网络探测所需的最小数据集,屏蔽驱动层的复杂逻辑
|
||||||
|
/// </summary>
|
||||||
|
public interface IDeviceConnectivity
|
||||||
|
{
|
||||||
|
// 设备的 ID (用于日志)
|
||||||
|
long Id { get; }
|
||||||
|
|
||||||
|
// 目标 IP 地址
|
||||||
|
string IpAddress { get; }
|
||||||
|
|
||||||
|
// 当前业务状态 (用于判断是否需要降级探测策略)
|
||||||
|
VideoSourceStatus Status { get; }
|
||||||
|
|
||||||
|
// 最后一次收到视频帧的时间 (用于帧心跳判定)
|
||||||
|
long LastFrameTick { get; }
|
||||||
|
|
||||||
|
// [核心] 代理入口:允许外部哨兵更新设备的在线状态
|
||||||
|
void SetNetworkStatus(bool isOnline);
|
||||||
|
}
|
||||||
@@ -195,7 +195,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
|||||||
Name = cam.Config.Name,
|
Name = cam.Config.Name,
|
||||||
IpAddress = cam.Config.IpAddress,
|
IpAddress = cam.Config.IpAddress,
|
||||||
Status = cam.Status.ToString(),
|
Status = cam.Status.ToString(),
|
||||||
IsOnline = cam.IsOnline,
|
IsOnline = cam.IsPhysicalOnline,
|
||||||
Fps = cam.RealFps,
|
Fps = cam.RealFps,
|
||||||
Bitrate = cam.RealBitrate, // [新增] 映射基类属性
|
Bitrate = cam.RealBitrate, // [新增] 映射基类属性
|
||||||
TotalFrames = cam.TotalFrames,
|
TotalFrames = cam.TotalFrames,
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public class CameraCoordinator
|
|||||||
#region --- 状态调和逻辑 (Reconciliation Logic) ---
|
#region --- 状态调和逻辑 (Reconciliation Logic) ---
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 相机状态调和(核心自愈逻辑)
|
/// 相机状态调和(核心自愈逻辑 - 修复版)
|
||||||
/// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性
|
/// 功能:校验相机物理连接、流状态,执行启动/停止/复位操作,确保状态一致性
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cam">待调和的相机设备</param>
|
/// <param name="cam">待调和的相机设备</param>
|
||||||
@@ -148,12 +148,16 @@ public class CameraCoordinator
|
|||||||
bool isFlowing = cam.IsOnline && secondsSinceLastFrame < StreamAliveThresholdSeconds;
|
bool isFlowing = cam.IsOnline && secondsSinceLastFrame < StreamAliveThresholdSeconds;
|
||||||
|
|
||||||
// 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测
|
// 3. 判定物理连接是否正常:流正常则直接判定在线;否则执行 Ping+TCP 探测
|
||||||
|
// (注意:如果哨兵已经更新了 Ping 状态,ProbeHardwareAsync 内部也可以优化为直接读取,
|
||||||
|
// 但在 Coordinator 里保留主动探测作为双重保险也是合理的)
|
||||||
bool isPhysicalOk = isFlowing ? true : await ProbeHardwareAsync(cam).ConfigureAwait(false);
|
bool isPhysicalOk = isFlowing ? true : await ProbeHardwareAsync(cam).ConfigureAwait(false);
|
||||||
|
|
||||||
// 4. 状态调和决策:根据物理状态与设备状态的差异执行对应操作
|
// 4. 状态调和决策:根据物理状态与设备状态的差异执行对应操作
|
||||||
|
|
||||||
|
// 场景 A: 物理在线 + 设备离线 + 用户要求运行 -> 执行启动
|
||||||
if (isPhysicalOk && !cam.IsOnline && cam.IsRunning)
|
if (isPhysicalOk && !cam.IsOnline && cam.IsRunning)
|
||||||
{
|
{
|
||||||
// 物理在线 + 设备离线 + 需运行 → 执行启动(加登录锁防止冲突)
|
// 加登录锁防止冲突
|
||||||
bool lockTaken = false;
|
bool lockTaken = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -173,14 +177,15 @@ public class CameraCoordinator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 场景 B: 物理离线 + 设备在线 -> 执行强制停止
|
||||||
else if (!isPhysicalOk && cam.IsOnline)
|
else if (!isPhysicalOk && cam.IsOnline)
|
||||||
{
|
{
|
||||||
// 物理离线 + 设备在线 → 执行停止
|
|
||||||
await cam.StopAsync().ConfigureAwait(false);
|
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}秒无帧),复位中...");
|
Console.WriteLine($"[自愈] 设备 {cam.Id} 僵死({secondsSinceLastFrame:F1}秒无帧),复位中...");
|
||||||
await cam.StopAsync().ConfigureAwait(false);
|
await cam.StopAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
103
SHH.CameraSdk/Core/Services/ConnectivitySentinel.cs
Normal file
103
SHH.CameraSdk/Core/Services/ConnectivitySentinel.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
|
namespace SHH.CameraSdk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [状态代理] 网络连通性哨兵
|
||||||
|
/// 特性:
|
||||||
|
/// 1. 低耦合:不依赖具体驱动,只依赖接口
|
||||||
|
/// 2. 高性能:使用 Parallel.ForEachAsync 实现受控并行
|
||||||
|
/// 3. 智能策略:播放中不Ping,空闲时才Ping
|
||||||
|
/// </summary>
|
||||||
|
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<IDeviceConnectivity>();
|
||||||
|
|
||||||
|
// 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<bool> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,26 @@
|
|||||||
/// <para>✅ [Bug π] 管道安全:Dispose 采用优雅关闭策略,确保剩余状态消息被消费</para>
|
/// <para>✅ [Bug π] 管道安全:Dispose 采用优雅关闭策略,确保剩余状态消息被消费</para>
|
||||||
/// <para>✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑</para>
|
/// <para>✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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) ---
|
#region --- 1. 核心配置与锁机制 (Core Config & Locks) ---
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -256,13 +274,17 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
|
|||||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||||
await OnStartAsync(cts.Token).ConfigureAwait(false);
|
await OnStartAsync(cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 【核心修复位置】
|
||||||
|
// 赋予心跳 5 秒的宽限期 (Grace Period)
|
||||||
|
// 这样 Coordinator 在未来 5 秒内计算出来的无帧时间将是负数或极小值,不会触发复位
|
||||||
|
// ============================================================
|
||||||
|
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 5000);
|
||||||
|
|
||||||
// 标记运行状态
|
// 标记运行状态
|
||||||
_isOnline = true;
|
_isOnline = true;
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
|
|
||||||
// 初始化心跳:给予 2 秒宽限期,防止刚启动被判定为僵死
|
|
||||||
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000);
|
|
||||||
|
|
||||||
// 更新状态为播放中,并刷新元数据
|
// 更新状态为播放中,并刷新元数据
|
||||||
UpdateStatus(VideoSourceStatus.Playing, "流传输正常运行");
|
UpdateStatus(VideoSourceStatus.Playing, "流传输正常运行");
|
||||||
await RefreshMetadataAsync().ConfigureAwait(false);
|
await RefreshMetadataAsync().ConfigureAwait(false);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using OpenCvSharp;
|
using OpenCvSharp;
|
||||||
using SHH.CameraSdk;
|
using SHH.CameraSdk;
|
||||||
@@ -32,6 +33,10 @@ namespace SHH.CameraSdk
|
|||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
var app = await StartWebMonitoring(cameraManager);
|
var app = await StartWebMonitoring(cameraManager);
|
||||||
|
|
||||||
|
// [新增] 启动网络哨兵 (它会自动在后台跑)
|
||||||
|
// 就像保安一样,你不需要管它,它每3秒会把所有摄像头的 IsOnline 状态刷一遍
|
||||||
|
var sentinel = new ConnectivitySentinel(cameraManager);
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// 3. 业务编排:配置设备与流控策略 (8+2 演示)
|
// 3. 业务编排:配置设备与流控策略 (8+2 演示)
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
@@ -75,6 +80,11 @@ namespace SHH.CameraSdk
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder();
|
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.AddControllers();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
@@ -134,6 +144,23 @@ namespace SHH.CameraSdk
|
|||||||
renderer.Enqueue(frame);
|
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)
|
||||||
|
//{
|
||||||
|
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user