增加摄像头列表增、改、删的本地化存储支持

This commit is contained in:
2025-12-26 22:24:12 +08:00
parent 71856b483e
commit 3d4eb34ca9
5 changed files with 306 additions and 234 deletions

View File

@@ -2,17 +2,27 @@
public interface IStorageService public interface IStorageService
{ {
// 1. 基础属性:让外界知道当前是几号进程 // 1. 基础属性
int ProcessId { get; } int ProcessId { get; }
// 2. 设备配置相关的空架子 // 2. 设备配置管理
Task SaveDevicesAsync(object configs); // 这里先用 object 占位,或者用您的 List<VideoSourceConfig> // 保存:接收 VideoSourceConfig 集合
Task SaveDevicesAsync(IEnumerable<VideoSourceConfig> configs);
Task<object> LoadDevicesAsync(); // 加载:返回 VideoSourceConfig 列表
Task<List<VideoSourceConfig>> LoadDevicesAsync();
// 3. 系统日志相关的空架子 // 3. 系统日志
// 记录系统操作 (如 POST /api/cameras)
Task AppendSystemLogAsync(string action, string ip, string path); Task AppendSystemLogAsync(string action, string ip, string path);
// 4. 设备审计日志相关的空架子 // 获取系统日志
Task AppendDeviceLogAsync(long deviceId, string message); Task<List<string>> GetSystemLogsAsync(int count);
// 4. 设备审计日志
// 记录单设备日志 (统一使用 int deviceId)
Task AppendDeviceLogAsync(int deviceId, string message);
// 获取单设备日志
Task<List<string>> GetDeviceLogsAsync(int deviceId, int count);
} }

View File

@@ -35,6 +35,7 @@ public class VideoSourceConfig
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
/// <summary> 渲染句柄(可选):用于硬解码时直接绑定显示窗口,提升渲染性能 </summary> /// <summary> 渲染句柄(可选):用于硬解码时直接绑定显示窗口,提升渲染性能 </summary>
[JsonIgnore]
public IntPtr RenderHandle { get; set; } = IntPtr.Zero; public IntPtr RenderHandle { get; set; } = IntPtr.Zero;
/// <summary> 物理通道号IPC 通常为 1NVR 对应接入的摄像头通道索引) </summary> /// <summary> 物理通道号IPC 通常为 1NVR 对应接入的摄像头通道索引) </summary>

View File

@@ -1,11 +1,8 @@
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// [管理层] 视频源总控管理器 (V3.3.1 修复版) /// [管理层] 视频源总控管理器 (V3.5 持久化集成版)
/// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈 /// 核心职责:统一管理所有相机设备的生命周期、状态监控与资源清理,对接协调器实现自动自愈
/// 核心修复:
/// <para>1. [Bug γ] 二次伤害:强化销毁流程,防止 Dispose 阶段因 GC 乱序导致的非托管内存非法访问</para>
/// <para>2. [Bug A/L] 继承之前的动态感知与末日销毁协同修复,保障多线程环境下的状态一致性</para>
/// </summary> /// </summary>
public class CameraManager : IDisposable, IAsyncDisposable public class CameraManager : IDisposable, IAsyncDisposable
{ {
@@ -24,11 +21,24 @@ public class CameraManager : IDisposable, IAsyncDisposable
private volatile bool _isDisposed; private volatile bool _isDisposed;
/// <summary> /// <summary>
/// [Fix Bug A: 动态失效] 协调器引擎运行状态标记 /// 协调器引擎运行状态标记
/// 使用 volatile 关键字确保多线程环境下的内存可见性,避免指令重排导致的状态不一致 /// 使用 volatile 关键字确保多线程环境下的内存可见性
/// </summary> /// </summary>
private volatile bool _isEngineStarted = false; private volatile bool _isEngineStarted = false;
// [新增] 存储服务引用
private readonly IStorageService _storage;
#endregion
#region --- (Constructor) ---
// [修改] 注入 IStorageService
public CameraManager(IStorageService storage)
{
_storage = storage;
}
#endregion #endregion
#region --- 2. (Device Management) --- #region --- 2. (Device Management) ---
@@ -39,29 +49,8 @@ public class CameraManager : IDisposable, IAsyncDisposable
/// <param name="config">相机设备配置信息</param> /// <param name="config">相机设备配置信息</param>
public void AddDevice(VideoSourceConfig config) public void AddDevice(VideoSourceConfig config)
{ {
//// [安全防护] 销毁过程中禁止添加新设备 // [安全防护] 销毁过程中禁止添加新设备
//if (_isDisposed) return; 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); var device = CreateDeviceInstance(config);
@@ -69,93 +58,130 @@ public class CameraManager : IDisposable, IAsyncDisposable
if (!_cameraPool.TryAdd(config.Id, device)) if (!_cameraPool.TryAdd(config.Id, device))
{ {
// 如果添加失败ID冲突由于 device 还没被使用,直接释放掉 // 如果添加失败ID冲突由于 device 还没被使用,直接释放掉
// 这里不需要 await因为刚 new 出来的对象还没 connect
device.DisposeAsync().AsTask().Wait(); device.DisposeAsync().AsTask().Wait();
throw new InvalidOperationException($"设备 ID {config.Id} 已存在"); throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
} }
// 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
if (_isEngineStarted)
{
device.IsRunning = true;
}
// [新增] 自动保存到文件
SaveChanges();
} }
/// <summary> /// <summary>
/// 根据设备ID获取指定的视频源实例 /// 根据设备ID获取指定的视频源实例
/// </summary> /// </summary>
/// <param name="id">设备唯一标识</param>
/// <returns>视频源实例 / 不存在则返回 null</returns>
public BaseVideoSource? GetDevice(long id) public BaseVideoSource? GetDevice(long id)
=> _cameraPool.TryGetValue(id, out var source) ? source : null; => _cameraPool.TryGetValue(id, out var source) ? source : null;
/// <summary> /// <summary>
/// 获取当前管理的所有相机设备 /// 获取当前管理的所有相机设备
/// </summary> /// </summary>
/// <returns>设备实例集合</returns>
public IEnumerable<BaseVideoSource> GetAllDevices() public IEnumerable<BaseVideoSource> GetAllDevices()
{ {
return _cameraPool.Values.ToList(); return _cameraPool.Values.ToList();
} }
/// <summary> /// <summary>
/// 从管理池中移除指定设备并释放资源 /// [修改] 异步移除设备 (从 RemoveDevice 改为 RemoveDeviceAsync)
/// </summary> /// </summary>
/// <param name="id">设备唯一标识</param> public async Task RemoveDeviceAsync(long id)
public void RemoveDevice(long id)
{ {
if (_cameraPool.TryRemove(id, out var device)) if (_cameraPool.TryRemove(id, out var device))
{ {
// 记录日志 // 记录日志
System.Console.WriteLine($"[Manager] 正在移除设备 {id}..."); Console.WriteLine($"[Manager] 正在移除设备 {id}...");
// 1. 停止物理连接 (异步转同步等待,防止资源未释放) // 1. 停止物理连接
// 在实际高并发场景建议改为 RemoveDeviceAsync await device.StopAsync();
device.StopAsync().GetAwaiter().GetResult();
// 2. 释放资源 (销毁非托管句柄) // 2. 释放资源
device.Dispose(); await device.DisposeAsync();
System.Console.WriteLine($"[Manager] 设备 {id} 已彻底移除"); Console.WriteLine($"[Manager] 设备 {id} 已彻底移除");
// [新增] 自动保存到文件
SaveChanges();
} }
} }
// 为了兼容旧代码保留同步方法,但不推荐使用
public void RemoveDevice(long id) => RemoveDeviceAsync(id).Wait();
#endregion #endregion
#region --- 3. (Engine Lifecycle) --- #region --- 3. (Engine Lifecycle) ---
/// <summary> /// <summary>
/// 启动视频管理引擎初始化SDK并启动协调器自愈循环 /// 启动视频管理引擎,加载配置,初始化SDK并启动协调器自愈循环
/// </summary> /// </summary>
public async Task StartAsync() public async Task StartAsync()
{ {
// 防护:已销毁则抛出异常 if (_isDisposed) throw new ObjectDisposedException(nameof(CameraManager));
if (_isDisposed) throw new System.ObjectDisposedException(nameof(CameraManager));
// 防护:避免重复启动
if (_isEngineStarted) return; if (_isEngineStarted) return;
// 1. 全局驱动环境预初始化:初始化厂商 SDK 运行环境 // =========================================================
// 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(); HikSdkManager.Initialize();
// 不要运行,手动运行 // 标记引擎启动状态
//// 2. 激活现有设备池中所有设备的“运行意图”,触发设备连接流程
//foreach (var source in _cameraPool.Values)
//{
// source.IsRunning = true;
//}
// 标记引擎启动状态,后续新增设备自动激活
_isEngineStarted = true; _isEngineStarted = true;
// 3. 启动协调器后台自愈循环(标记为 LongRunning 提升调度优先级) // =========================================================
// 3. 启动协调器后台自愈循环
// =========================================================
_ = Task.Factory.StartNew( _ = Task.Factory.StartNew(
() => _coordinator.RunCoordinationLoopAsync(_globalCts.Token), () => _coordinator.RunCoordinationLoopAsync(_globalCts.Token),
_globalCts.Token, _globalCts.Token,
TaskCreationOptions.LongRunning, TaskCreationOptions.LongRunning,
TaskScheduler.Default); TaskScheduler.Default);
System.Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。"); // 这里传递 _cameraPool 给协调器(如果协调器是独立引用的,可能需要 Register 逻辑,
// 但根据您的代码,协调器似乎是依赖外部注册或共享引用的。
// *注意*:如果 Coordinator 需要显式注册,请在这里补上:
foreach (var dev in _cameraPool.Values) _coordinator.Register(dev);
Console.WriteLine($"[CameraManager] 引擎启动成功,当前管理 {_cameraPool.Count} 路相机设备。");
await Task.CompletedTask; await Task.CompletedTask;
} }
/// <summary> /// <summary>
/// 获取当前所有相机的全局状态简报 /// 获取当前所有相机的全局状态简报
/// </summary> /// </summary>
/// <returns>包含设备ID、IP、运行状态的元组集合</returns>
public IEnumerable<(long Id, string Ip, VideoSourceStatus Status)> GetGlobalStatus() public IEnumerable<(long Id, string Ip, VideoSourceStatus Status)> GetGlobalStatus()
{ {
return _cameraPool.Values.Select(v => (v.Id, v.Config.IpAddress, v.Status)); return _cameraPool.Values.Select(v => (v.Id, v.Config.IpAddress, v.Status));
@@ -168,7 +194,6 @@ public class CameraManager : IDisposable, IAsyncDisposable
/// <summary> /// <summary>
/// 获取所有相机的健康度报告 /// 获取所有相机的健康度报告
/// </summary> /// </summary>
/// <returns>相机健康度报告集合</returns>
public IEnumerable<CameraHealthReport> GetDetailedTelemetry() public IEnumerable<CameraHealthReport> GetDetailedTelemetry()
{ {
return _cameraPool.Values.Select(cam => new CameraHealthReport return _cameraPool.Values.Select(cam => new CameraHealthReport
@@ -177,28 +202,24 @@ public class CameraManager : IDisposable, IAsyncDisposable
Ip = cam.Config.IpAddress, Ip = cam.Config.IpAddress,
Status = cam.Status.ToString(), Status = cam.Status.ToString(),
LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常" LastError = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : "运行正常"
// 扩展:可补充 RealFps/DropFrames/ReconnectCount 等指标
}); });
} }
/// <summary> /// <summary>
/// [新增] 获取全量相机实时遥测数据快照 /// 获取全量相机实时遥测数据快照 (MonitorController 使用)
/// 用于 WebAPI 实时监控大屏展示
/// </summary> /// </summary>
/// <returns>相机遥测数据快照集合</returns>
public IEnumerable<CameraTelemetryInfo> GetTelemetrySnapshot() public IEnumerable<CameraTelemetryInfo> GetTelemetrySnapshot()
{ {
// 立即物化列表,防止枚举过程中集合被修改导致异常
return _cameraPool.Values.Select(cam => return _cameraPool.Values.Select(cam =>
{ {
// 健康度评分算法(示例):基于设备状态与实时帧率综合判定 // 健康度评分算法
int healthScore = 100; int healthScore = 100;
if (cam.Status == VideoSourceStatus.Faulted) if (cam.Status == VideoSourceStatus.Faulted)
healthScore = 0; healthScore = 0;
else if (cam.Status == VideoSourceStatus.Reconnecting) else if (cam.Status == VideoSourceStatus.Reconnecting)
healthScore = 60; healthScore = 60;
else if (cam.RealFps < 1.0 && cam.Status == VideoSourceStatus.Playing) else if (cam.RealFps < 1.0 && cam.Status == VideoSourceStatus.Playing)
healthScore = 40; // 有连接状态但无有效流 healthScore = 40;
return new CameraTelemetryInfo return new CameraTelemetryInfo
{ {
@@ -208,12 +229,11 @@ public class CameraManager : IDisposable, IAsyncDisposable
Status = cam.Status.ToString(), Status = cam.Status.ToString(),
IsOnline = cam.IsPhysicalOnline, IsOnline = cam.IsPhysicalOnline,
Fps = cam.RealFps, Fps = cam.RealFps,
Bitrate = cam.RealBitrate, // [新增] 映射基类属性 Bitrate = cam.RealBitrate,
TotalFrames = cam.TotalFrames, TotalFrames = cam.TotalFrames,
HealthScore = healthScore, HealthScore = healthScore,
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null, LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
Timestamp = System.DateTime.Now, Timestamp = DateTime.Now,
// [新增] 映射分辨率
Width = cam.Width, Width = cam.Width,
Height = cam.Height, Height = cam.Height,
}; };
@@ -227,9 +247,6 @@ public class CameraManager : IDisposable, IAsyncDisposable
/// <summary> /// <summary>
/// 智能更新设备配置 (含冷热分离逻辑) /// 智能更新设备配置 (含冷热分离逻辑)
/// </summary> /// </summary>
/// <param name="deviceId">设备唯一标识</param>
/// <param name="dto">配置更新传输对象</param>
/// <exception cref="KeyNotFoundException">设备不存在时抛出</exception>
public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto) public async Task UpdateDeviceConfigAsync(long deviceId, DeviceUpdateDto dto)
{ {
if (!_cameraPool.TryGetValue(deviceId, out var device)) if (!_cameraPool.TryGetValue(deviceId, out var device))
@@ -242,7 +259,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
var oldConfig = device.Config; var oldConfig = device.Config;
var newConfig = oldConfig.DeepCopy(); var newConfig = oldConfig.DeepCopy();
// 3. 映射 DTO 值 (仅当不为空时修改) // 3. 映射 DTO 值
if (dto.IpAddress != null) newConfig.IpAddress = dto.IpAddress; if (dto.IpAddress != null) newConfig.IpAddress = dto.IpAddress;
if (dto.Port != null) newConfig.Port = dto.Port.Value; if (dto.Port != null) newConfig.Port = dto.Port.Value;
if (dto.Username != null) newConfig.Username = dto.Username; if (dto.Username != null) newConfig.Username = dto.Username;
@@ -250,10 +267,10 @@ public class CameraManager : IDisposable, IAsyncDisposable
if (dto.ChannelIndex != null) newConfig.ChannelIndex = dto.ChannelIndex.Value; if (dto.ChannelIndex != null) newConfig.ChannelIndex = dto.ChannelIndex.Value;
if (dto.StreamType != null) newConfig.StreamType = dto.StreamType.Value; if (dto.StreamType != null) newConfig.StreamType = dto.StreamType.Value;
if (dto.Name != null) newConfig.Name = dto.Name; if (dto.Name != null) newConfig.Name = dto.Name;
if (dto.RenderHandle != null) newConfig.RenderHandle = (System.IntPtr)dto.RenderHandle.Value; if (dto.RenderHandle != null) newConfig.RenderHandle = (IntPtr)dto.RenderHandle.Value;
if (dto.Brand != null) newConfig.Brand = (DeviceBrand)dto.Brand;
// 4. 判定冷热更新 // 4. 判定冷热更新
// 核心参数变更 -> 冷重启
bool needColdRestart = bool needColdRestart =
newConfig.IpAddress != oldConfig.IpAddress || newConfig.IpAddress != oldConfig.IpAddress ||
newConfig.Port != oldConfig.Port || newConfig.Port != oldConfig.Port ||
@@ -265,8 +282,6 @@ public class CameraManager : IDisposable, IAsyncDisposable
if (needColdRestart) if (needColdRestart)
{ {
device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)"); device.AddAuditLog($"检测到核心参数变更,执行冷重启 (Reboot)");
// 记录之前的运行状态
bool wasRunning = device.IsRunning; bool wasRunning = device.IsRunning;
// A. 彻底停止 // A. 彻底停止
@@ -275,7 +290,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
// B. 写入新配置 // B. 写入新配置
device.UpdateConfig(newConfig); device.UpdateConfig(newConfig);
// C. 如果之前是运行意图,则自动重启连接 // C. 自动重启
if (wasRunning) await device.StartAsync(); if (wasRunning) await device.StartAsync();
} }
else else
@@ -285,146 +300,128 @@ public class CameraManager : IDisposable, IAsyncDisposable
// A. 更新配置数据 // A. 更新配置数据
device.UpdateConfig(newConfig); device.UpdateConfig(newConfig);
// B. 在线应用策略 (无需断线) // B. 在线应用策略
if (device.IsOnline) if (device.IsOnline)
{ {
var options = new DynamicStreamOptions var options = new DynamicStreamOptions
{ {
StreamType = dto.StreamType, StreamType = dto.StreamType,
RenderHandle = dto.RenderHandle.HasValue ? (System.IntPtr)dto.RenderHandle : null RenderHandle = dto.RenderHandle.HasValue ? (IntPtr)dto.RenderHandle : null
}; };
// 触发驱动层的 OnApplyOptions
device.ApplyOptions(options); 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 #endregion
#region --- 6. (Disposal) --- #region --- 6. (Disposal) ---
/// <summary>
/// 同步销毁:内部调用异步销毁逻辑,等待销毁完成
/// </summary>
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
/// <summary>
/// [修复 Bug L & Bug γ] 异步执行全局资源清理
/// 严格遵循销毁顺序:停止任务 → 销毁设备 → 卸载SDK防止非托管内存泄漏
/// </summary>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
// 防护:避免重复销毁
if (_isDisposed) return; if (_isDisposed) return;
// 标记为已销毁,禁止后续操作
_isDisposed = true; _isDisposed = true;
_isEngineStarted = false; _isEngineStarted = false;
try try
{ {
// 1. 发送全局取消信号,立即停止协调器所有后台扫描任务
_globalCts.Cancel(); _globalCts.Cancel();
// 2. [Fix Bug L] 锁定设备池快照并清空,防止并发修改导致异常
var devices = _cameraPool.Values.ToArray(); var devices = _cameraPool.Values.ToArray();
_cameraPool.Clear(); _cameraPool.Clear();
// 3. 并行销毁所有相机设备,释放设备持有的非托管资源
var disposeTasks = devices.Select(async device => var disposeTasks = devices.Select(async device =>
{ {
try { await device.DisposeAsync(); } try { await device.DisposeAsync(); }
catch { /* 隔离单个设备销毁异常,不影响其他设备 */ } catch { }
}); });
await Task.WhenAll(disposeTasks); await Task.WhenAll(disposeTasks);
// 4. [Fix Bug γ: 二次伤害] 彻底卸载全局 SDK 环境
// 加 try-catch 防护极端场景(如进程强制终止时 SDK 已被系统回收)
try try
{ {
HikSdkManager.Uninitialize(); HikSdkManager.Uninitialize();
} }
catch catch { }
{
// 忽略卸载异常,保证销毁流程正常结束
}
} }
finally finally
{ {
// 释放取消令牌源资源
_globalCts.Dispose(); _globalCts.Dispose();
} }
} }
#endregion #endregion
/// <summary> #region --- 7. (Helpers) ---
/// 更新设备配置(热重载)
/// 流程:停止旧设备 -> 释放资源 -> 创建新设备 -> 替换引用 -> (可选)自动重启
/// </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) private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
{ {
return config.Brand switch return config.Brand switch
{ {
DeviceBrand.HikVision => new HikVideoSource(config), DeviceBrand.HikVision => new HikVideoSource(config),
// 如果你有大华或其他品牌,在这里扩展
// DeviceBrand.Dahua => new DahuaVideoSource(config),
_ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}") _ => 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
} }

View File

@@ -1,51 +1,105 @@
namespace SHH.CameraSdk; using System.Text.Json;
namespace SHH.CameraSdk
{
public class FileStorageService : IStorageService public class FileStorageService : IStorageService
{ {
public int ProcessId { get; } public int ProcessId { get; }
private readonly string _basePath; // 专属数据目录 private readonly string _baseDir;
private readonly string _devicesPath;
private readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
// [关键修复] 配置序列化选项,解决“只存属性不存字段”的问题
private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
WriteIndented = true, // 格式化 JSON让人眼可读
IncludeFields = true, // [核心] 允许序列化 public int Id; 这种字段
PropertyNameCaseInsensitive = true, // 忽略大小写差异
NumberHandling = JsonNumberHandling.AllowReadingFromString // 允许 "8000" 读为 int 8000
};
public FileStorageService(int processId) public FileStorageService(int processId)
{ {
ProcessId = processId; ProcessId = processId;
// 核心逻辑:数据隔离 _baseDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}");
// 1号进程 -> App_Data/Process_1/ _devicesPath = Path.Combine(_baseDir, "devices.json");
// 2号进程 -> App_Data/Process_2/
_basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data", $"Process_{processId}");
// 既然是框架搭建,我们要确保这个目录存在,否则后面谁写谁报错 if (!Directory.Exists(_baseDir)) Directory.CreateDirectory(_baseDir);
if (!Directory.Exists(_basePath))
Console.WriteLine($"[Storage] 路径: {_devicesPath}");
}
public async Task SaveDevicesAsync(IEnumerable<VideoSourceConfig> configs)
{ {
Directory.CreateDirectory(_basePath); await _fileLock.WaitAsync();
} try
Console.WriteLine($"[Storage] 存储服务已就绪。数据隔离路径: {_basePath}");
}
// --- 下面是未实现的空架子 ---
public Task SaveDevicesAsync(object configs)
{ {
// TODO: 待实现序列化写入 // [调试] 打印正在保存的数量,确保 Manager 传过来的数据是对的
return Task.CompletedTask; // Console.WriteLine($"[Debug] 正在保存 {configs.Count()} 台设备...");
var json = JsonSerializer.Serialize(configs, _jsonOptions);
await File.WriteAllTextAsync(_devicesPath, json);
// [调试] 打印部分 JSON 内容,验证是否为空对象 "{}"
// if (json.Length < 200) Console.WriteLine($"[Debug] JSON 内容: {json}");
}
catch (Exception ex)
{
Console.WriteLine($"[Storage] ❌ 保存配置失败: {ex.Message}");
}
finally
{
_fileLock.Release();
}
} }
public Task<object> LoadDevicesAsync() public async Task<List<VideoSourceConfig>> LoadDevicesAsync()
{ {
// TODO: 待实现读取 if (!File.Exists(_devicesPath))
return Task.FromResult<object>(null); {
Console.WriteLine("[Storage] ⚠️ 配置文件不存在,将使用空列表");
return new List<VideoSourceConfig>();
} }
public Task AppendSystemLogAsync(string action, string ip, string path) await _fileLock.WaitAsync();
try
{ {
// TODO: 待实现系统日志写入 var json = await File.ReadAllTextAsync(_devicesPath);
return Task.CompletedTask;
if (string.IsNullOrWhiteSpace(json)) return new List<VideoSourceConfig>();
// [调试] 打印读取到的原始 JSON
// Console.WriteLine($"[Debug] 读取文件内容: {json.Substring(0, Math.Min(json.Length, 100))}...");
var list = JsonSerializer.Deserialize<List<VideoSourceConfig>>(json, _jsonOptions);
// 二次校验:如果读出来列表不为空,但 ID 全是 0说明序列化还是没对上
if (list != null && list.Count > 0 && list[0].Id == 0 && list[0].Port == 0)
{
Console.WriteLine("[Storage] ⚠️ 警告:读取到设备,但字段似乎为空。请检查 VideoSourceConfig 是否使用了 private 属性?");
} }
public Task AppendDeviceLogAsync(long deviceId, string message) return list ?? new List<VideoSourceConfig>();
}
catch (Exception ex)
{ {
// TODO: 待实现设备日志写入 Console.WriteLine($"[Storage] ❌ 读取配置失败: {ex.Message}");
return Task.CompletedTask; // 出错时返回空列表,不要抛出异常,否则 StartAsync 会崩溃
return new List<VideoSourceConfig>();
}
finally
{
_fileLock.Release();
}
}
// ==================================================================
// 日志部分 (保持空实现以免干扰)
// ==================================================================
public Task AppendSystemLogAsync(string action, string ip, string path) => Task.CompletedTask;
public Task<List<string>> GetSystemLogsAsync(int count) => Task.FromResult(new List<string>());
public Task AppendDeviceLogAsync(int deviceId, string message) => Task.CompletedTask;
public Task<List<string>> GetDeviceLogsAsync(int deviceId, int count) => Task.FromResult(new List<string>());
} }
} }

View File

@@ -44,7 +44,7 @@ public class Program
// 核心设备管理器 // 核心设备管理器
// 注意:暂时保持无参构造,后续我们在改造 CameraManager 时再注入 storageService // 注意:暂时保持无参构造,后续我们在改造 CameraManager 时再注入 storageService
using var cameraManager = new CameraManager(); using var cameraManager = new CameraManager(storageService);
// 动态窗口管理器 // 动态窗口管理器
var displayManager = new DisplayWindowManager(); var displayManager = new DisplayWindowManager();
@@ -60,13 +60,17 @@ public class Program
// ============================================================================== // ==============================================================================
// 4. 业务编排 // 4. 业务编排
// ============================================================================== // ==============================================================================
// 【关键修复 1】先 StartAsync让它先从文件把 999 号设备读进内存
await cameraManager.StartAsync();
// 【关键修复 2】文件加载完后再决定要不要加默认设备
await ConfigureBusinessLogic(cameraManager); await ConfigureBusinessLogic(cameraManager);
// ============================================================================== // ==============================================================================
// 5. 启动引擎与交互 // 5. 启动引擎与交互
// ============================================================================== // ==============================================================================
Console.WriteLine("\n[系统] 正在启动全局管理引擎..."); Console.WriteLine("\n[系统] 正在启动全局管理引擎...");
await cameraManager.StartAsync();
Console.WriteLine($">> 系统就绪。Web 管理地址: http://localhost:{port}"); Console.WriteLine($">> 系统就绪。Web 管理地址: http://localhost:{port}");
Console.WriteLine($">> 数据存储路径: App_Data/Process_{processId}/"); Console.WriteLine($">> 数据存储路径: App_Data/Process_{processId}/");
@@ -112,9 +116,9 @@ public class Program
}); });
}); });
// 2. 日志降噪 //// 2. 日志降噪
builder.Logging.SetMinimumLevel(LogLevel.Warning); //builder.Logging.SetMinimumLevel(LogLevel.Warning);
builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning); //builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
// 3. 【核心】依赖注入注册 // 3. 【核心】依赖注入注册
// 将 storageService 注册为单例,这样 UserActionFilter 和 MonitorController 就能拿到它了 // 将 storageService 注册为单例,这样 UserActionFilter 和 MonitorController 就能拿到它了
@@ -156,6 +160,8 @@ public class Program
static int processIdFromPort(int port) => port - 5000 + 1; static int processIdFromPort(int port) => port - 5000 + 1;
static async Task ConfigureBusinessLogic(CameraManager manager) static async Task ConfigureBusinessLogic(CameraManager manager)
{
try
{ {
// 1. 添加测试设备 // 1. 添加测试设备
var config = new VideoSourceConfig var config = new VideoSourceConfig
@@ -182,4 +188,8 @@ public class Program
}; };
manager.AddDevice(config2); manager.AddDevice(config2);
} }
catch
{
}
}
} }