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

430 lines
17 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);
//}
// 使用工厂方法创建
var device = CreateDeviceInstance(config);
if (!_cameraPool.TryAdd(config.Id, device))
{
// 如果添加失败ID冲突由于 device 还没被使用,直接释放掉
// 这里不需要 await因为刚 new 出来的对象还没 connect
device.DisposeAsync().AsTask().Wait();
throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
}
}
/// <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,
// [新增] 映射分辨率
Width = cam.Width,
Height = cam.Height,
};
}).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
/// <summary>
/// 更新设备配置(热重载)
/// 流程:停止旧设备 -> 释放资源 -> 创建新设备 -> 替换引用 -> (可选)自动重启
/// </summary>
public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig)
{
// 1. 检查设备是否存在
if (!_cameraPool.TryGetValue(id, out var oldDevice))
{
throw new KeyNotFoundException($"设备 #{id} 不存在");
}
// 2. 捕获旧状态(用于决定是否需要自动重启新设备)
// 如果旧设备正在运行或尝试连接中,我们在更新后应该尝试恢复它
bool wasRunning = oldDevice.IsRunning ||
oldDevice.Status == VideoSourceStatus.Playing ||
oldDevice.Status == VideoSourceStatus.Connecting;
// 获取旧的流控需求(如果希望更新配置后,之前的订阅依然生效,需要把需求搬过去)
// 这里简化处理:更新配置通常意味着环境变了,我们选择清空旧订阅,让前端重新下发,或者你可以手动拷贝
// var oldRequirements = oldDevice.Controller.GetCurrentRequirements();
Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中...");
// 3. 【关键步骤】优雅停止并销毁旧实例
// 必须先 Stop 再 Dispose确保 SDK 句柄(如 lUserId, lRealPlayHandle被释放
try
{
await oldDevice.StopAsync(); // 停止取流
await oldDevice.DisposeAsync(); // 注销登录、释放非托管资源
}
catch (Exception ex)
{
Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}");
// 继续执行,不要因为旧设备销毁失败阻断新配置的应用
}
// 4. 使用新配置创建新实例
// 建议将创建逻辑提取为私有方法 CreateDeviceInstance避免与 AddDevice 代码重复
var newDevice = CreateDeviceInstance(newConfig);
// 5. 原子替换 (ConcurrentDictionary 的索引器赋值是线程安全的)
_cameraPool[id] = newDevice;
Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。");
// 6. 状态恢复
if (wasRunning)
{
Console.WriteLine($"[Manager] 检测到设备 #{id} 之前为运行状态,正在自动重启...");
// 不等待 StartAsync 完成,避免阻塞 HTTP 请求太久
// 如果希望前端看到转圈直到启动完成,则加上 await
await newDevice.StartAsync();
}
}
/// <summary>
/// [辅助工厂方法] 根据配置创建具体的驱动实例
/// 请确保你的 AddDevice 方法也改为调用此方法,减少重复代码
/// </summary>
private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
{
return config.Brand switch
{
DeviceBrand.HikVision => new HikVideoSource(config),
// 如果你有大华或其他品牌,在这里扩展
// DeviceBrand.Dahua => new DahuaVideoSource(config),
_ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}")
};
}
}