WebAPI 支持摄像头启停控制、码流切换、审计日志的提供

This commit is contained in:
2025-12-26 12:15:10 +08:00
parent e9f5975a79
commit adcdc56c7a
12 changed files with 1176 additions and 357 deletions

View File

@@ -1,118 +1,162 @@
using System.Threading.Channels;
namespace SHH.CameraSdk;
namespace SHH.CameraSdk;
/// <summary>
/// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版)
/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。
/// 修复记录:
/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。
/// 2. [Bug π] 管道安全Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。
/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。
/// 核心职责:
/// <para>1. 提供线程安全的生命周期管理(启动/停止/销毁)</para>
/// <para>2. 实现状态变更的可靠分发与异常隔离</para>
/// <para>3. 支持配置热更新与动态参数应用</para>
/// <para>4. 内置 FPS/码率统计、心跳保活、审计日志能力</para>
/// 关键修复记录:
/// <para>✅ [Bug A] 死锁免疫:所有 await 均添加 ConfigureAwait(false),解除 UI 线程依赖</para>
/// <para>✅ [Bug π] 管道安全Dispose 采用优雅关闭策略,确保剩余状态消息被消费</para>
/// <para>✅ [编译修复] 补全 CloneConfig 中 Transport/VendorArguments 的深拷贝逻辑</para>
/// </summary>
public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
{
#region --- (Core Config & Locks) ---
#region --- 1. (Core Config & Locks) ---
// [Fix Bug δ] 核心配置对象
// 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例
/// <summary>
/// 核心配置对象(支持热更新,去除 readonly 修饰符
/// 注意:外部修改需通过 UpdateConfig 方法,确保线程安全
/// </summary>
protected VideoSourceConfig _config;
/// <summary>
/// 状态同步锁
/// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致状态读取不一致
/// <summary>
/// 状态同步锁
/// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致状态不一致
/// </summary>
private readonly object _stateSyncRoot = new();
/// <summary>
/// 生命周期互斥锁
/// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致状态机混乱
/// <summary>
/// 生命周期互斥锁(信号量)
/// 作用:确保 StartAsync/StopAsync/UpdateConfig 串行执行,防止重入导致状态机混乱
/// 配置:初始计数 1最大计数 1 → 互斥锁
/// </summary>
private readonly SemaphoreSlim _lifecycleLock = new(1, 1);
#endregion
#region --- (Internal States & Infrastructure) ---
#region --- 2. (Internal States & Infrastructure) ---
// 内部状态标志位
/// <summary> 设备在线状态标志volatile 确保多线程可见性) </summary>
private volatile bool _isOnline;
/// <summary> 视频源核心状态(受 _stateSyncRoot 保护) </summary>
private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
/// <summary>
/// 状态通知队列 (有界)
/// 特性:采用 DropOldest 策略,消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β]
/// <summary>
/// 状态通知有界通道
/// 特性DropOldest 策略,消费者过载时丢弃旧状态,防止内存溢出
/// 配置:容量 100 | 单读者多写者
/// </summary>
private readonly Channel<StatusChangedEventArgs> _statusQueue;
// 状态分发器的取消令牌源
/// <summary> 状态分发器的取消令牌源 </summary>
private CancellationTokenSource? _distributorCts;
// [新增修复 Bug π] 分发任务引用
// 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费
/// <summary> 状态分发任务引用(用于 Dispose 时优雅等待) </summary>
private Task? _distributorTask;
// [Fix Bug V] 单调时钟
// 作用:记录最后一次收到帧的系统 Tick用于心跳检测不受系统时间修改影响
/// <summary> 最后一帧接收的系统 Tick单调时钟不受系统时间修改影响 </summary>
private long _lastFrameTick = 0;
/// <summary> 获取最后帧的时间戳 (线程安全读取) </summary>
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
/// <summary> 视频帧回调事件 (热路径) </summary>
/// <summary> 视频帧回调事件(热路径,低延迟分发) </summary>
public event Action<object>? FrameReceived;
#endregion
#region --- (Public Properties) ---
#region --- 3. (Public Properties) ---
/// <summary> 视频源唯一标识 </summary>
public long Id => _config.Id;
/// <summary> 只读配置副本(外部仅能通过 UpdateConfig 修改) </summary>
public VideoSourceConfig Config => _config;
public VideoSourceStatus Status { get { lock (_stateSyncRoot) return _status; } }
/// <summary> 视频源当前状态(线程安全读取) </summary>
public VideoSourceStatus Status
{
get
{
lock (_stateSyncRoot)
{
return _status;
}
}
}
/// <summary> 运行状态标记 </summary>
public bool IsRunning { get; set; }
/// <summary> 设备在线状态 </summary>
public bool IsOnline => _isOnline;
/// <summary> 设备元数据(能力集、通道信息等) </summary>
public DeviceMetadata Metadata { get; protected set; } = new();
/// <summary> 状态变更事件(对外暴露状态通知) </summary>
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
/// <summary> 最后一帧接收的 Tick 时间戳(线程安全读取) </summary>
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
#endregion
#region --- (Telemetry Properties) ---
#region --- 4. (Telemetry Properties) ---
// [新增] 遥测统计专用字段
private long _totalFramesReceived = 0; // 生命周期内总帧数
private int _tempFrameCounter = 0; // 用于计算FPS的临时计数器
private long _lastFpsCalcTick = 0; // 上次计算FPS的时间点
private double _currentFps = 0.0; // 当前实时FPS
/// <summary> 生命周期内接收的总帧数 </summary>
private long _totalFramesReceived = 0;
// [新增] 公开的遥测属性 (线程安全读取)
public double RealFps => _currentFps;
/// <summary> FPS 计算临时计数器 </summary>
private int _tempFrameCounter = 0;
/// <summary> 上次 FPS 计算的 Tick 时间 </summary>
private long _lastFpsCalcTick = 0;
/// <summary> 实时 FPS每秒更新一次 </summary>
public double RealFps { get; private set; } = 0.0;
/// <summary> 实时码率 (Mbps) </summary>
protected double _currentBitrate = 0;
/// <summary> 码率计算临时字节计数器 </summary>
private long _tempByteCounter = 0;
/// <summary> 生命周期总帧数(线程安全读取) </summary>
public long TotalFrames => Interlocked.Read(ref _totalFramesReceived);
#endregion
#region --- (Constructor) ---
#region --- 5. (Audit Log System) ---
/// <summary> 审计日志列表(线程安全访问) </summary>
private readonly List<string> _auditLogs = new();
/// <summary> 最大日志条数(滚动清除,防止内存溢出) </summary>
private const int MaxAuditLogCount = 100;
#endregion
#region --- 6. (Constructor) ---
/// <summary>
/// 构造函数:初始化基础设施
/// 初始化视频源基础设施
/// </summary>
/// <param name="config">视频源基础配置(含设备连接信息、通道号等)</param>
/// <param name="config">视频源基础配置</param>
/// <exception cref="ArgumentNullException">配置为空时抛出</exception>
protected BaseVideoSource(VideoSourceConfig config)
{
if (config == null) throw new ArgumentNullException(nameof(config));
// 入参校验
_config = config ?? throw new ArgumentNullException(nameof(config), "视频源配置不能为空");
// [Fix Bug U] 初始配置深拷贝
// 防止外部引用修改导致内部状态不可控(配置防漂移)
// 初始化帧控制器
Controller = new FrameController();
// 配置深拷贝(防漂移:内部配置与外部引用隔离)
_config = CloneConfig(config);
// [Fix Bug β] 初始化有界通道
// 容量 100单读者多写者模式
// 初始化有界状态通道
_statusQueue = Channel.CreateBounded<StatusChangedEventArgs>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
@@ -120,44 +164,46 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
SingleWriter = false
});
// 初始化状态分发器
_distributorCts = new CancellationTokenSource();
// [关键逻辑] 启动后台状态分发循环
// 明确持有 Task 引用,以便后续进行优雅关闭等待
_distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token));
}
#endregion
#region --- (Config Management) ---
#region --- 7. (Config Management) ---
/// <summary>
/// [修复 Bug δ] 更新配置实现
/// 允许在不销毁实例的情况下更新 IP、端口等参数新配置下次连接生效
/// 热更新视频源配置(线程安全)
/// 新配置将在下次启动/重连时生效
/// </summary>
/// <param name="newConfig">新的视频源配置</param>
public void UpdateConfig(VideoSourceConfig newConfig)
{
if (newConfig == null) return;
// 1. 获取生命周期锁
// 虽然只是内存操作,但为了防止与 Start/Stop 并发导致读取到脏配置,仍需加锁
// 加生命周期锁:防止与启动/停止操作并发
_lifecycleLock.Wait();
try
{
// 2. 执行深拷贝
_config = CloneConfig(newConfig);
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置已更新 ({_config.IpAddress}),下次连接生效。");
// 深拷贝新配置,隔离外部引用
_config = newConfig.DeepCopy();
// 写入审计日志
AddAuditLog($"配置已更新 [IP:{_config.IpAddress}],生效时机:{(_isOnline ? "" : "")}");
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置落地完成");
}
finally
{
_lifecycleLock.Release();
}
finally { _lifecycleLock.Release(); }
}
/// <summary>
/// 配置深拷贝辅助方法
/// [编译修复] 严格匹配源文件中的属性复制逻辑,确保 Dictionary 等引用类型被重新创建
/// 配置深拷贝辅助方法(确保引用类型独立)
/// </summary>
/// <param name="source">源配置对象</param>
/// <returns>深拷贝后的配置实例</returns>
/// <param name="source">源配置</param>
/// <returns>深拷贝后的配置</returns>
private VideoSourceConfig CloneConfig(VideoSourceConfig source)
{
return new VideoSourceConfig
@@ -172,7 +218,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
StreamType = source.StreamType,
Transport = source.Transport,
ConnectionTimeoutMs = source.ConnectionTimeoutMs,
// 必须深拷贝字典,防止外部修改影响内部
// Dictionary 深拷贝:防止外部修改影响内部
VendorArguments = source.VendorArguments != null
? new Dictionary<string, string>(source.VendorArguments)
: new Dictionary<string, string>()
@@ -181,316 +227,391 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
#endregion
#region --- (Lifecycle Control) ---
#region --- 8. (Lifecycle Control) ---
/// <summary>
/// 异步启动设备连接
/// 包含:状态校验、生命周期锁、非托管初始化、元数据刷新
/// 包含:状态校验、非托管初始化、元数据刷新
/// </summary>
public async Task StartAsync()
{
// [修复 Bug A] 必须加 ConfigureAwait(false)
// 确保后续代码在线程池线程执行,防止 UI 线程死锁
// 死锁免疫:不捕获当前同步上下文
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 1. 强制等待上一个生命周期动作完全结束
// 防止快速点击 Start/Stop 导致的逻辑重叠
// 等待上一个生命周期任务完成
await _pendingLifecycleTask.ConfigureAwait(false);
// 2. 状态幂等性检查
// 幂等性检查:已在线则直接返回
if (_isOnline) return;
// 3. 更新状态为连接中
UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand}...");
// 更新状态为连接中
UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand} 设备...");
// 4. 执行具体的驱动启动逻辑 (带超时控制)
// 驱动启动逻辑(带 15 秒超时)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await OnStartAsync(cts.Token).ConfigureAwait(false);
// 5. 标记运行状态
// 标记运行状态
_isOnline = true;
IsRunning = true;
// [Fix Bug D/J] 重置心跳
// 给予初始宽限期,防止刚启动就被判定为僵尸流
// 初始化心跳:给予 2 秒宽限期,防止刚启动被判定为僵死
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000);
// 6. 更新状态为播放中并刷新元数据
UpdateStatus(VideoSourceStatus.Playing, "流传输运行");
// 更新状态为播放中并刷新元数据
UpdateStatus(VideoSourceStatus.Playing, "流传输正常运行");
await RefreshMetadataAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
// 7. 异常处理:回滚状态
// 异常回滚:标记离线并更新状态
_isOnline = false;
UpdateStatus(VideoSourceStatus.Disconnected, $"启动失败: {ex.Message}");
throw;
}
finally { _lifecycleLock.Release(); }
}
/// <summary>
/// 异步停止设备连接
/// 流程:标记离线→执行驱动停止逻辑→更新状态
/// </summary>
public async Task StopAsync()
{
// [修复 Bug A] ConfigureAwait(false) 护体
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 1. 标记离线,阻断后续的数据处理
_isOnline = false;
// 2. 执行具体的驱动停止逻辑
await OnStopAsync().ConfigureAwait(false);
throw; // 向上抛出异常,由上层处理
}
finally
{
// 3. 更新状态并释放锁
UpdateStatus(VideoSourceStatus.Disconnected, "连接已断开");
_lifecycleLock.Release();
}
}
/// <summary>
/// 刷新设备元数据(能力集)
/// 对比新旧元数据差异,更新设备支持的功能、通道信息等
/// 异步停止设备连接
/// </summary>
public async Task StopAsync()
{
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 标记离线,阻断后续数据处理
_isOnline = false;
// 执行驱动层停止逻辑
await OnStopAsync().ConfigureAwait(false);
}
finally
{
// 更新状态并释放锁
UpdateStatus(VideoSourceStatus.Disconnected, "连接已手动断开");
_lifecycleLock.Release();
}
}
/// <summary>
/// 刷新设备元数据,对比差异并更新
/// </summary>
/// <returns>元数据差异描述符</returns>
public async Task<MetadataDiff> RefreshMetadataAsync()
{
// 离线状态不刷新元数据
if (!_isOnline) return MetadataDiff.None;
try
{
// 1. 调用驱动层获取最新元数据
// 获取驱动层最新元数据
var latestMetadata = await OnFetchMetadataAsync().ConfigureAwait(false);
// 2. 比对差异并更新
if (latestMetadata != null && latestMetadata.ChannelCount > 0)
{
// 对比新旧元数据差异
var diff = Metadata.CompareWith(latestMetadata);
// 更新元数据并标记同步时间
Metadata = latestMetadata;
Metadata.MarkSynced(); // 标记同步时间
Metadata.MarkSynced();
return diff;
}
}
catch (Exception ex) { Console.WriteLine($"[MetadataWarning] {Id}: {ex.Message}"); }
catch (Exception ex)
{
AddAuditLog($"元数据刷新失败: {ex.Message}");
Debug.WriteLine($"[MetadataWarning] 设备 {Id}: {ex.Message}");
}
return MetadataDiff.None;
}
/// <summary>
/// 应用动态参数(如码流切换、OSD设置
/// 支持运行时调整画面分辨率、帧率、渲染句柄等
/// 应用动态参数(运行时热更新
/// 支持:分辨率切换、码流类型切换、渲染句柄变更
/// </summary>
/// <param name="options">动态配置项</param>
public void ApplyOptions(DynamicStreamOptions options)
{
if (options == null || !_isOnline) return;
// 离线或参数为空时,忽略请求
if (options == null || !_isOnline)
{
AddAuditLog("动态参数应用失败:设备离线或参数为空");
return;
}
try
{
// 1. 校验参数合法性
if (Metadata.ValidateOptions(options, out string error))
// 1. 基于设备能力校验参数合法性
if (Metadata.ValidateOptions(options, out string errorMsg))
{
// 2. 调用驱动层应用参数
// 2. 执行驱动层参数应用逻辑
OnApplyOptions(options);
UpdateStatus(_status, "动态参数已应用");
UpdateStatus(_status, "动态参数已成功应用");
// 3. 记录参数变更日志
var changeLog = new List<string>();
if (options.StreamType.HasValue) changeLog.Add($"码流类型={options.StreamType}");
if (options.RenderHandle.HasValue) changeLog.Add($"渲染句柄已更新");
if (options.TargetAnalyzeFps.HasValue) changeLog.Add($"分析帧率={options.TargetAnalyzeFps}fps");
AddAuditLog($"动态参数应用: {string.Join(" | ", changeLog)}");
}
else
{
Debug.WriteLine($"[OptionRejected] 设备 {Id}: {errorMsg}");
}
else { Debug.WriteLine($"[OptionRejected] {error}"); }
}
catch (Exception ex) { Debug.WriteLine($"[ApplyOptionsError] {ex.Message}"); }
catch (Exception ex)
{
AddAuditLog($"动态参数应用失败: {ex.Message}");
Debug.WriteLine($"[ApplyOptionsError] {ex.Message}");
}
}
// 虚方法:供子类重写具体的参数应用逻辑
/// <summary>
/// 驱动层参数应用逻辑(子类重写)
/// </summary>
/// <param name="options">动态配置项</param>
protected virtual void OnApplyOptions(DynamicStreamOptions options) { }
#endregion
#region --- (Frame Processing Helpers) ---
#region --- 9. (Frame Processing & Status Management) ---
/// <summary>
/// 检查是否帧订阅者
/// 用于优化性能:无订阅时可跳过解码等耗时操作
/// 检查是否存在帧订阅者(性能优化)
/// 无订阅时可跳过解码/预处理等耗时操作
/// </summary>
/// <returns>有订阅者返回 true,否则返回 false</returns>
/// <returns>有订阅者返回 true</returns>
protected bool HasFrameSubscribers() => FrameReceived != null;
/// <summary>
/// 上报驱动层异常
/// 将底层异常转换为 Reconnecting 状态,触发协调器介入自愈
/// 上报驱动层异常,触发重连自愈逻辑
/// </summary>
/// <param name="ex">相机统一异常对象</param>
/// <param name="ex">相机统一异常</param>
protected void ReportError(CameraException ex)
{
if (!_isOnline) return;
// 标记离线并更新状态为重连中
_isOnline = false;
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK报错: {ex.Message}");
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK异常: {ex.Message}", ex);
}
/// <summary>
/// 标记收到一帧数据(心跳保活 + FPS计
/// [修改] 增强了 FPS 计算逻辑每1秒结算一次实时帧率
/// 标记帧接收事件(心跳保活 + FPS/码率统计)
/// </summary>
protected void MarkFrameReceived()
/// <param name="dataSize">当前帧字节大小</param>
protected void MarkFrameReceived(uint dataSize = 0)
{
long now = Environment.TickCount64;
var now = Environment.TickCount64;
// 1. 更新心跳时间 (原有逻辑)
// 1. 更新心跳时间戳(原子操作)
Interlocked.Exchange(ref _lastFrameTick, now);
// 2. 加总帧数 (原子操作)
// 2. 加总帧数原子操作
Interlocked.Increment(ref _totalFramesReceived);
// 3. 计算实时帧率 (FPS)
// 注意:这里不需要加锁,因为通常回调是单线程串行的
// 即便有多线程微小竞争对于FPS统计来说误差可忽略优先保证性能
// 3. 累加临时计数器(用于 FPS/码率计算)
_tempFrameCounter++;
long timeDiff = now - _lastFpsCalcTick;
_tempByteCounter += dataSize;
// 每 1000ms (1秒) 结算一次 FPS
if (timeDiff >= 1000)
// 4. 每秒结算一次统计指标
var timeDiff = now - _lastFpsCalcTick;
if (timeDiff >= 1000 && _lastFpsCalcTick > 0)
{
if (_lastFpsCalcTick > 0) // 忽略第一次冷启动的数据
{
// 计算公式: 帧数 / (时间间隔秒)
_currentFps = Math.Round(_tempFrameCounter / (timeDiff / 1000.0), 1);
}
var duration = timeDiff / 1000.0;
// 计算实时 FPS (保留 1 位小数)
RealFps = Math.Round(_tempFrameCounter / duration, 1);
// 计算实时码率 (Mbps) = (字节数 * 8) / 1024 / 1024 / 秒
_currentBitrate = Math.Round((_tempByteCounter * 8.0) / 1024 / 1024 / duration, 2);
// 重置临时计数器
_lastFpsCalcTick = now;
_tempFrameCounter = 0;
_tempByteCounter = 0;
}
else if (_lastFpsCalcTick == 0)
{
// 初始化 FPS 计算起始时间
_lastFpsCalcTick = now;
}
}
/// <summary>
/// 触发帧回调事件
/// 向所有订阅者分发帧数据(热路径,尽量减少耗时操作)
/// 触发帧回调事件(热路径优化)
/// </summary>
/// <param name="frameData">帧数据(通常为 OpenCvSharp.Mat 或 SmartFrame</param>
/// <param name="frameData">帧数据(如 Mat/SmartFrame</param>
protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData);
#endregion
#region --- (Status Distribution) ---
/// <summary>
/// 后台状态分发循环
/// 负责将 Channel 中的状态变更事件调度到 StatusChanged 事件订阅者
/// 后台状态分发循环(单线程消费状态队列)
/// </summary>
/// <param name="token">取消令牌,用于终止分发循环</param>
/// <param name="token">取消令牌</param>
private async Task StatusDistributorLoopAsync(CancellationToken token)
{
try
{
// [修复 Bug π] 关键修复点
// 使用 CancellationToken.None 作为 WaitToReadAsync 的参数
// 含义:即使 token 被取消,只要 Channel 里还有数据,就继续读取,直到 Channel 被 Complete 且为空
// 关键修复:使用 CancellationToken.None 等待读取
// 确保取消时仍能消费完队列剩余消息
while (await _statusQueue.Reader.WaitToReadAsync(CancellationToken.None).ConfigureAwait(false))
{
// 批量读取队列中的所有消息
while (_statusQueue.Reader.TryRead(out var args))
{
// [Fix Bug M] 玻璃心防护:捕获用户层回调异常,防止崩溃
// 异常隔离:捕获订阅者回调异常,防止分发器崩溃
try
{
StatusChanged?.Invoke(this, args);
}
catch (Exception ex)
{
Debug.WriteLine($"[UIEventError] {Id}: {ex.Message}");
Debug.WriteLine($"[UIEventError] 设备 {Id} 状态回调异常: {ex.Message}");
}
// 退出条件:仅当明确取消 且 队列已空 时才退出
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0) return;
// 退出条件:取消令牌已触发 且 队列为空
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0)
{
return;
}
}
// 双重检查退出条件
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0) return;
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0)
{
return;
}
}
}
catch (Exception ex) { Debug.WriteLine($"[DistributorFatal] {Id}: {ex.Message}"); }
catch (Exception ex)
{
Debug.WriteLine($"[DistributorFatal] 设备 {Id} 状态分发器崩溃: {ex.Message}");
}
}
/// <summary>
/// 更新设备状态并写入通道
/// 线程安全,采用 DropOldest 策略防止状态队列溢出
/// 更新设备状态并写入分发队列
/// </summary>
/// <param name="status">新状态</param>
/// <param name="msg">状态描述信息</param>
/// <param name="ex">可选:状态变更关联异常</param>
/// <param name="msg">状态描述</param>
/// <param name="ex">关联异常(可选)</param>
protected void UpdateStatus(VideoSourceStatus status, string msg, CameraException? ex = null)
{
lock (_stateSyncRoot)
{
// 更新内部状态
_status = status;
// 尝试写入有界通道如果满了则丢弃旧数据DropOldest策略在构造时指定
_statusQueue.Writer.TryWrite(new StatusChangedEventArgs(status, msg, ex, ex?.RawErrorCode));
// 写入状态队列(满时自动丢弃旧数据)
_ = _statusQueue.Writer.TryWrite(new StatusChangedEventArgs(
status,
msg,
ex,
ex?.RawErrorCode)
{
NewHandle = ex?.Context.TryGetValue("NativeHandle", out var handle) == true ? (IntPtr)handle : null
});
}
}
#endregion
#region --- (Abstract Methods) ---
#region --- 10. (Audit Log Helpers) ---
/// <summary>
/// 驱动层启动逻辑(必须由具体驱动实现
/// 包含设备连接、登录、取流等底层操作
/// 添加审计日志(线程安全
/// </summary>
/// <param name="message">日志内容</param>
public void AddAuditLog(string message)
{
lock (_auditLogs)
{
var logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
_auditLogs.Add(logEntry);
// 滚动清除旧日志
if (_auditLogs.Count > MaxAuditLogCount)
{
_auditLogs.RemoveAt(0);
}
}
}
/// <summary>
/// 获取审计日志副本
/// </summary>
/// <returns>日志列表副本</returns>
public List<string> GetAuditLogs()
{
lock (_auditLogs)
{
return new List<string>(_auditLogs);
}
}
#endregion
#region --- 11. (Abstract Methods) ---
/// <summary>
/// 驱动层启动逻辑(子类必须实现)
/// 包含:设备登录、码流订阅、取流线程启动等
/// </summary>
/// <param name="token">取消令牌</param>
protected abstract Task OnStartAsync(CancellationToken token);
/// <summary>
/// 驱动层停止逻辑(必须由具体驱动实现)
/// 包含设备登出、连接断开、资源释放等底层操作
/// 驱动层停止逻辑(子类必须实现)
/// 包含:码流停止、设备登出、资源释放等
/// </summary>
protected abstract Task OnStopAsync();
/// <summary>
/// 驱动层元数据获取逻辑(必须由具体驱动实现)
/// 用于获取设备型号、通道能力、固件版本等信息
/// 驱动层元数据获取逻辑(子类必须实现)
/// </summary>
/// <returns>设备元数据实例</returns>
/// <returns>设备元数据</returns>
protected abstract Task<DeviceMetadata> OnFetchMetadataAsync();
#endregion
#region --- (Disposal) ---
#region --- 12. (Resource Disposal) ---
/// <summary>
/// [Fix Bug A: 死锁终结者] 同步销毁入口
/// 原理:强制启动一个新的后台 Task 执行 DisposeAsync并同步阻塞等待其完成
/// 效果:彻底规避了在 UI 线程直接 wait 导致的死锁问题
/// 同步销毁入口(死锁免疫)
/// </summary>
public void Dispose()
{
// 异步销毁在后台执行,避免阻塞 UI 线程
Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
}
/// <summary>
/// 异步销毁资源
/// 包含:停止业务、关闭管道、断开事件引用、释放非托管资源
/// 异步销毁资源(优雅关闭)
/// </summary>
/// <returns>ValueTask</returns>
public virtual async ValueTask DisposeAsync()
{
// 1. 停止业务逻辑
await StopAsync().ConfigureAwait(false);
// 2. [Fix Bug π] 优雅关闭状态管道
_statusQueue.Writer.TryComplete(); // 标记不再接受新数据
_distributorCts?.Cancel(); // 通知消费者准备退出
// 2. 优雅关闭状态分发器
_statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息
_distributorCts?.Cancel(); // 触发分发器取消
// 3. 等待分发器处理完剩余消息(最多等待 500ms
if (_distributorTask != null)
{
// 3. 等待分发器处理完剩余消息
// 给予 500ms 的宽限期,防止无限等待
await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false);
}
// 4. [Fix Bug ε] 强力切断事件引用
// 防止 UI 控件忘记取消订阅导致的内存泄漏
// 4. 切断事件引用,防止内存泄漏
FrameReceived = null;
StatusChanged = null;
@@ -498,15 +619,19 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
_lifecycleLock.Dispose();
_distributorCts?.Dispose();
// 6. 抑制垃圾回收器的终结器
GC.SuppressFinalize(this);
}
#endregion
#region --- (Internal Fields) ---
#region --- 13. ---
// 用于跟踪上一个未完成的生命周期任务
/// <summary> 跟踪上一个未完成的生命周期任务 </summary>
private Task _pendingLifecycleTask = Task.CompletedTask;
/// <summary> 帧控制器(用于帧分发策略管理) </summary>
public FrameController Controller { get; protected set; }
#endregion
}