Files
Ayay/SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs
2026-02-08 16:19:48 +08:00

253 lines
9.1 KiB
C#
Raw Permalink 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 AForge.Video.DirectShow;
using OpenCvSharp;
using Serilog;
using System.Runtime.ExceptionServices;
using System.Security;
namespace SHH.CameraSdk;
/// <summary>
/// [USB驱动] 工业级视频源实现 (参考 DahuaVideoSource 逻辑重构版)
/// <para>当前模块: AiVideo | 核心原则: 逻辑对标、高并发防御、零拷贝</para>
/// </summary>
public class UsbVideoSource : BaseVideoSource
{
protected override ILogger _sdkLog => Log.ForContext("SourceContext", "UsbSdk");
#region --- 1. (Static Resources) ---
// USB 驱动通常为本地设备,无需像大华那样维护全局 ConcurrentDictionary 句柄映射
#endregion
#region --- 2. (Instance Members) ---
private VideoCapture? _capture;
private CancellationTokenSource? _cts;
private int _deviceIndex = -1;
private FramePool? _framePool;
private volatile bool _isPoolReady = false;
private readonly object _initLock = new();
#endregion
public UsbVideoSource(VideoSourceConfig config) : base(config) { }
#region --- 3. (Lifecycle Overrides) ---
protected override async Task OnStartAsync(CancellationToken token)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(token);
await Task.Run(() =>
{
lock (_initLock)
{
// 1. 获取配置的标识符 (存放在 RtspPath 中的那串 @device:sw...)
string targetMoniker = _config.RtspPath;
_deviceIndex = -1;
_sdkLog.Information($"[SDK] 正在扫描 USB 设备以匹配 Moniker: {targetMoniker}");
// 2. 使用 AForge 列出所有视频采集设备
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
if (videoDevices.Count == 0)
{
throw new Exception("系统中未检测到任何 USB 摄像头设备。");
}
// 3. 遍历并匹配
for (int i = 0; i < videoDevices.Count; i++)
{
_sdkLog.Debug($"[Scan] Index: {i}, Name: {videoDevices[i].Name}, Moniker: {videoDevices[i].MonikerString}");
// 进行比对
if (videoDevices[i].MonikerString.Equals(targetMoniker, StringComparison.OrdinalIgnoreCase))
{
_deviceIndex = i; // 找到了!这就是 OpenCV 需要的整数索引
_sdkLog.Information($"[Match] 成功匹配到设备: {videoDevices[i].Name}, 对应 OpenCV Index: {i}");
break;
}
}
// 4. 容错处理:如果没找到,尝试 fallback 到 IpAddress 里的索引
if (_deviceIndex == -1)
{
if (int.TryParse(_config.IpAddress, out int fallbackIndex))
{
_deviceIndex = fallbackIndex;
_sdkLog.Warning($"[Match] 未找到匹配的 Moniker退回到使用 IpAddress 指定的索引: {fallbackIndex}");
}
else
{
throw new Exception($"无法找到指定的 USB 设备: {targetMoniker}");
}
}
// 5. 使用 DSHOW 模式打开 OpenCV
_capture = new VideoCapture(_deviceIndex, VideoCaptureAPIs.DSHOW);
_sdkLog.Information($"[SDK] USB 开启成功 => Index:{_deviceIndex}");
AddAuditLog($"[SDK] USB 开启成功");
// ... 后续设置和启动循环逻辑 ...
_ = Task.Run(() => InternalCaptureLoop(_cts.Token), _cts.Token);
}
}, token);
}
protected override async Task OnStopAsync()
{
_cts?.Cancel();
await Task.Run(() =>
{
lock (_initLock)
{
if (_capture != null)
{
_capture.Release();
_capture.Dispose();
_capture = null;
}
_framePool?.Dispose();
_isPoolReady = false;
_sdkLog.Information($"[SDK] USB 已停止 ID:{Id}");
}
});
}
#endregion
#region --- 4. (Core Logic) ---
/// <summary>
/// 内部采集循环:逻辑完全对标大华的 ProcessRealData + SafeOnDecodingCallBack
/// </summary>
private void InternalCaptureLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (_capture == null || !_capture.IsOpened()) break;
// 1. 探测当前分辨率
int currentWidth = (int)_capture.FrameWidth;
int currentHeight = (int)_capture.FrameHeight;
if (currentWidth <= 0 || currentHeight <= 0)
{
Thread.Sleep(10);
continue;
}
// 2. 模拟大华的“心跳”统计
MarkFrameReceived(0);
// 3. 逻辑对标SafeOnDecodingCallBack 中的“防死锁重建”
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
bool lockTaken = false;
try
{
Monitor.TryEnter(_initLock, 50, ref lockTaken);
if (lockTaken)
{
if (!_isPoolReady || Width != currentWidth || Height != currentHeight)
{
_sdkLog.Information($"[Res] USB分辨率变更: {Width}x{Height} -> {currentWidth}x{currentHeight}");
_framePool?.Dispose();
Width = currentWidth;
Height = currentHeight;
// 重建帧池 (对标大华参数: 3, 5)
_framePool = new FramePool(currentWidth, currentHeight, MatType.CV_8UC3, 3, 5);
_isPoolReady = true;
}
}
else continue;
}
finally { if (lockTaken) Monitor.Exit(_initLock); }
}
// 4. [流控决策] 复用基类控制器
// 注意:这里 decision 的类型取决于你基类的返回定义,通常为 StreamDecision
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
if (!decision.IsCaptured)
{
_capture.Grab(); // 仅抓取不解码,保持底层缓冲区最新
continue;
}
// 5. 执行数据处理
ExecuteFrameProcessing(decision);
}
catch (Exception ex)
{
_sdkLog.Error(ex, $"[SDK] USB 采集循环异常 ID:{Id}");
Thread.Sleep(500); // 异常规避
}
}
}
/// <summary>
/// 帧处理逻辑:对标大华的 SafeOnDecodingCallBack 内部逻辑
/// </summary>
[HandleProcessCorruptedStateExceptions]
[SecurityCritical]
private void ExecuteFrameProcessing(dynamic decision) // 使用 dynamic 规避特定的类定义编译问题
{
SmartFrame? smartFrame = null;
try
{
smartFrame = _framePool?.Get();
if (smartFrame == null) return;
// [核心操作:零拷贝思想]
// 直接将 USB 原始 BGR 数据读入 SmartFrame 分配好的内存中
if (_capture != null && _capture.Read(smartFrame.InternalMat))
{
// 🛡️ [空结果防御]
if (smartFrame.InternalMat.Empty())
{
_sdkLog.Warning($"[SDK] USB 解码后 Mat 为空. ID:{Id}");
return;
}
Cv2.CvtColor(smartFrame.InternalMat, smartFrame.InternalMat, ColorConversionCodes.BGR2RGB);
// 填充订阅者
foreach (var appId in decision.TargetAppIds)
smartFrame.SubscriberIds.Enqueue(appId);
// 发送到全局路由 (Router 内部必须 AddRef)
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
}
}
catch (Exception ex)
{
_sdkLog.Error(ex, $"[SDK] USB 帧转换流程异常. ID:{Config.Id}");
AddAuditLog($"[SDK] USB 处理异常: {ex.Message}");
}
finally
{
// ♻️ [引用闭环] 对标大华的 finally Dispose
smartFrame?.Dispose();
}
}
#endregion
#region --- 5. ---
protected override void OnApplyOptions(DynamicStreamOptions options) { }
protected override Task<DeviceMetadata> OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata());
#endregion
}