Files
Ayay/SHH.CameraSdk/Core/Manager/CameraManager.cs
wilson 3351ae739e 在 AiVideo 中能看到图像
增加了在线状态同步逻辑
2026-01-09 12:30:36 +08:00

461 lines
15 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.5 持久化集成版)
/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈
/// </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>
/// 协调器引擎运行状态标记
/// 使用 volatile 关键字确保多线程环境下的内存可见性
/// </summary>
private volatile bool _isEngineStarted = false;
// [新增] 存储服务引用
private readonly IStorageService _storage;
#endregion
#region --- (Constructor) ---
// [修改] 注入 IStorageService
public CameraManager(IStorageService storage)
{
_storage = storage;
}
#endregion
#region --- 2. (Device Management) ---
/// <summary>
/// 向管理池添加新相机设备
/// </summary>
/// <param name="config">相机设备配置信息</param>
public void AddDevice(VideoSourceConfig config)
{
// [安全防护] 销毁过程中禁止添加新设备
if (_isDisposed) return;
// 使用工厂方法创建
var device = CreateDeviceInstance(config);
if (!_cameraPool.TryAdd(config.Id, device))
{
// 如果添加失败ID冲突由于 device 还没被使用,直接释放掉
device.DisposeAsync().AsTask().Wait();
throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
}
// 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
if (_isEngineStarted)
{
device.IsRunning = true;
}
// [新增] 自动保存到文件
SaveChanges();
}
/// <summary>
/// 根据设备ID获取指定的视频源实例
/// </summary>
public BaseVideoSource? GetDevice(long id)
=> _cameraPool.TryGetValue(id, out var source) ? source : null;
/// <summary>
/// 获取当前管理的所有相机设备
/// </summary>
public IEnumerable<BaseVideoSource> GetAllDevices()
{
return _cameraPool.Values.ToList();
}
/// <summary>
/// [修改] 异步移除设备 (从 RemoveDevice 改为 RemoveDeviceAsync)
/// </summary>
public async Task RemoveDeviceAsync(long id)
{
if (_cameraPool.TryRemove(id, out var device))
{
// 记录日志
Console.WriteLine($"[Manager] 正在移除设备 {id}...");
// 1. 停止物理连接
await device.StopAsync();
// 2. 释放资源
await device.DisposeAsync();
Console.WriteLine($"[Manager] 设备 {id} 已彻底移除");
// [新增] 自动保存到文件
SaveChanges();
}
}
// 为了兼容旧代码保留同步方法,但不推荐使用
public void RemoveDevice(long id) => RemoveDeviceAsync(id).Wait();
#endregion
#region --- 3. (Engine Lifecycle) ---
/// <summary>
/// 启动视频管理引擎加载配置初始化SDK并启动协调器自愈循环
/// </summary>
public async Task StartAsync()
{
if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager));
if (_isEngineStarted) return;
// =========================================================
// 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);
// 默认设为运行状态,让协调器稍后去连接
//device.IsRunning = true;
_cameraPool.TryAdd(config.Id, device);
loadedCount++;
}
}
if (loadedCount > 0)
Console.WriteLine($"[Manager] 已从文件恢复 {loadedCount} 台设备配置");
}
catch (Exception ex)
{
Console.WriteLine($"[Manager] 加载配置文件警告: {ex.Message}");
}
// =========================================================
// 2. 全局驱动环境预初始化
// =========================================================
HikSdkManager.Initialize();
// 标记引擎启动状态
_isEngineStarted = true;
// =========================================================
// 3. 启动协调器后台自愈循环
// =========================================================
_ = Task.Factory.StartNew(
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
_globalCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
// 这里传递 _cameraPool 给协调器(如果协调器是独立引用的,可能需要 Register 逻辑,
// 但根据您的代码,协调器似乎是依赖外部注册或共享引用的。
// *注意*:如果 Coordinator 需要显式注册,请在这里补上:
foreach (var dev in _cameraPool.Values) _coordinator.Register(dev);
Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
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>
/// 获取全量相机实时遥测数据快照 (MonitorController 使用)
/// </summary>
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 = DateTime.Now,
Width = cam.Width,
Height = cam.Height,
};
}).ToList();
}
#endregion
#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();
// 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.Brand != null) newConfig.Brand = (DeviceBrand)dto.Brand;
newConfig.RtspPath = dto.RtspPath;
newConfig.MainboardIp = dto.MainboardIp;
newConfig.MainboardPort = dto.MainboardPort;
newConfig.RenderHandle = dto.RenderHandle;
// 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 ?? newConfig.StreamType,
RenderHandle = (IntPtr)dto.RenderHandle
};
device.ApplyOptions(options);
}
}
// [新增] 保存文件
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();
}
#endregion
#region --- 6. (Disposal) ---
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(); }
catch { }
});
await Task.WhenAll(disposeTasks);
try
{
HikSdkManager.Uninitialize();
}
catch { }
}
finally
{
_globalCts.Dispose();
}
}
#endregion
#region --- 7. (Helpers) ---
private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
{
return config.Brand switch
{
DeviceBrand.HikVision => new HikVideoSource(config),
_ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}")
};
}
/// <summary>
/// [新增] 触发异步保存 (Fire-and-Forget)
/// 不阻塞当前 API 线程,让后台存储服务去排队写入
/// </summary>
private void SaveChanges()
{
try
{
var allConfigs = _cameraPool.Values.Select(d => d.Config).ToList();
// 异步调用存储服务,不使用 await 以免阻塞 API 响应
_ = _storage.SaveDevicesAsync(allConfigs);
}
catch (Exception ex)
{
Console.WriteLine($"[Manager] 触发保存失败: {ex.Message}");
}
}
#endregion
/// <summary>
/// [新增] 获取当前管理的所有相机设备(兼容网络引擎接口)
/// </summary>
public IEnumerable<BaseVideoSource> GetAllCameras()
{
// 复用现有的 GetAllDevices 逻辑
return GetAllDevices();
}
#region --- [] 线 (SDK ) ---
/// <summary>
/// 当设备在线/离线状态发生变更时触发
/// <para>参数1: DeviceId</para>
/// <para>参数2: IsOnline (true=在线, false=离线)</para>
/// <para>参数3: Reason (变更原因)</para>
/// </summary>
public event Action<long, bool, string>? OnDeviceStatusChanged;
/// <summary>
/// [内部方法] 供 Sentinel 调用,触发事件冒泡
/// </summary>
internal void NotifyStatusChange(long deviceId, bool isOnline, string reason)
{
// 仅仅是触发 C# 事件,完全不知道网络发送的存在
OnDeviceStatusChanged?.Invoke(deviceId, isOnline, reason);
}
#endregion
}