Files
Ayay/SHH.CameraSdk/Drivers/BaseVideoSource.cs

810 lines
29 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.
using Serilog;
namespace SHH.CameraSdk;
/// <summary>
/// [架构基类] 工业级视频源抽象核心 (V3.5.0 严格匹配版)
/// <para>当前模块: AiVideo | 核心原则: 低耦合、高并发、零拷贝</para>
/// 核心职责:
/// <para>1. 生命周期管理:基于 <see cref="SemaphoreSlim"/> 实现 Start/Stop/UpdateConfig 的线程安全串行化</para>
/// <para>2. 状态分发系统:利用 <see cref="Channel{T}"/> (DropOldest策略) 实现高性能、非阻塞的状态变更通知</para>
/// <para>3. 遥测统计引擎:内置原子级 FPS 计算与 Mbps 带宽监控,支持网络层与解码层双路流量计费</para>
/// <para>4. 弹性自愈机制:集成 IsAuthFailed 冻结期与物理网络 (Ping) 状态同步,支持 Coordinator 级自动重连</para>
/// <para>5. 审计与追踪:内置环形审计日志 (Max:100),记录配置变更、动态参数应用及驱动层异常</para>
/// 关键修复记录:
/// <para>✅ [V3.3.5] 资源防御:在 DisposeAsync 中强化生命周期锁,防止销毁期间触发重入</para>
/// <para>✅ [V3.3.5] 自愈增强:引入 Environment.TickCount64 宽限期机制,解决启动瞬间心跳超时误判</para>
/// <para>✅ [Bug A] 死锁免疫:强制所有 await 添加 ConfigureAwait(false),彻底解除对同步上下文的依赖</para>
/// <para>✅ [Bug π] 管道安全Dispose 采用 TryComplete 优雅关闭策略,确保缓冲区剩余状态消息被完全消费</para>
/// </summary>
public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceConnectivity
{
#region --- 1. (Fields & Locks) ---
/// <summary>
/// 核心配置对象(支持热更新)
/// 注意:外部修改需通过 UpdateConfig 方法,确保线程安全
/// </summary>
protected VideoSourceConfig _config;
/// <summary>状态同步锁:保护 _status 字段的读写原子性,防止多线程竞争导致状态不一致</summary>
private readonly object _stateSyncRoot = new();
/// <summary>
/// 生命周期互斥锁(信号量)
/// 作用:确保 StartAsync/StopAsync/UpdateConfig 串行执行,防止重入导致状态机混乱
/// 配置:初始计数 1最大计数 1 → 互斥锁
/// </summary>
private readonly SemaphoreSlim _lifecycleLock = new(1, 1);
/// <summary> 跟踪上一个未完成的生命周期任务 </summary>
private Task _pendingLifecycleTask = Task.CompletedTask;
/// <summary> 物理在线状态(由哨兵 Ping 更新) </summary>
private volatile bool _isPhysicalOnline;
/// <summary> 设备在线状态标志volatile 确保多线程可见性) </summary>
private volatile bool _isActived;
/// <summary> 视频源核心状态(受 _stateSyncRoot 保护) </summary>
private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
/// <summary> 资源销毁标记 </summary>
private volatile bool _isDisposed = false;
#endregion
#region --- 2. (Status Channel) ---
/// <summary>
/// 状态通知有界通道
/// 特性DropOldest 策略,消费者过载时丢弃旧状态,防止内存溢出
/// 配置:容量 100 | 单读者多写者
/// </summary>
private readonly Channel<StatusChangedEventArgs> _statusQueue;
/// <summary> 状态分发器的取消令牌源 </summary>
private CancellationTokenSource? _distributorCts;
/// <summary> 状态分发任务引用(用于 Dispose 时优雅等待) </summary>
private Task? _distributorTask;
#endregion
#region --- 3. (Telemetry & Stats) ---
/// <summary> 最后一帧接收的系统 Tick单调时钟不受系统时间修改影响 </summary>
private long _lastFrameTick = 0;
/// <summary> 生命周期内接收的总帧数 </summary>
private long _totalFramesReceived = 0;
/// <summary> FPS 计算临时计数器 </summary>
private int _tempFrameCounter = 0;
/// <summary> 上次 FPS 计算的 Tick 时间 </summary>
private long _lastFpsCalcTick = 0;
/// <summary> 实时码率 (Mbps) </summary>
protected double _currentBitrate = 0;
/// <summary> 码率计算临时字节计数器 </summary>
private long _tempByteCounter = 0;
#endregion
#region --- 4. (Audit & Resilience) ---
/// <summary> 审计日志列表(线程安全访问) </summary>
private readonly List<string> _auditLogs = new();
/// <summary> 最大日志条数(滚动清除,防止内存溢出) </summary>
private const int MaxAuditLogCount = 100;
/// <summary>
/// 认证类致命错误标记(如密码错、用户锁定)
/// 作用:触发 15 分钟长冷冻期,防止 IP 被相机锁定
/// </summary>
public bool IsAuthFailed { get; set; }
/// <summary>上次尝试执行 StartAsync 的系统 Tick 时间 (单调时钟)</summary>
private long _lastStartAttemptTick = 0;
#endregion
#region --- 5. (Events) ---
/// <summary> 视频帧回调事件(热路径,低延迟分发) </summary>
public event Action<object>? FrameReceived;
/// <summary> 状态变更事件(对外异步暴露状态通知) </summary>
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
#endregion
#region --- 6. (Properties) ---
/// <summary> 视频源唯一标识 </summary>
public long Id => _config.Id;
/// <summary> 只读配置副本(外部仅能通过 UpdateConfig 修改) </summary>
public VideoSourceConfig Config => _config;
/// <summary> 视频源当前状态(线程安全读取) </summary>
public VideoSourceStatus Status
{
get
{
lock (_stateSyncRoot) return _status;
}
}
/// <summary> 运行状态标记 </summary>
public bool IsRunning { get; set; }
/// <summary> 设备在线状态 </summary>
public bool IsActived => _isActived;
/// <summary> 物理在线状态Ping 结果) </summary>
public bool IsPhysicalOnline => _isPhysicalOnline;
/// <summary> 设备元数据(能力集、通道信息等) </summary>
public DeviceMetadata Metadata { get; protected set; } = new();
/// <summary> 帧控制器(用于帧分发策略管理) </summary>
public FrameController Controller { get; protected set; }
/// <summary>
/// 图像预处理配置(缩放、增量等)
/// 放置在基类中确保所有接入协议HIK/DH/RTSP均可共享处理逻辑
/// </summary>
public PreprocessConfig PreprocessSettings { get; set; }
= new PreprocessConfig();
/// <summary> 最后一帧接收的 Tick 时间戳(线程安全读取) </summary>
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
/// <summary> 生命周期总帧数(线程安全读取) </summary>
public long TotalFrames => Interlocked.Read(ref _totalFramesReceived);
/// <summary> 实时 FPS每秒更新一次 </summary>
public double RealFps { get; private set; } = 0.0;
// 提供一个最近一秒的输入帧率参考值
public int NominalInputFps => (int)Math.Round(RealFps);
/// <summary> 实时码率 (Mbps) </summary>
public double RealBitrate => _currentBitrate;
/// <summary> 视频宽度 </summary>
public int Width { get; protected set; }
/// <summary> 视频高度 </summary>
public int Height { get; protected set; }
/// <summary> 满足接口要求的 IP 地址 </summary>
string IDeviceConnectivity.IpAddress => _config.IpAddress;
/// <summary> 上次启动尝试的时间戳 </summary>
public long LastStartAttemptTick => Interlocked.Read(ref _lastStartAttemptTick);
#endregion
#region --- 7. (Abstracts) ---
/// <summary> 子类特定的日志记录器 </summary>
protected abstract ILogger _sdkLog { get; }
/// <summary>
/// 驱动层启动逻辑(子类必须实现)
/// 包含:设备登录、码流订阅、取流线程启动等
/// </summary>
/// <param name="token">取消令牌</param>
protected abstract Task OnStartAsync(CancellationToken token);
/// <summary>
/// 驱动层停止逻辑(子类必须实现)
/// 包含:码流停止、设备登出、资源释放等
/// </summary>
protected abstract Task OnStopAsync();
/// <summary>
/// 驱动层元数据获取逻辑(子类必须实现)
/// </summary>
/// <returns>设备元数据</returns>
protected abstract Task<DeviceMetadata> OnFetchMetadataAsync();
#endregion
#region --- 8. (Constructor) ---
/// <summary>
/// 初始化视频源基础设施
/// </summary>
/// <param name="config">视频源基础配置</param>
/// <exception cref="ArgumentNullException">配置为空时抛出</exception>
protected BaseVideoSource(VideoSourceConfig config)
{
// 入参校验
_config = config ?? throw new ArgumentNullException(nameof(config), "视频源配置不能为空");
// 初始化帧控制器
Controller = new FrameController();
// 配置深拷贝(防漂移:内部配置与外部引用隔离)
_config = CloneConfig(config);
// 初始化有界状态通道
_statusQueue = Channel.CreateBounded<StatusChangedEventArgs>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
// 初始化状态分发器
_distributorCts = new CancellationTokenSource();
_distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token));
}
#endregion
#region --- 9. (Lifecycle) ---
/// <summary>
/// 异步启动设备连接
/// 包含:状态校验、非托管初始化、元数据刷新
/// </summary>
public async Task StartAsync()
{
// Optimized: [原因] 增加销毁前置检查,配合 Bug 修复方案
if (_isDisposed || _lifecycleLock == null)
throw new ObjectDisposedException(nameof(BaseVideoSource), "设备实例已销毁");
// 死锁免疫:不捕获当前同步上下文
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 等待上一个生命周期任务完成
await _pendingLifecycleTask.ConfigureAwait(false);
// 幂等性检查:已在线则直接返回
if (_isActived) return;
// 更新状态为连接中
UpdateStatus(VideoSourceStatus.Connecting, $"正在启动 {_config.Brand} 设备...");
// 驱动层启动逻辑(带 15 秒超时)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await OnStartAsync(cts.Token).ConfigureAwait(false);
// ============================================================
// 【核心修复位置】
// 赋予心跳 5 秒的宽限期 (Grace Period)
// 这样 Coordinator 在未来 5 秒内计算出来的无帧时间将是负数或极小值,不会触发复位
// ============================================================
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 5000);
// 标记运行状态
_isActived = true;
IsRunning = true;
// 更新状态为播放中,并刷新元数据
UpdateStatus(VideoSourceStatus.Playing, "流传输正常运行");
await RefreshMetadataAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
// 异常回滚:标记离线并更新状态
_isActived = false;
UpdateStatus(VideoSourceStatus.Disconnected, $"启动失败: {ex.Message}");
throw; // 向上抛出异常,由上层处理
}
finally
{
_lifecycleLock.Release();
}
}
/// <summary>
/// 异步停止设备连接
/// </summary>
public async Task StopAsync()
{
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 标记离线,阻断后续数据处理
_isActived = false;
// 执行驱动层停止逻辑
await OnStopAsync().ConfigureAwait(false);
}
finally
{
// 更新状态并释放锁
UpdateStatus(VideoSourceStatus.Disconnected, "连接已手动断开");
_lifecycleLock.Release();
}
}
#endregion
#region --- 10. (Config & Metadata) ---
/// <summary>
/// 热更新视频源配置(线程安全)
/// 新配置将在下次启动/重连时生效
/// </summary>
/// <param name="newConfig">新的视频源配置</param>
public void UpdateConfig(VideoSourceConfig newConfig)
{
if (newConfig == null) return;
// 加生命周期锁:防止与启动/停止操作并发
_lifecycleLock.Wait();
try
{
// 深拷贝新配置,隔离外部引用
_config = newConfig.DeepCopy();
// 写入审计日志
AddAuditLog($"配置已更新 [IP:{_config.IpAddress}],生效时机:{(_isActived ? "" : "")}");
Debug.WriteLine($"[ConfigUpdated] 设备 {Id} 配置落地完成");
}
finally
{
_lifecycleLock.Release();
}
}
/// <summary>
/// 配置深拷贝辅助方法(确保引用类型独立)
/// </summary>
/// <param name="source">源配置</param>
/// <returns>深拷贝后的新配置</returns>
private VideoSourceConfig CloneConfig(VideoSourceConfig source)
{
return new VideoSourceConfig
{
Id = source.Id,
Brand = source.Brand,
IpAddress = source.IpAddress,
Port = source.Port,
Username = source.Username,
Password = source.Password,
ChannelIndex = source.ChannelIndex,
StreamType = source.StreamType,
Transport = source.Transport,
ConnectionTimeoutMs = source.ConnectionTimeoutMs,
MainboardIp = source.MainboardIp,
MainboardPort = source.MainboardPort,
RtspPath = source.RtspPath,
RenderHandle = source.RenderHandle,
// Dictionary 深拷贝:防止外部修改影响内部
VendorArguments = source.VendorArguments != null
? new Dictionary<string, string>(source.VendorArguments)
: new Dictionary<string, string>()
};
}
/// <summary>
/// 刷新设备元数据,对比差异并更新
/// </summary>
/// <returns>元数据差异描述符</returns>
public async Task<MetadataDiff> RefreshMetadataAsync()
{
// 离线状态不刷新元数据
if (!_isActived) return MetadataDiff.None;
try
{
// 获取驱动层最新元数据
var latestMetadata = await OnFetchMetadataAsync().ConfigureAwait(false);
if (latestMetadata != null && latestMetadata.ChannelCount > 0)
{
// 对比新旧元数据差异
var diff = Metadata.CompareWith(latestMetadata);
// 更新元数据并标记同步时间
Metadata = latestMetadata;
Metadata.MarkSynced();
return diff;
}
}
catch (Exception ex)
{
AddAuditLog($"元数据刷新失败: {ex.Message}");
Debug.WriteLine($"[MetadataWarning] 设备 {Id}: {ex.Message}");
}
return MetadataDiff.None;
}
/// <summary>
/// 应用动态流参数(运行时热更新)
/// 支持:分辨率切换、码流类型切换、渲染句柄变更等
/// </summary>
/// <param name="options">动态配置项</param>
public void ApplyOptions(DynamicStreamOptions options)
{
// 离线或参数为空时,忽略请求
if (options == null || !_isActived)
{
AddAuditLog("动态参数应用失败:设备离线或参数为空");
return;
}
try
{
// 1. 基于设备能力校验参数合法性
if (Metadata.ValidateOptions(options, out string errorMsg))
{
// 2. 执行驱动层参数应用逻辑
OnApplyOptions(options);
UpdateStatus(_status, "动态参数已成功应用");
// 3. 记录参数变更日志
var changeLog = new List<string>();
if (options.StreamType.HasValue) changeLog.Add($"码流类型={options.StreamType}");
if (options.RenderHandle.HasValue) changeLog.Add($"渲染句柄已更新");
if (options.TargetFps.HasValue) changeLog.Add($"分析帧率={options.TargetFps}fps");
AddAuditLog($"动态参数应用: {string.Join(" | ", changeLog)}");
}
else
{
Debug.WriteLine($"[OptionRejected] 设备 {Id}: {errorMsg}");
}
}
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 --- 11. (Data Handling) ---
/// <summary>
/// 检查是否存在帧订阅者(性能优化)
/// 无订阅时可跳过解码/预处理等耗时操作
/// </summary>
/// <returns>有订阅者返回 true</returns>
protected bool HasFrameSubscribers() => FrameReceived != null;
/// <summary>
/// 触发帧回调事件(热路径优化)
/// </summary>
/// <param name="frameData">帧数据(如 Mat/SmartFrame</param>
protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData);
/// <summary>
/// 标记数据接收(心跳保活 + 双路统计)
/// <para>调用规则:</para>
/// <para>1. 网络层收到流数据时:调用 MarkFrameReceived(dwBufSize),只统计流量。</para>
/// <para>2. 解码层流控通过后:调用 MarkFrameReceived(0),只统计有效帧率。</para>
/// </summary>
/// <param name="dataSize">数据包大小字节0 表示这是一帧解码后的图像</param>
protected void MarkFrameReceived(uint dataSize = 0)
{
long now = Environment.TickCount64;
// 1. [心跳保活] 无论网络包还是解码帧,都视为设备“活着”
// 使用 Interlocked 保证多线程读写安全
Interlocked.Exchange(ref _lastFrameTick, now);
// 2. [分流累加] 根据来源不同,累加不同的计数器
if (dataSize > 0)
{
// --- 来源:网络层回调 (SafeOnRealDataReceived) ---
// 只累加字节数,用于计算带宽 (Mbps)
// 绝对不能在这里累加帧数,否则会被网络包的数量误导(导致 FPS 虚高)
Interlocked.Add(ref _tempByteCounter, dataSize);
}
else
{
// --- 来源:解码层回调 (SafeOnDecodingCallBack) ---
// 只累加帧数,用于计算有效 FPS
// 只有经过 MakeDecision() 筛选保留下来的帧才走到这里,所以是真实的 "Output FPS"
Interlocked.Increment(ref _tempFrameCounter);
// 累加生命周期总帧数
Interlocked.Increment(ref _totalFramesReceived);
}
// 3. [定期结算] 每 1000ms (1秒) 结算一次统计指标
long timeDiff = now - _lastFpsCalcTick;
if (timeDiff >= 1000)
{
// 忽略第一次冷启动的数据(避免除以 0 或时间跨度过大)
if (_lastFpsCalcTick > 0)
{
double duration = timeDiff / 1000.0;
// --- A. 结算有效帧率 (FPS) ---
// 原子读取并重置计数器,防止漏算
int frames = Interlocked.Exchange(ref _tempFrameCounter, 0);
RealFps = Math.Round(frames / duration, 1);
// --- B. 结算网络带宽 (Mbps) ---
// 公式: (字节数 * 8位) / 1024 / 1024 / 秒数
long bytes = Interlocked.Exchange(ref _tempByteCounter, 0);
_currentBitrate = Math.Round((bytes * 8.0) / 1048576.0 / duration, 2);
}
else
{
// 初始化重置:确保原子性
Interlocked.Exchange(ref _tempFrameCounter, 0);
Interlocked.Exchange(ref _tempByteCounter, 0);
}
// 更新结算时间锚点
_lastFpsCalcTick = now;
}
}
#endregion
#region --- 12. (Status Distributor) ---
/// <summary>
/// 更新设备状态并写入分发队列
/// </summary>
/// <param name="status">新状态</param>
/// <param name="msg">状态描述</param>
/// <param name="ex">关联异常(可选)</param>
protected void UpdateStatus(VideoSourceStatus status, string msg, CameraException? ex = null)
{
lock (_stateSyncRoot)
{
// 更新内部状态
_status = status;
// 写入状态队列(满时自动丢弃旧数据)
_ = _statusQueue.Writer.TryWrite(new StatusChangedEventArgs(
status,
msg,
ex,
ex?.RawErrorCode)
{
NewHandle = ex?.Context.TryGetValue("NativeHandle", out var handle) == true ? (IntPtr)handle : null
});
}
}
/// <summary>
/// 允许哨兵从外部更新 _isOnline 字段
/// </summary>
/// <param name="isOnline"></param>
void IDeviceConnectivity.SetNetworkStatus(bool isOnline)
{
if (_isPhysicalOnline != isOnline)
{
_isPhysicalOnline = isOnline;
// 触发状态变更是为了通知 UI 更新绿色小圆点,但不改变 Status
// 注意:这里传 _status 保持原样,只变消息
StatusChanged?.Invoke(this, new StatusChangedEventArgs(_status, isOnline ? "物理网络恢复" : "物理网络中断"));
}
}
/// <summary>
/// 后台状态分发循环(单线程消费状态队列)
/// </summary>
/// <param name="token">取消令牌</param>
private async Task StatusDistributorLoopAsync(CancellationToken token)
{
try
{
// 关键修复:使用 CancellationToken.None 等待读取
// 确保取消时仍能消费完队列剩余消息
while (await _statusQueue.Reader.WaitToReadAsync(CancellationToken.None).ConfigureAwait(false))
{
// 批量读取队列中的所有消息
while (_statusQueue.Reader.TryRead(out var args))
{
// 异常隔离:捕获订阅者回调异常,防止分发器崩溃
try
{
StatusChanged?.Invoke(this, args);
}
catch (Exception ex)
{
_sdkLog.Error(ex, "设备 {Id} 状态变更回调异常", Id);
}
// 退出条件:取消令牌已触发 且 队列为空
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0)
{
return;
}
}
// 双重检查退出条件
if (token.IsCancellationRequested && _statusQueue.Reader.Count == 0)
{
return;
}
}
}
catch (OperationCanceledException) { /* 正常退出 */ }
catch (Exception ex)
{
_sdkLog.Fatal(ex, "设备 {Id} 状态分发器致命异常", Id);
}
finally
{
// Optimized: [原因] 确保在分发器关闭后,缓冲区内残余的状态消息能被强制消费完。
while (_statusQueue.Reader.TryRead(out var args))
{
try { StatusChanged?.Invoke(this, args); } catch { /* 忽略销毁期的回调异常 */ }
}
}
}
/// <summary>
/// 上报驱动层异常,触发重连自愈逻辑
/// </summary>
/// <param name="ex">相机统一异常</param>
protected void ReportError(CameraException ex)
{
if (!_isActived) return;
// 标记离线并更新状态为重连中
_isActived = false;
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK异常: {ex.Message}", ex);
}
#endregion
#region --- 13. (Resilience & Audit) ---
/// <summary>
/// 更新最后一次启动尝试的时间戳为当前时间
/// </summary>
public void MarkStartAttempt()
{
Interlocked.Exchange(ref _lastStartAttemptTick, Environment.TickCount64);
}
/// <summary>
/// 强制重置自愈相关的冷却与错误标记
/// 用于用户手动干预(如修改密码)后,使协调器能立即触发下一次尝试
/// </summary>
public void ResetResilience()
{
// 1. 清除认证失败标记
IsAuthFailed = false;
// 2. 将尝试时间戳归零
// 这样在 Coordinator 中计算 elapsed = now - 0结果会远大于 30s
Interlocked.Exchange(ref _lastStartAttemptTick, 0);
_sdkLog.Debug($"[Sdk] 设备 {Id} 自愈状态已人工重置");
}
/// <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);
}
}
/// <summary>
/// 清空审计日志
/// </summary>
public void ClearAuditLogs()
{
_auditLogs.Clear();
AddAuditLog("用户清空了审计日志");
}
#endregion
#region --- 14. (Disposal) ---
/// <summary>
/// 同步销毁入口(死锁免疫)
/// </summary>
public void Dispose()
{
// 触发异步销毁,但设定一个超时兜底,防止永久卡死 UI
// 这里等待 2 秒,如果还没销毁完也强行返回,避免界面冻结
Task.Run(async () => await DisposeAsync().ConfigureAwait(false))
.Wait(TimeSpan.FromSeconds(2));
GC.SuppressFinalize(this);
}
/// <summary>
/// 异步销毁资源(优雅关闭)
/// </summary>
/// <returns>ValueTask</returns>
public virtual async ValueTask DisposeAsync()
{
// 防止重复 Dispose
if (_isDisposed) return;
// 提前锁定并获取引用
var semaphore = _lifecycleLock;
if (semaphore == null) return;
// Optimized: [原因] 获取生命周期锁,防止在 DisposeAsync 执行期间被并发触发 Start/Stop 操作
await _lifecycleLock.WaitAsync().ConfigureAwait(false);
try
{
// 防止重复 Dispose
if (_isDisposed) return;
// 1. 停止业务逻辑
await StopAsync().ConfigureAwait(false);
_isDisposed = true;
// 2. 优雅关闭状态分发器
_statusQueue.Writer.TryComplete(); // 标记队列不再接受新消息
_distributorCts?.Cancel(); // 触发分发器取消
// 3. 等待分发器处理完剩余消息(最多等待 500ms
if (_distributorTask != null)
{
await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false);
}
// 4. 切断事件引用,防止内存泄漏
FrameReceived = null;
StatusChanged = null;
// 5. 释放基础资源
_lifecycleLock.Dispose();
_distributorCts?.Dispose();
}
finally
{
// Optimized: [原因] 修复信号量销毁顺序。
// 先释放锁,让可能存在的阻塞线程(虽然被 _isDisposed 阻断)能正常通过,
// 然后检查是否为销毁流程的最后一步。
semaphore.Release();
// 彻底销毁信号量并清空引用,确保后续调用不再访问已释放的对象
_lifecycleLock?.Dispose();
// 6. 抑制垃圾回收器的终结器
GC.SuppressFinalize(this);
}
}
#endregion
}