namespace SHH.CameraSdk; /// /// [管理层] 视频源总控管理器 (V3.3.1 修复版) /// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈 /// 核心修复: /// 1. [Bug γ] 二次伤害:强化销毁流程,防止 Dispose 阶段因 GC 乱序导致的非托管内存非法访问 /// 2. [Bug A/L] 继承之前的动态感知与末日销毁协同修复,保障多线程环境下的状态一致性 /// public class CameraManager : IDisposable, IAsyncDisposable { #region --- 1. 核心资源与状态 (Fields & States) --- /// 全局设备实例池(线程安全),Key = 设备唯一标识 private readonly ConcurrentDictionary _cameraPool = new(); /// 后台协调器实例:负责心跳检测、断线重连、僵尸流恢复 private readonly CameraCoordinator _coordinator = new(); /// 全局取消令牌源:用于销毁时瞬间关停所有异步扫描任务 private readonly CancellationTokenSource _globalCts = new(); /// 销毁状态标记:防止重复销毁或销毁过程中执行操作 private volatile bool _isDisposed; /// /// [Fix Bug A: 动态失效] 协调器引擎运行状态标记 /// 使用 volatile 关键字确保多线程环境下的内存可见性,避免指令重排导致的状态不一致 /// private volatile bool _isEngineStarted = false; #endregion #region --- 2. 设备管理 (Device Management) --- /// /// 向管理池添加新相机设备 /// /// 相机设备配置信息 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} 已存在"); } } /// /// 根据设备ID获取指定的视频源实例 /// /// 设备唯一标识 /// 视频源实例 / 不存在则返回 null public BaseVideoSource? GetDevice(long id) => _cameraPool.TryGetValue(id, out var source) ? source : null; /// /// 获取当前管理的所有相机设备 /// /// 设备实例集合 public IEnumerable GetAllDevices() { return _cameraPool.Values.ToList(); } /// /// 从管理池中移除指定设备并释放资源 /// /// 设备唯一标识 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) --- /// /// 启动视频管理引擎,初始化SDK并启动协调器自愈循环 /// 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; } /// /// 获取当前所有相机的全局状态简报 /// /// 包含设备ID、IP、运行状态的元组集合 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) --- /// /// 获取所有相机的健康度报告 /// /// 相机健康度报告集合 public IEnumerable 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 等指标 }); } /// /// [新增] 获取全量相机实时遥测数据快照 /// 用于 WebAPI 实时监控大屏展示 /// /// 相机遥测数据快照集合 public IEnumerable 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) --- /// /// 智能更新设备配置 (含冷热分离逻辑) /// /// 设备唯一标识 /// 配置更新传输对象 /// 设备不存在时抛出 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) --- /// /// 同步销毁:内部调用异步销毁逻辑,等待销毁完成 /// public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// /// [修复 Bug L & Bug γ] 异步执行全局资源清理 /// 严格遵循销毁顺序:停止任务 → 销毁设备 → 卸载SDK,防止非托管内存泄漏 /// 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 /// /// 更新设备配置(热重载) /// 流程:停止旧设备 -> 释放资源 -> 创建新设备 -> 替换引用 -> (可选)自动重启 /// 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(); } } /// /// [辅助工厂方法] 根据配置创建具体的驱动实例 /// 请确保你的 AddDevice 方法也改为调用此方法,减少重复代码 /// private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config) { return config.Brand switch { DeviceBrand.HikVision => new HikVideoSource(config), // 如果你有大华或其他品牌,在这里扩展 // DeviceBrand.Dahua => new DahuaVideoSource(config), _ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}") }; } }