Files
Ayay/SHH.CameraSdk/Core/Manager/CameraManager.cs

345 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 获取当前管理的所有相机设备
/// </summary>
/// <returns>设备实例集合</returns>
public IEnumerable<BaseVideoSource> GetAllDevices()
{
return _cameraPool.Values.ToList();
}
/// <summary>
/// 从管理池中移除指定设备并释放资源
/// </summary>
/// <param name="id">设备唯一标识</param>
public void RemoveDevice(long id)
{
if (_cameraPool.TryRemove(id, out var device))
{
// 记录日志
System.Console.WriteLine($"[Manager] 正在移除设备 {id}...");
// 1. 停止物理连接 (异步转同步等待,防止资源未释放)
// 在实际高并发场景建议改为 RemoveDeviceAsync
device.StopAsync().GetAwaiter().GetResult();
// 2. 释放资源 (销毁非托管句柄)
device.Dispose();
System.Console.WriteLine($"[Manager] 设备 {id} 已彻底移除");
}
}
#endregion
#region --- 3. (Engine Lifecycle) ---
/// <summary>
/// 启动视频管理引擎初始化SDK并启动协调器自愈循环
/// </summary>
public async Task StartAsync()
{
// 防护:已销毁则抛出异常
if (_isDisposed) throw new System.ObjectDisposedException(nameof(CameraManager));
// 防护:避免重复启动
if (_isEngineStarted) return;
// 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境
HikSdkManager.Initialize();
// 不要运行,手动运行
//// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程
//foreach (var source in _cameraPool.Values)
//{
// source.IsRunning = true;
//}
// 标记引擎启动状态,后续新增设备自动激活
_isEngineStarted = true;
// 3. 启动协调器后台自愈循环(标记为 LongRunning 提升调度优先级)
_ = Task.Factory.StartNew(
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
_globalCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
System.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.IsPhysicalOnline,
Fps = cam.RealFps,
Bitrate = cam.RealBitrate, // [新增] 映射基类属性
TotalFrames = cam.TotalFrames,
HealthScore = healthScore,
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
Timestamp = System.DateTime.Now
};
}).ToList();
}
#endregion
#region --- 5. (Config Hot Update) ---
/// <summary>
/// 智能更新设备配置 (含冷热分离逻辑)
/// </summary>
/// <param name="deviceId">设备唯一标识</param>
/// <param name="dto">配置更新传输对象</param>
/// <exception cref="KeyNotFoundException">设备不存在时抛出</exception>
public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto)
{
if (!_cameraPool.TryGetValue(deviceId, out var device))
throw new KeyNotFoundException($"设备 {deviceId} 不存在");
// 1. 审计
device.AddAuditLog("收到配置更新请求");
// 2. 创建副本进行对比
var oldConfig = device.Config;
var newConfig = oldConfig.DeepCopy();
// 3. 映射 DTO 值 (仅当不为空时修改)
if (dto.IpAddress != null) newConfig.IpAddress = dto.IpAddress;
if (dto.Port != null) newConfig.Port = dto.Port.Value;
if (dto.Username != null) newConfig.Username = dto.Username;
if (dto.Password != null) newConfig.Password = dto.Password;
if (dto.ChannelIndex != null) newConfig.ChannelIndex = dto.ChannelIndex.Value;
if (dto.StreamType != null) newConfig.StreamType = dto.StreamType.Value;
if (dto.Name != null) newConfig.Name = dto.Name;
if (dto.RenderHandle != null) newConfig.RenderHandle = (System.IntPtr)dto.RenderHandle.Value;
// 4. 判定冷热更新
// 核心参数变更 -> 冷重启
bool needColdRestart =
newConfig.IpAddress != oldConfig.IpAddress ||
newConfig.Port != oldConfig.Port ||
newConfig.Username != oldConfig.Username ||
newConfig.Password != oldConfig.Password ||
newConfig.ChannelIndex != oldConfig.ChannelIndex ||
newConfig.Brand != oldConfig.Brand;
if (needColdRestart)
{
device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)");
// 记录之前的运行状态
bool wasRunning = device.IsRunning;
// A. 彻底停止
if (device.IsOnline) await device.StopAsync();
// B. 写入新配置
device.UpdateConfig(newConfig);
// C. 如果之前是运行意图,则自动重启连接
if (wasRunning) await device.StartAsync();
}
else
{
device.AddAuditLog($"检测到运行时参数变更,执行热更新 (HotSwap)");
// A. 更新配置数据
device.UpdateConfig(newConfig);
// B. 在线应用策略 (无需断线)
if (device.IsOnline)
{
var options = new DynamicStreamOptions
{
StreamType = dto.StreamType,
RenderHandle = dto.RenderHandle.HasValue ? (System.IntPtr)dto.RenderHandle : null
};
// 触发驱动层的 OnApplyOptions
device.ApplyOptions(options);
}
}
}
#endregion
#region --- 6. (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
}