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

512 lines
19 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 System.Threading.Channels;
namespace SHH.CameraSdk;
/// <summary>
/// [架构基类] 工业级视频源抽象核心 (V3.3.4 严格匹配版)
/// 核心职责:提供线程安全的生命周期管理、状态分发、配置热更新及资源清理能力。
/// 修复记录:
/// 1. [Bug A] 死锁免疫:所有 await 增加 ConfigureAwait(false),解除对 UI 线程同步上下文的依赖。
/// 2. [Bug π] 管道安全Dispose 时采用优雅关闭策略,确保最后的状态变更通知能发送出去。
/// 3. [编译修复] 补全了 CloneConfig 中对于 Transport 和 VendorArguments 的属性复制。
/// </summary>
public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
{
#region --- (Core Config & Locks) ---
// [Fix Bug δ] 核心配置对象
// 去除 readonly 修饰符以支持热更新 (Hot Update),允许在运行时替换配置实例
protected VideoSourceConfig _config;
/// <summary>
/// 状态同步锁
/// 作用:保护 _status 字段的读写原子性,防止多线程竞争导致的状态读取不一致
/// </summary>
private readonly object _stateSyncRoot = new();
/// <summary>
/// 生命周期互斥锁
/// 作用:确保 StartAsync/StopAsync/UpdateConfig 等操作串行执行,防止重入导致的状态机混乱
/// </summary>
private readonly SemaphoreSlim _lifecycleLock = new(1, 1);
#endregion
#region --- (Internal States & Infrastructure) ---
// 内部状态标志位
private volatile bool _isOnline;
private VideoSourceStatus _status = VideoSourceStatus.Disconnected;
/// <summary>
/// 状态通知队列 (有界)
/// 特性:采用 DropOldest 策略,当消费者处理不过来时丢弃旧状态,防止背压导致内存溢出 [Fix Bug β]
/// </summary>
private readonly Channel<StatusChangedEventArgs> _statusQueue;
// 状态分发器的取消令牌源
private CancellationTokenSource? _distributorCts;
// [新增修复 Bug π] 分发任务引用
// 作用:用于在 DisposeAsync 时执行 Task.WhenAny 等待,确保剩余消息被消费
private Task? _distributorTask;
// [Fix Bug V] 单调时钟
// 作用:记录最后一次收到帧的系统 Tick用于心跳检测不受系统时间修改影响
private long _lastFrameTick = 0;
/// <summary> 获取最后帧的时间戳 (线程安全读取) </summary>
public long LastFrameTick => Interlocked.Read(ref _lastFrameTick);
/// <summary> 视频帧回调事件 (热路径) </summary>
public event Action<object>? FrameReceived;
#endregion
#region --- (Public Properties) ---
public long Id => _config.Id;
public VideoSourceConfig Config => _config;
public VideoSourceStatus Status { get { lock (_stateSyncRoot) return _status; } }
public bool IsRunning { get; set; }
public bool IsOnline => _isOnline;
public DeviceMetadata Metadata { get; protected set; } = new();
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
#endregion
#region --- (Telemetry Properties) ---
// [新增] 遥测统计专用字段
private long _totalFramesReceived = 0; // 生命周期内总帧数
private int _tempFrameCounter = 0; // 用于计算FPS的临时计数器
private long _lastFpsCalcTick = 0; // 上次计算FPS的时间点
private double _currentFps = 0.0; // 当前实时FPS
// [新增] 公开的遥测属性 (线程安全读取)
public double RealFps => _currentFps;
public long TotalFrames => Interlocked.Read(ref _totalFramesReceived);
#endregion
#region --- (Constructor) ---
/// <summary>
/// 构造函数:初始化基础设施
/// </summary>
/// <param name="config">视频源基础配置(含设备连接信息、通道号等)</param>
/// <exception cref="ArgumentNullException">配置为空时抛出</exception>
protected BaseVideoSource(VideoSourceConfig config)
{
if (config == null) throw new ArgumentNullException(nameof(config));
// [Fix Bug U] 初始配置深拷贝
// 防止外部引用修改导致内部状态不可控(配置防漂移)
_config = CloneConfig(config);
// [Fix Bug β] 初始化有界通道
// 容量 100单读者多写者模式
_statusQueue = Channel.CreateBounded<StatusChangedEventArgs>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false
});
_distributorCts = new CancellationTokenSource();
// [关键逻辑] 启动后台状态分发循环
// 明确持有 Task 引用,以便后续进行优雅关闭等待
_distributorTask = Task.Run(() => StatusDistributorLoopAsync(_distributorCts.Token));
}
#endregion
#region --- (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}),下次连接生效。");
}
finally { _lifecycleLock.Release(); }
}
/// <summary>
/// 配置深拷贝辅助方法
/// [编译修复] 严格匹配源文件中的属性复制逻辑,确保 Dictionary 等引用类型被重新创建
/// </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,
// 必须深拷贝字典,防止外部修改影响内部
VendorArguments = source.VendorArguments != null
? new Dictionary<string, string>(source.VendorArguments)
: new Dictionary<string, string>()
};
}
#endregion
#region --- (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}...");
// 4. 执行具体的驱动启动逻辑 (带超时控制)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
await OnStartAsync(cts.Token).ConfigureAwait(false);
// 5. 标记运行状态
_isOnline = true;
IsRunning = true;
// [Fix Bug D/J] 重置心跳
// 给予初始宽限期,防止刚启动就被判定为僵尸流
Interlocked.Exchange(ref _lastFrameTick, Environment.TickCount64 + 2000);
// 6. 更新状态为播放中并刷新元数据
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);
}
finally
{
// 3. 更新状态并释放锁
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(); // 标记同步时间
return diff;
}
}
catch (Exception ex) { Console.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;
try
{
// 1. 校验参数合法性
if (Metadata.ValidateOptions(options, out string error))
{
// 2. 调用驱动层应用参数
OnApplyOptions(options);
UpdateStatus(_status, "动态参数已应用");
}
else { Debug.WriteLine($"[OptionRejected] {error}"); }
}
catch (Exception ex) { Debug.WriteLine($"[ApplyOptionsError] {ex.Message}"); }
}
// 虚方法:供子类重写具体的参数应用逻辑
protected virtual void OnApplyOptions(DynamicStreamOptions options) { }
#endregion
#region --- (Frame Processing Helpers) ---
/// <summary>
/// 检查是否有帧订阅者
/// 用于优化性能:无订阅时可跳过解码等耗时操作
/// </summary>
/// <returns>有订阅者返回 true否则返回 false</returns>
protected bool HasFrameSubscribers() => FrameReceived != null;
/// <summary>
/// 上报驱动层异常
/// 将底层异常转换为 Reconnecting 状态,触发协调器介入自愈
/// </summary>
/// <param name="ex">相机统一异常对象</param>
protected void ReportError(CameraException ex)
{
if (!_isOnline) return;
_isOnline = false;
UpdateStatus(VideoSourceStatus.Reconnecting, $"SDK报错: {ex.Message}");
}
/// <summary>
/// 标记收到一帧数据(心跳保活 + FPS计算
/// [修改] 增强了 FPS 计算逻辑每1秒结算一次实时帧率
/// </summary>
protected void MarkFrameReceived()
{
long now = Environment.TickCount64;
// 1. 更新心跳时间 (原有逻辑)
Interlocked.Exchange(ref _lastFrameTick, now);
// 2. 增加总帧数 (原子操作)
Interlocked.Increment(ref _totalFramesReceived);
// 3. 计算实时帧率 (FPS)
// 注意:这里不需要加锁,因为通常回调是单线程串行的
// 即便有多线程微小竞争对于FPS统计来说误差可忽略优先保证性能
_tempFrameCounter++;
long timeDiff = now - _lastFpsCalcTick;
// 每 1000ms (1秒) 结算一次 FPS
if (timeDiff >= 1000)
{
if (_lastFpsCalcTick > 0) // 忽略第一次冷启动的数据
{
// 计算公式: 帧数 / (时间间隔秒)
_currentFps = Math.Round(_tempFrameCounter / (timeDiff / 1000.0), 1);
}
_lastFpsCalcTick = now;
_tempFrameCounter = 0;
}
}
/// <summary>
/// 触发帧回调事件
/// 向所有订阅者分发帧数据(热路径,尽量减少耗时操作)
/// </summary>
/// <param name="frameData">帧数据(通常为 OpenCvSharp.Mat 或 SmartFrame</param>
protected void RaiseFrameReceived(object frameData) => FrameReceived?.Invoke(frameData);
#endregion
#region --- (Status Distribution) ---
/// <summary>
/// 后台状态分发循环
/// 负责将 Channel 中的状态变更事件调度到 StatusChanged 事件订阅者
/// </summary>
/// <param name="token">取消令牌,用于终止分发循环</param>
private async Task StatusDistributorLoopAsync(CancellationToken token)
{
try
{
// [修复 Bug π] 关键修复点
// 使用 CancellationToken.None 作为 WaitToReadAsync 的参数
// 含义:即使 token 被取消,只要 Channel 里还有数据,就继续读取,直到 Channel 被 Complete 且为空
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}");
}
// 退出条件:仅当明确取消 且 队列已空 时才退出
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}"); }
}
/// <summary>
/// 更新设备状态并写入通道
/// 线程安全,采用 DropOldest 策略防止状态队列溢出
/// </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;
// 尝试写入有界通道如果满了则丢弃旧数据DropOldest策略在构造时指定
_statusQueue.Writer.TryWrite(new StatusChangedEventArgs(status, msg, ex, ex?.RawErrorCode));
}
}
#endregion
#region --- (Abstract Methods) ---
/// <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 --- (Disposal) ---
/// <summary>
/// [Fix Bug A: 死锁终结者] 同步销毁入口
/// 原理:强制启动一个新的后台 Task 执行 DisposeAsync并同步阻塞等待其完成
/// 效果:彻底规避了在 UI 线程直接 wait 导致的死锁问题
/// </summary>
public void Dispose()
{
Task.Run(async () => await DisposeAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
}
/// <summary>
/// 异步销毁资源
/// 包含:停止业务、关闭管道、断开事件引用、释放非托管资源
/// </summary>
public virtual async ValueTask DisposeAsync()
{
// 1. 停止业务逻辑
await StopAsync().ConfigureAwait(false);
// 2. [Fix Bug π] 优雅关闭状态管道
_statusQueue.Writer.TryComplete(); // 标记不再接受新数据
_distributorCts?.Cancel(); // 通知消费者准备退出
if (_distributorTask != null)
{
// 3. 等待分发器处理完剩余消息
// 给予 500ms 的宽限期,防止无限等待
await Task.WhenAny(_distributorTask, Task.Delay(500)).ConfigureAwait(false);
}
// 4. [Fix Bug ε] 强力切断事件引用
// 防止 UI 控件忘记取消订阅导致的内存泄漏
FrameReceived = null;
StatusChanged = null;
// 5. 释放基础资源
_lifecycleLock.Dispose();
_distributorCts?.Dispose();
GC.SuppressFinalize(this);
}
#endregion
#region --- (Internal Fields) ---
// 用于跟踪上一个未完成的生命周期任务
private Task _pendingLifecycleTask = Task.CompletedTask;
#endregion
}