2025-12-26 03:18:21 +08:00
|
|
|
|
namespace SHH.CameraSdk;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [管理层] 视频源总控管理器 (V3.3.1 修复版)
|
|
|
|
|
|
/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈
|
|
|
|
|
|
/// 核心修复:
|
|
|
|
|
|
/// <para>1. [Bug γ] 二次伤害:强化销毁流程,防止 Dispose 阶段因 GC 乱序导致的非托管内存非法访问</para>
|
|
|
|
|
|
/// <para>2. [Bug A/L] 继承之前的动态感知与末日销毁协同修复,保障多线程环境下的状态一致性</para>
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class CameraManager : IDisposable, IAsyncDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
#region --- 1. 核心资源与状态 (Fields & States) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> 全局设备实例池(线程安全),Key = 设备唯一标识 </summary>
|
|
|
|
|
|
private readonly ConcurrentDictionary<long, BaseVideoSource> _cameraPool = new();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> 后台协调器实例:负责心跳检测、断线重连、僵尸流恢复 </summary>
|
|
|
|
|
|
private readonly CameraCoordinator _coordinator = new();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> 全局取消令牌源:用于销毁时瞬间关停所有异步扫描任务 </summary>
|
|
|
|
|
|
private readonly CancellationTokenSource _globalCts = new();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary> 销毁状态标记:防止重复销毁或销毁过程中执行操作 </summary>
|
|
|
|
|
|
private volatile bool _isDisposed;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [Fix Bug A: 动态失效] 协调器引擎运行状态标记
|
|
|
|
|
|
/// 使用 volatile 关键字确保多线程环境下的内存可见性,避免指令重排导致的状态不一致
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private volatile bool _isEngineStarted = false;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 2. 设备管理 (Device Management) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 向管理池添加新相机设备
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="config">相机设备配置信息</param>
|
|
|
|
|
|
public void AddDevice(VideoSourceConfig config)
|
|
|
|
|
|
{
|
|
|
|
|
|
// [安全防护] 销毁过程中禁止添加新设备
|
|
|
|
|
|
if (_isDisposed) return;
|
|
|
|
|
|
// 防止重复添加同一设备
|
|
|
|
|
|
if (_cameraPool.ContainsKey(config.Id)) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 根据设备品牌实例化对应的驱动实现类
|
|
|
|
|
|
BaseVideoSource source = config.Brand switch
|
|
|
|
|
|
{
|
|
|
|
|
|
DeviceBrand.HikVision => new HikVideoSource(config),
|
|
|
|
|
|
_ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
|
|
|
|
|
|
if (_isEngineStarted)
|
|
|
|
|
|
{
|
|
|
|
|
|
source.IsRunning = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 将设备注册到内存池与协调器,纳入统一管理
|
|
|
|
|
|
if (_cameraPool.TryAdd(config.Id, source))
|
|
|
|
|
|
{
|
|
|
|
|
|
_coordinator.Register(source);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据设备ID获取指定的视频源实例
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="id">设备唯一标识</param>
|
|
|
|
|
|
/// <returns>视频源实例 / 不存在则返回 null</returns>
|
|
|
|
|
|
public BaseVideoSource? GetDevice(long id)
|
|
|
|
|
|
=> _cameraPool.TryGetValue(id, out var source) ? source : null;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 3. 生命周期控制 (Engine Lifecycle) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 启动视频管理引擎,初始化SDK并启动协调器自愈循环
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task StartAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 防护:已销毁则抛出异常
|
|
|
|
|
|
if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager));
|
|
|
|
|
|
// 防护:避免重复启动
|
|
|
|
|
|
if (_isEngineStarted) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境
|
|
|
|
|
|
HikSdkManager.Initialize();
|
|
|
|
|
|
|
2025-12-26 06:14:55 +08:00
|
|
|
|
//// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程
|
|
|
|
|
|
//foreach (var source in _cameraPool.Values)
|
|
|
|
|
|
//{
|
|
|
|
|
|
// source.IsRunning = true;
|
|
|
|
|
|
//}
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 标记引擎启动状态,后续新增设备自动激活
|
|
|
|
|
|
_isEngineStarted = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 启动协调器后台自愈循环(标记为 LongRunning 提升调度优先级)
|
|
|
|
|
|
_ = Task.Factory.StartNew(
|
|
|
|
|
|
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
|
|
|
|
|
|
_globalCts.Token,
|
|
|
|
|
|
TaskCreationOptions.LongRunning,
|
|
|
|
|
|
TaskScheduler.Default);
|
|
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
|
|
|
|
|
|
await Task.CompletedTask;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前所有相机的全局状态简报
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>包含设备ID、IP、运行状态的元组集合</returns>
|
|
|
|
|
|
public IEnumerable<(long Id, string Ip, VideoSourceStatus Status)> GetGlobalStatus()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _cameraPool.Values.Select(v => (v.Id, v.Config.IpAddress, v.Status));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 4. 监控数据采集 (Telemetry Collection) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取所有相机的健康度报告
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>相机健康度报告集合</returns>
|
|
|
|
|
|
public IEnumerable<CameraHealthReport> GetDetailedTelemetry()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _cameraPool.Values.Select(cam => new CameraHealthReport
|
|
|
|
|
|
{
|
|
|
|
|
|
DeviceId = cam.Id,
|
|
|
|
|
|
Ip = cam.Config.IpAddress,
|
|
|
|
|
|
Status = cam.Status.ToString(),
|
|
|
|
|
|
LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常"
|
|
|
|
|
|
// 扩展:可补充 RealFps/DropFrames/ReconnectCount 等指标
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [新增] 获取全量相机实时遥测数据快照
|
|
|
|
|
|
/// 用于 WebAPI 实时监控大屏展示
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>相机遥测数据快照集合</returns>
|
|
|
|
|
|
public IEnumerable<CameraTelemetryInfo> GetTelemetrySnapshot()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 立即物化列表,防止枚举过程中集合被修改导致异常
|
|
|
|
|
|
return _cameraPool.Values.Select(cam =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 健康度评分算法(示例):基于设备状态与实时帧率综合判定
|
|
|
|
|
|
int healthScore = 100;
|
|
|
|
|
|
if (cam.Status == VideoSourceStatus.Faulted)
|
|
|
|
|
|
healthScore = 0;
|
|
|
|
|
|
else if (cam.Status == VideoSourceStatus.Reconnecting)
|
|
|
|
|
|
healthScore = 60;
|
|
|
|
|
|
else if (cam.RealFps < 1.0 && cam.Status == VideoSourceStatus.Playing)
|
|
|
|
|
|
healthScore = 40; // 有连接状态但无有效流
|
|
|
|
|
|
|
|
|
|
|
|
return new CameraTelemetryInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
DeviceId = cam.Id,
|
|
|
|
|
|
Name = cam.Config.Name,
|
|
|
|
|
|
IpAddress = cam.Config.IpAddress,
|
|
|
|
|
|
Status = cam.Status.ToString(),
|
|
|
|
|
|
IsOnline = cam.IsOnline,
|
|
|
|
|
|
Fps = cam.RealFps,
|
|
|
|
|
|
TotalFrames = cam.TotalFrames,
|
|
|
|
|
|
HealthScore = healthScore,
|
|
|
|
|
|
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
|
|
|
|
|
|
Timestamp = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 5. 资源清理 (Disposal) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [修复 Bug L & Bug γ] 异步执行全局资源清理
|
|
|
|
|
|
/// 严格遵循销毁顺序:停止任务 → 销毁设备 → 卸载SDK,防止非托管内存泄漏
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async ValueTask DisposeAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 防护:避免重复销毁
|
|
|
|
|
|
if (_isDisposed) return;
|
|
|
|
|
|
// 标记为已销毁,禁止后续操作
|
|
|
|
|
|
_isDisposed = true;
|
|
|
|
|
|
_isEngineStarted = false;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 发送全局取消信号,立即停止协调器所有后台扫描任务
|
|
|
|
|
|
_globalCts.Cancel();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. [Fix Bug L] 锁定设备池快照并清空,防止并发修改导致异常
|
|
|
|
|
|
var devices = _cameraPool.Values.ToArray();
|
|
|
|
|
|
_cameraPool.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 并行销毁所有相机设备,释放设备持有的非托管资源
|
|
|
|
|
|
var disposeTasks = devices.Select(async device =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try { await device.DisposeAsync(); }
|
|
|
|
|
|
catch { /* 隔离单个设备销毁异常,不影响其他设备 */ }
|
|
|
|
|
|
});
|
|
|
|
|
|
await Task.WhenAll(disposeTasks);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. [Fix Bug γ: 二次伤害] 彻底卸载全局 SDK 环境
|
|
|
|
|
|
// 加 try-catch 防护极端场景(如进程强制终止时 SDK 已被系统回收)
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
HikSdkManager.Uninitialize();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
// 忽略卸载异常,保证销毁流程正常结束
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
// 释放取消令牌源资源
|
|
|
|
|
|
_globalCts.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|