2025-12-26 03:18:21 +08:00
|
|
|
|
namespace SHH.CameraSdk;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-26 22:24:12 +08:00
|
|
|
|
/// [管理层] 视频源总控管理器 (V3.5 持久化集成版)
|
2025-12-26 03:18:21 +08:00
|
|
|
|
/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈
|
|
|
|
|
|
/// </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>
|
2025-12-26 22:24:12 +08:00
|
|
|
|
/// 协调器引擎运行状态标记
|
|
|
|
|
|
/// 使用 volatile 关键字确保多线程环境下的内存可见性
|
2025-12-26 03:18:21 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private volatile bool _isEngineStarted = false;
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// [新增] 存储服务引用
|
|
|
|
|
|
private readonly IStorageService _storage;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 构造函数 (Constructor) ---
|
|
|
|
|
|
|
|
|
|
|
|
// [修改] 注入 IStorageService
|
|
|
|
|
|
public CameraManager(IStorageService storage)
|
|
|
|
|
|
{
|
|
|
|
|
|
_storage = storage;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 03:18:21 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 2. 设备管理 (Device Management) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 向管理池添加新相机设备
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="config">相机设备配置信息</param>
|
|
|
|
|
|
public void AddDevice(VideoSourceConfig config)
|
|
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// [安全防护] 销毁过程中禁止添加新设备
|
|
|
|
|
|
if (_isDisposed) return;
|
2025-12-26 18:55:04 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用工厂方法创建
|
|
|
|
|
|
var device = CreateDeviceInstance(config);
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
2025-12-26 18:55:04 +08:00
|
|
|
|
if (!_cameraPool.TryAdd(config.Id, device))
|
2025-12-26 03:18:21 +08:00
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
// 如果添加失败(ID冲突),由于 device 还没被使用,直接释放掉
|
|
|
|
|
|
device.DisposeAsync().AsTask().Wait();
|
|
|
|
|
|
throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
2025-12-26 22:24:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
|
|
|
|
|
|
if (_isEngineStarted)
|
|
|
|
|
|
{
|
|
|
|
|
|
device.IsRunning = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// [新增] 自动保存到文件
|
|
|
|
|
|
SaveChanges();
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据设备ID获取指定的视频源实例
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public BaseVideoSource? GetDevice(long id)
|
|
|
|
|
|
=> _cameraPool.TryGetValue(id, out var source) ? source : null;
|
|
|
|
|
|
|
2025-12-26 12:15:10 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前管理的所有相机设备
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public IEnumerable<BaseVideoSource> GetAllDevices()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _cameraPool.Values.ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-26 22:24:12 +08:00
|
|
|
|
/// [修改] 异步移除设备 (从 RemoveDevice 改为 RemoveDeviceAsync)
|
2025-12-26 12:15:10 +08:00
|
|
|
|
/// </summary>
|
2025-12-26 22:24:12 +08:00
|
|
|
|
public async Task RemoveDeviceAsync(long id)
|
2025-12-26 12:15:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (_cameraPool.TryRemove(id, out var device))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 记录日志
|
2025-12-26 22:24:12 +08:00
|
|
|
|
Console.WriteLine($"[Manager] 正在移除设备 {id}...");
|
2025-12-26 12:15:10 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 1. 停止物理连接
|
|
|
|
|
|
await device.StopAsync();
|
2025-12-26 12:15:10 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 2. 释放资源
|
|
|
|
|
|
await device.DisposeAsync();
|
2025-12-26 12:15:10 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
Console.WriteLine($"[Manager] 设备 {id} 已彻底移除");
|
|
|
|
|
|
|
|
|
|
|
|
// [新增] 自动保存到文件
|
|
|
|
|
|
SaveChanges();
|
2025-12-26 12:15:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 为了兼容旧代码保留同步方法,但不推荐使用
|
|
|
|
|
|
public void RemoveDevice(long id) => RemoveDeviceAsync(id).Wait();
|
|
|
|
|
|
|
2025-12-26 03:18:21 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 3. 生命周期控制 (Engine Lifecycle) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-26 22:24:12 +08:00
|
|
|
|
/// 启动视频管理引擎,加载配置,初始化SDK并启动协调器自愈循环
|
2025-12-26 03:18:21 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task StartAsync()
|
|
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager));
|
2025-12-26 03:18:21 +08:00
|
|
|
|
if (_isEngineStarted) return;
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 1. [新增] 从文件加载设备配置
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine("[Manager] 正在检查本地配置文件...");
|
|
|
|
|
|
var savedConfigs = await _storage.LoadDevicesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
int loadedCount = 0;
|
|
|
|
|
|
foreach (var config in savedConfigs)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 防止ID冲突(虽然文件里理论上不重复)
|
|
|
|
|
|
if (!_cameraPool.ContainsKey(config.Id))
|
|
|
|
|
|
{
|
|
|
|
|
|
var device = CreateDeviceInstance(config);
|
|
|
|
|
|
// 默认设为运行状态,让协调器稍后去连接
|
2025-12-26 23:05:08 +08:00
|
|
|
|
//device.IsRunning = true;
|
2025-12-26 22:24:12 +08:00
|
|
|
|
_cameraPool.TryAdd(config.Id, device);
|
|
|
|
|
|
loadedCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (loadedCount > 0)
|
|
|
|
|
|
Console.WriteLine($"[Manager] 已从文件恢复 {loadedCount} 台设备配置");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"[Manager] 加载配置文件警告: {ex.Message}");
|
|
|
|
|
|
}
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 2. 全局驱动环境预初始化
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
HikSdkManager.Initialize();
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 标记引擎启动状态
|
2025-12-26 03:18:21 +08:00
|
|
|
|
_isEngineStarted = true;
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// =========================================================
|
|
|
|
|
|
// 3. 启动协调器后台自愈循环
|
|
|
|
|
|
// =========================================================
|
2025-12-26 03:18:21 +08:00
|
|
|
|
_ = Task.Factory.StartNew(
|
|
|
|
|
|
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
|
|
|
|
|
|
_globalCts.Token,
|
|
|
|
|
|
TaskCreationOptions.LongRunning,
|
|
|
|
|
|
TaskScheduler.Default);
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 这里传递 _cameraPool 给协调器(如果协调器是独立引用的,可能需要 Register 逻辑,
|
|
|
|
|
|
// 但根据您的代码,协调器似乎是依赖外部注册或共享引用的。
|
|
|
|
|
|
// *注意*:如果 Coordinator 需要显式注册,请在这里补上:
|
|
|
|
|
|
foreach (var dev in _cameraPool.Values) _coordinator.Register(dev);
|
|
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
|
2025-12-26 03:18:21 +08:00
|
|
|
|
await Task.CompletedTask;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前所有相机的全局状态简报
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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>
|
|
|
|
|
|
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 ? "设备故障或网络中断" : "运行正常"
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-26 22:24:12 +08:00
|
|
|
|
/// 获取全量相机实时遥测数据快照 (MonitorController 使用)
|
2025-12-26 03:18:21 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public IEnumerable<CameraTelemetryInfo> GetTelemetrySnapshot()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _cameraPool.Values.Select(cam =>
|
|
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 健康度评分算法
|
2025-12-26 03:18:21 +08:00
|
|
|
|
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)
|
2025-12-26 22:24:12 +08:00
|
|
|
|
healthScore = 40;
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
|
|
|
|
|
return new CameraTelemetryInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
DeviceId = cam.Id,
|
|
|
|
|
|
Name = cam.Config.Name,
|
|
|
|
|
|
IpAddress = cam.Config.IpAddress,
|
|
|
|
|
|
Status = cam.Status.ToString(),
|
2025-12-26 16:58:12 +08:00
|
|
|
|
IsOnline = cam.IsPhysicalOnline,
|
2025-12-26 03:18:21 +08:00
|
|
|
|
Fps = cam.RealFps,
|
2025-12-26 22:24:12 +08:00
|
|
|
|
Bitrate = cam.RealBitrate,
|
2025-12-26 03:18:21 +08:00
|
|
|
|
TotalFrames = cam.TotalFrames,
|
|
|
|
|
|
HealthScore = healthScore,
|
|
|
|
|
|
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
|
2025-12-26 22:24:12 +08:00
|
|
|
|
Timestamp = DateTime.Now,
|
2025-12-26 18:55:04 +08:00
|
|
|
|
Width = cam.Width,
|
|
|
|
|
|
Height = cam.Height,
|
2025-12-26 03:18:21 +08:00
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2025-12-26 12:15:10 +08:00
|
|
|
|
#region --- 5. 配置热更新 (Config Hot Update) ---
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 智能更新设备配置 (含冷热分离逻辑)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// 3. 映射 DTO 值
|
2025-12-26 12:15:10 +08:00
|
|
|
|
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;
|
2025-12-26 22:24:12 +08:00
|
|
|
|
if (dto.RenderHandle != null) newConfig.RenderHandle = (IntPtr)dto.RenderHandle.Value;
|
|
|
|
|
|
if (dto.Brand != null) newConfig.Brand = (DeviceBrand)dto.Brand;
|
2025-12-26 12:15:10 +08:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// C. 自动重启
|
2025-12-26 12:15:10 +08:00
|
|
|
|
if (wasRunning) await device.StartAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
device.AddAuditLog($"检测到运行时参数变更,执行热更新 (HotSwap)");
|
|
|
|
|
|
|
|
|
|
|
|
// A. 更新配置数据
|
|
|
|
|
|
device.UpdateConfig(newConfig);
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
// B. 在线应用策略
|
2025-12-26 12:15:10 +08:00
|
|
|
|
if (device.IsOnline)
|
|
|
|
|
|
{
|
|
|
|
|
|
var options = new DynamicStreamOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
StreamType = dto.StreamType,
|
2025-12-26 22:24:12 +08:00
|
|
|
|
RenderHandle = dto.RenderHandle.HasValue ? (IntPtr)dto.RenderHandle : null
|
2025-12-26 12:15:10 +08:00
|
|
|
|
};
|
|
|
|
|
|
device.ApplyOptions(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-26 22:24:12 +08:00
|
|
|
|
|
|
|
|
|
|
// [新增] 保存文件
|
|
|
|
|
|
SaveChanges();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 全量替换更新 (兼容接口)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_cameraPool.TryGetValue(id, out var oldDevice))
|
|
|
|
|
|
throw new KeyNotFoundException($"设备 #{id} 不存在");
|
|
|
|
|
|
|
|
|
|
|
|
bool wasRunning = oldDevice.IsRunning ||
|
|
|
|
|
|
oldDevice.Status == VideoSourceStatus.Playing ||
|
|
|
|
|
|
oldDevice.Status == VideoSourceStatus.Connecting;
|
|
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中...");
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await oldDevice.StopAsync();
|
|
|
|
|
|
await oldDevice.DisposeAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var newDevice = CreateDeviceInstance(newConfig);
|
|
|
|
|
|
_cameraPool[id] = newDevice;
|
|
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。");
|
|
|
|
|
|
|
|
|
|
|
|
if (wasRunning)
|
|
|
|
|
|
{
|
|
|
|
|
|
await newDevice.StartAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// [新增] 保存文件
|
|
|
|
|
|
SaveChanges();
|
2025-12-26 12:15:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region --- 6. 资源清理 (Disposal) ---
|
2025-12-26 03:18:21 +08:00
|
|
|
|
|
|
|
|
|
|
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
|
|
|
|
|
|
|
|
|
|
|
|
public async ValueTask DisposeAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_isDisposed) return;
|
|
|
|
|
|
_isDisposed = true;
|
|
|
|
|
|
_isEngineStarted = false;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
_globalCts.Cancel();
|
|
|
|
|
|
|
|
|
|
|
|
var devices = _cameraPool.Values.ToArray();
|
|
|
|
|
|
_cameraPool.Clear();
|
|
|
|
|
|
|
|
|
|
|
|
var disposeTasks = devices.Select(async device =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try { await device.DisposeAsync(); }
|
2025-12-26 22:24:12 +08:00
|
|
|
|
catch { }
|
2025-12-26 03:18:21 +08:00
|
|
|
|
});
|
|
|
|
|
|
await Task.WhenAll(disposeTasks);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
HikSdkManager.Uninitialize();
|
|
|
|
|
|
}
|
2025-12-26 22:24:12 +08:00
|
|
|
|
catch { }
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_globalCts.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
2025-12-26 18:55:04 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
#region --- 7. 内部辅助 (Helpers) ---
|
|
|
|
|
|
|
|
|
|
|
|
private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
|
2025-12-26 18:55:04 +08:00
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
return config.Brand switch
|
2025-12-26 18:55:04 +08:00
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
DeviceBrand.HikVision => new HikVideoSource(config),
|
|
|
|
|
|
_ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}")
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-12-26 18:55:04 +08:00
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [新增] 触发异步保存 (Fire-and-Forget)
|
|
|
|
|
|
/// 不阻塞当前 API 线程,让后台存储服务去排队写入
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void SaveChanges()
|
|
|
|
|
|
{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
var allConfigs = _cameraPool.Values.Select(d => d.Config).ToList();
|
|
|
|
|
|
// 异步调用存储服务,不使用 await 以免阻塞 API 响应
|
|
|
|
|
|
_ = _storage.SaveDevicesAsync(allConfigs);
|
2025-12-26 18:55:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-12-26 22:24:12 +08:00
|
|
|
|
Console.WriteLine($"[Manager] 触发保存失败: {ex.Message}");
|
2025-12-26 18:55:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 22:24:12 +08:00
|
|
|
|
#endregion
|
2025-12-26 03:18:21 +08:00
|
|
|
|
}
|