From c8f25aeba5510be4d9dab43a795d6531acd0597f Mon Sep 17 00:00:00 2001 From: wilson <3518499@qq.com> Date: Sun, 8 Feb 2026 16:19:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20USB=20=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SHH.CameraSdk/Core/Manager/CameraManager.cs | 1 + SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs | 253 ++++++++++++++++++++ SHH.CameraSdk/SHH.CameraSdk.csproj | 1 + 3 files changed, 255 insertions(+) create mode 100644 SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index 0323c9a..05be56e 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -366,6 +366,7 @@ public class CameraManager : IDisposable, IAsyncDisposable { DeviceBrand.HikVision => new HikVideoSource(config), DeviceBrand.Dahua => new DahuaVideoSource(config), + DeviceBrand.Usb => new UsbVideoSource(config), // 使用模式匹配获取不匹配的值,记录详细的 DTO 上下文 _ => HandleUnsupportedBrand(config) diff --git a/SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs b/SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs new file mode 100644 index 0000000..8832129 --- /dev/null +++ b/SHH.CameraSdk/Drivers/Usb/UsbVideoSource.cs @@ -0,0 +1,253 @@ +using AForge.Video.DirectShow; +using OpenCvSharp; +using Serilog; +using System.Runtime.ExceptionServices; +using System.Security; + +namespace SHH.CameraSdk; + +/// +/// [USB驱动] 工业级视频源实现 (参考 DahuaVideoSource 逻辑重构版) +/// 当前模块: AiVideo | 核心原则: 逻辑对标、高并发防御、零拷贝 +/// +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) --- + + /// + /// 内部采集循环:逻辑完全对标大华的 ProcessRealData + SafeOnDecodingCallBack + /// + 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); // 异常规避 + } + } + } + + /// + /// 帧处理逻辑:对标大华的 SafeOnDecodingCallBack 内部逻辑 + /// + [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 OnFetchMetadataAsync() => Task.FromResult(new DeviceMetadata()); + + #endregion +} \ No newline at end of file diff --git a/SHH.CameraSdk/SHH.CameraSdk.csproj b/SHH.CameraSdk/SHH.CameraSdk.csproj index 3809a3f..978a1b9 100644 --- a/SHH.CameraSdk/SHH.CameraSdk.csproj +++ b/SHH.CameraSdk/SHH.CameraSdk.csproj @@ -16,6 +16,7 @@ +