增加 USB 驱动的支持
This commit is contained in:
@@ -366,6 +366,7 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
|||||||
{
|
{
|
||||||
DeviceBrand.HikVision => new HikVideoSource(config),
|
DeviceBrand.HikVision => new HikVideoSource(config),
|
||||||
DeviceBrand.Dahua => new DahuaVideoSource(config),
|
DeviceBrand.Dahua => new DahuaVideoSource(config),
|
||||||
|
DeviceBrand.Usb => new UsbVideoSource(config),
|
||||||
|
|
||||||
// 使用模式匹配获取不匹配的值,记录详细的 DTO 上下文
|
// 使用模式匹配获取不匹配的值,记录详细的 DTO 上下文
|
||||||
_ => HandleUnsupportedBrand(config)
|
_ => HandleUnsupportedBrand(config)
|
||||||
|
|||||||
253
SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs
Normal file
253
SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AForge.Video.DirectShow" Version="2.2.5" />
|
||||||
<PackageReference Include="Lennox.LibYuvSharp" Version="1.1.2" />
|
<PackageReference Include="Lennox.LibYuvSharp" Version="1.1.2" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user