diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs index cac6691..ed42fbf 100644 --- a/SHH.CameraSdk/Controllers/CamerasController.cs +++ b/SHH.CameraSdk/Controllers/CamerasController.cs @@ -280,4 +280,19 @@ public class CamerasController : ControllerBase UseGrayscale = dto.UseGrayscale }; } + + /// + /// [新增] 查询某台设备的当前流控策略 + /// + [HttpGet("{id}/subscriptions")] + public IActionResult GetSubscriptions(long id) + { + var device = _manager.GetDevice(id); + if (device == null) return NotFound(); + + // 调用刚才在 FrameController 写的方法 + var subs = device.Controller.GetCurrentRequirements(); + + return Ok(subs); + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Manager/CameraManager.cs b/SHH.CameraSdk/Core/Manager/CameraManager.cs index dd82281..6599c7d 100644 --- a/SHH.CameraSdk/Core/Manager/CameraManager.cs +++ b/SHH.CameraSdk/Core/Manager/CameraManager.cs @@ -197,6 +197,7 @@ public class CameraManager : IDisposable, IAsyncDisposable Status = cam.Status.ToString(), IsOnline = cam.IsOnline, Fps = cam.RealFps, + Bitrate = cam.RealBitrate, // [新增] 映射基类属性 TotalFrames = cam.TotalFrames, HealthScore = healthScore, LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null, diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs index 9304c42..899b2fb 100644 --- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs +++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs @@ -95,4 +95,19 @@ public class FrameController } #endregion + + /// + /// [新增] 获取当前所有活跃的订阅需求快照 + /// + public List GetCurrentRequirements() + { + // 将 ConcurrentDictionary 转换为列表返回 + return _requirements.Values.Select(r => new + { + r.AppId, + r.TargetFps, + // 还可以计算一个预计带宽占用,或者上次取帧时间 + LastActive = r.LastCaptureTick + }).ToList(); + } } \ No newline at end of file diff --git a/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs b/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs index 0d3158f..16c6e17 100644 --- a/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs +++ b/SHH.CameraSdk/Core/Telemetry/CameraTelemetryInfo.cs @@ -41,6 +41,9 @@ public class CameraTelemetryInfo /// 累计接收帧数(相机启动后接收的总帧数,用于统计数据完整性) public long TotalFrames { get; set; } + /// 实时码率 (Mbps) + public double Bitrate { get; set; } + #endregion #region --- 健康度与统计 (Health & Statistics) --- diff --git a/SHH.CameraSdk/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index c372cb9..c584e37 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -119,6 +119,7 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable /// 实时码率 (Mbps) protected double _currentBitrate = 0; + public double RealBitrate => _currentBitrate; /// 码率计算临时字节计数器 private long _tempByteCounter = 0; @@ -404,43 +405,66 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable } /// - /// 标记帧接收事件(心跳保活 + FPS/码率统计) + /// 标记数据接收(心跳保活 + 双路统计) + /// 调用规则: + /// 1. 网络层收到流数据时:调用 MarkFrameReceived(dwBufSize),只统计流量。 + /// 2. 解码层流控通过后:调用 MarkFrameReceived(0),只统计有效帧率。 /// - /// 当前帧字节大小 + /// 数据包大小(字节),0 表示这是一帧解码后的图像 protected void MarkFrameReceived(uint dataSize = 0) { - var now = Environment.TickCount64; + long now = Environment.TickCount64; - // 1. 更新心跳时间戳(原子操作) + // 1. [心跳保活] 无论网络包还是解码帧,都视为设备“活着” + // 使用 Interlocked 保证多线程读写安全 Interlocked.Exchange(ref _lastFrameTick, now); - // 2. 累加总帧数(原子操作) - Interlocked.Increment(ref _totalFramesReceived); - - // 3. 累加临时计数器(用于 FPS/码率计算) - _tempFrameCounter++; - _tempByteCounter += dataSize; - - // 4. 每秒结算一次统计指标 - var timeDiff = now - _lastFpsCalcTick; - if (timeDiff >= 1000 && _lastFpsCalcTick > 0) + // 2. [分流累加] 根据来源不同,累加不同的计数器 + if (dataSize > 0) { - var duration = timeDiff / 1000.0; - - // 计算实时 FPS (保留 1 位小数) - RealFps = Math.Round(_tempFrameCounter / duration, 1); - - // 计算实时码率 (Mbps) = (字节数 * 8) / 1024 / 1024 / 秒 - _currentBitrate = Math.Round((_tempByteCounter * 8.0) / 1024 / 1024 / duration, 2); - - // 重置临时计数器 - _lastFpsCalcTick = now; - _tempFrameCounter = 0; - _tempByteCounter = 0; + // --- 来源:网络层回调 (SafeOnRealDataReceived) --- + // 只累加字节数,用于计算带宽 (Mbps) + // 绝对不能在这里累加帧数,否则会被网络包的数量误导(导致 FPS 虚高) + Interlocked.Add(ref _tempByteCounter, dataSize); } - else if (_lastFpsCalcTick == 0) + else { - // 初始化 FPS 计算起始时间 + // --- 来源:解码层回调 (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) / 1024 / 1024 / duration, 2); + } + else + { + // 初始化重置 + _tempFrameCounter = 0; + _tempByteCounter = 0; + } + + // 更新结算时间锚点 _lastFpsCalcTick = now; } } diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index a53552c..279e3b3 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -244,7 +244,8 @@ public class HikVideoSource : BaseVideoSource { try { - // [优化] 维持心跳,防止被哨兵误杀 + // 【关键位置】:在此处调用,统计网络层收到的每一字节数据 + // 因为 dwBufSize > 0,MarkFrameReceived 内部只会累加码流,不会增加 FPS 计数 MarkFrameReceived(dwBufSize); if (_realPlayHandle == -1) return; @@ -301,6 +302,9 @@ public class HikVideoSource : BaseVideoSource // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return; + // [优化] 维持心跳,防止被哨兵误杀 + MarkFrameReceived(0); + int width = pFrameInfo.nWidth; int height = pFrameInfo.nHeight; diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs index cc11a0c..9e4be59 100644 --- a/SHH.CameraSdk/Program.cs +++ b/SHH.CameraSdk/Program.cs @@ -122,56 +122,16 @@ namespace SHH.CameraSdk if (manager.GetDevice(101) is HikVideoSource hikCamera) { - // 2. 注册需求 (告诉控制器我要什么) - // ---------------------------------------------------- - hikCamera.Controller.Register("WPF_Display_Main", 8); // UI 要 8 帧 - hikCamera.Controller.Register("AI_Behavior_Engine", 2); // AI 要 2 帧 - // 1. 注册差异化需求 (给每个消费者唯一的 AppId) // ---------------------------------------------------- - // 模拟:A 进程(如远程预览)带宽有限,只要 3fps - hikCamera.Controller.Register("Process_A_Remote", 20); + // 1. 注册需求时,手动加上 _Display 后缀 + hikCamera.Controller.Register("Process_A_Remote_Display", 20); - // 模拟:B 进程(如本地大屏)性能强劲,要 8fps - hikCamera.Controller.Register("Process_B_Local", 8); - - // 模拟:AI 引擎 - hikCamera.Controller.Register("AI_Engine_Core", 2); - - // [已移除] 这里的 using var remoteRenderer = ... 已被移除 - // 改为使用传入的 renderer 参数,确保其生命周期受控于 Main - - // 2. 精准订阅 (Subscribe 替代了 +=) - // ---------------------------------------------------- - - // [消费者 A] - 绝对只会收到 3fps - GlobalStreamDispatcher.Subscribe("Process_A_Remote", 101, frame => + // 2. 订阅时,也改用带后缀的名称 + GlobalStreamDispatcher.Subscribe("Process_A_Remote_Display", 101, frame => { - // 关键:增加引用计数,防止在投递过程中被 Pipeline 回收 frame.AddRef(); - - // 投递到渲染线程 (FrameConsumer) renderer.Enqueue(frame); - - //Console.WriteLine("Frame Enqueued"); - }); - - // [消费者 B] - 绝对只会收到 8fps - GlobalStreamDispatcher.Subscribe("Process_B_Local", (deviceId, frame) => - { - //if (deviceId == 101) - //{ - // Console.WriteLine($"[Process B] 本地渲染一帧 (8fps节奏)"); - //} - }); - - // [消费者 AI] - GlobalStreamDispatcher.Subscribe("AI_Engine_Core", (deviceId, frame) => - { - //if (deviceId == 101) - //{ - // Console.WriteLine($" >>> [AI] 分析一帧..."); - //} }); } }