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

416 lines
14 KiB
C#
Raw Normal View History

2026-01-16 14:30:42 +08:00
using Ayay.SerilogLogs;
using Serilog;
namespace SHH.CameraSdk;
/// <summary>
/// [管理层] 视频源总控管理器 (V3.5 持久化集成版)
/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈
/// </summary>
public class CameraManager : IDisposable, IAsyncDisposable
{
#region --- 1. (Fields & States) ---
2026-01-16 15:17:23 +08:00
private ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core);
2026-01-16 14:30:42 +08:00
/// <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;
#endregion
#region --- (Constructor) ---
// [修改] 注入 IStorageService
2026-01-16 14:30:42 +08:00
public CameraManager()
{
}
#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();
2026-01-16 14:30:42 +08:00
_sysLog.Warning($"[Core] 设备 ID:{config.Id} 已存在");
_sysLog.Debug($"[Core] 设备 ID:{config.Id} => 明细:" + "{@cfg}.", config);
throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
}
2026-01-17 00:03:16 +08:00
_coordinator.Register(device);
// 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
if (_isEngineStarted)
device.IsRunning = true;
}
/// <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))
{
// 记录日志
2026-01-16 14:30:42 +08:00
_sysLog.Information("[Core] 正在移除设备, ID {0} ", id);
// 1. 停止物理连接
await device.StopAsync();
// 2. 释放资源
await device.DisposeAsync();
2026-01-16 14:30:42 +08:00
_sysLog.Warning("[Core] 设备已彻底移除, ID {0} ", id);
}
}
// 为了兼容旧代码保留同步方法,但不推荐使用
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;
// =========================================================
2026-01-16 14:30:42 +08:00
// 1. 全局驱动环境预初始化
// =========================================================
HikSdkManager.Initialize();
// 标记引擎启动状态
_isEngineStarted = true;
// =========================================================
2026-01-16 14:30:42 +08:00
// 2. 启动协调器后台自愈循环
// =========================================================
_ = 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);
2026-01-16 14:30:42 +08:00
_sysLog.Warning($"[Core] 设备管理引擎启动成功, 当前管理 {_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>
/// 获取全量相机实时遥测数据快照 (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(),
2025-12-26 16:58:12 +08:00
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))
2026-01-16 14:30:42 +08:00
{
_sysLog.Warning($"[Core] 设备更新制作, ID:{deviceId} 不存在.");
throw new KeyNotFoundException($"设备 ID:{deviceId} 不存在.");
}
// 1. 审计
2026-01-16 14:30:42 +08:00
_sysLog.Debug($"[Core] 响应设备配置更新请求, ID:{deviceId}.");
2026-01-17 00:03:16 +08:00
// ============================================================
// 【核心修复:手动解除冷冻】
// [原因] 用户已干预配置,无论之前是否认证失败,都应立即重置标记
// 这样下一次 Coordinator (5秒内) 扫描时,会因为 IsAuthFailed == false
// 且经过了 NormalRetryMs (30s) 而立即尝试拉起。
// ============================================================
device.ResetResilience();
// 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.RtspPath != oldConfig.RtspPath ||
newConfig.RenderHandle != oldConfig.RenderHandle ||
newConfig.Brand != oldConfig.Brand;
if (needColdRestart)
{
2026-01-16 14:30:42 +08:00
_sysLog.Debug($"[Core] 检测到核心参数变更, 执行冷重启, ID:{deviceId}.");
bool wasRunning = device.IsRunning;
// A. 彻底停止
2026-01-17 00:03:16 +08:00
if (device.IsActived) await device.StopAsync();
// B. 写入新配置
device.UpdateConfig(newConfig);
// C. 自动重启
2026-01-17 00:03:16 +08:00
if (wasRunning)
await device.StartAsync();
}
else
{
2026-01-16 14:30:42 +08:00
_sysLog.Debug($"[Core] 检测到运行时参数变更, 执行热更新, ID:{deviceId}.");
// A. 更新配置数据
device.UpdateConfig(newConfig);
// B. 在线应用策略
2026-01-17 00:03:16 +08:00
if (device.IsActived)
{
2026-01-17 13:13:17 +08:00
// Optimized: 仅构造真正发生变化的参数
var options = new DynamicStreamOptions();
// 判定码流是否真的变了(或者 DTO 明确传了新值)
if (dto.StreamType.HasValue && dto.StreamType != oldConfig.StreamType)
{
options.StreamType = dto.StreamType.Value;
}
// 判定句柄是否真的变了
// Modified: 只有当 DTO 的句柄与旧配置不一致时,才放入 options
if (dto.RenderHandle != oldConfig.RenderHandle)
{
options.RenderHandle = (IntPtr)dto.RenderHandle;
}
// 只有当至少有一个参数需要更新时,才调用底层
// 假设 DynamicStreamOptions 内部有检测是否有值的方法,或者判断其属性
if (options.StreamType.HasValue || options.RenderHandle.HasValue)
{
2026-01-17 13:13:17 +08:00
device.ApplyOptions(options);
}
}
}
}
#endregion
#region --- 6. (Disposal) ---
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
2026-01-16 14:30:42 +08:00
/// <summary>
/// 释放资源
/// </summary>
/// <returns></returns>
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) ---
2026-01-16 14:30:42 +08:00
/// <summary>
/// 创建设备实例
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
{
return config.Brand switch
{
DeviceBrand.HikVision => new HikVideoSource(config),
2026-01-16 14:30:42 +08:00
// 使用模式匹配获取不匹配的值,记录详细的 DTO 上下文
_ => HandleUnsupportedBrand(config)
};
}
/// <summary>
2026-01-16 14:30:42 +08:00
/// 处理不支持的设备品牌
/// </summary>
2026-01-16 14:30:42 +08:00
/// <param name="config"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
private BaseVideoSource HandleUnsupportedBrand(VideoSourceConfig config)
{
2026-01-16 14:30:42 +08:00
// 1. 构造错误消息
string errorMsg = $"❌ 不支持的设备品牌: {config.Brand} (ID: {config.Id}, Name: {config.Name})";
// 2. 写入日志 - 建议带上 config 的解构信息,方便排查是否是前端传参错误
_sysLog.Error($"[Core] {errorMsg} | 配置详情: " + "{@Config}", config);
// 3. 抛出异常,阻止程序进入不确定状态
throw new NotSupportedException(errorMsg);
}
#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
}