From 93782bcdf19c4322cb2a3b908ed013562d25e496 Mon Sep 17 00:00:00 2001 From: twice109 <3518499@qq.com> Date: Fri, 26 Dec 2025 13:38:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=B1=E4=BA=8E=E6=B5=81?= =?UTF-8?q?=E6=8E=A7=E5=AF=BC=E8=87=B4=E7=9A=84=E5=B8=A7=E7=8E=87=E9=99=8D?= =?UTF-8?q?=E4=BD=8E=EF=BC=8C=E7=94=B1=E4=BA=8E=E6=91=84=E5=83=8F=E5=A4=B4?= =?UTF-8?q?=E8=BF=87=E6=9D=A5=E7=9A=84=E5=B8=A7=E4=B8=8D=E6=98=AF=E5=B9=B3?= =?UTF-8?q?=E5=9D=87=E7=9A=84=2040ms=20=E4=B8=80=E5=B8=A7=EF=BC=8C?= =?UTF-8?q?=E6=89=80=E4=BB=A5=E9=9C=80=E8=A6=81=E5=AE=B9=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Scheduling/FrameController.cs | 131 +++++++++--------- SHH.CameraSdk/Drivers/BaseVideoSource.cs | 3 + .../Drivers/HikVision/HikVideoSource.cs | 3 +- 3 files changed, 73 insertions(+), 64 deletions(-) diff --git a/SHH.CameraSdk/Core/Scheduling/FrameController.cs b/SHH.CameraSdk/Core/Scheduling/FrameController.cs index 899b2fb..2b5ff9b 100644 --- a/SHH.CameraSdk/Core/Scheduling/FrameController.cs +++ b/SHH.CameraSdk/Core/Scheduling/FrameController.cs @@ -1,113 +1,118 @@ namespace SHH.CameraSdk; /// -/// 帧控制器(帧调度核心) -/// 功能:管理订阅者的帧需求,基于需求动态判定每帧的处理命运(保留/丢弃、分发目标) -/// 核心逻辑:采用“并集采样”策略,满足任意订阅者的帧率需求即保留帧,避免重复采样浪费资源 +/// 帧控制器(混合模式最终版) +/// 策略: +/// 1. 高速直通 (> 20 FPS):直接放行,保留硬件原始流畅度与波动。 +/// 2. 低速精控 (<= 20 FPS):使用积分算法进行精准降采样。 /// public class FrameController { - #region --- 私有资源与状态 (Private Resources & States) --- - - /// 订阅者帧需求集合(线程安全) - /// Key:订阅者AppId,Value:该订阅者的帧需求配置 + // 需求字典 private readonly ConcurrentDictionary _requirements = new(); - /// 全局决策序列号(原子递增,确保决策唯一标识) + // 积分累加器(仅用于 <= 20 FPS 的精准控制) + private readonly ConcurrentDictionary _accumulators = new(); + private long _globalSequence = 0; - #endregion + // 逻辑基准分母:假设标准输入是 25 帧 + // 用于低帧率时的积分计算基准 + private const int LOGICAL_BASE_FPS = 25; - #region --- 需求管理 (Requirement Management) --- + // --------------------------------------------------------- + // 注册与注销 + // --------------------------------------------------------- - /// - /// 注册/更新订阅者的帧需求 - /// 功能:新增订阅者需求或更新已有订阅者的目标帧率 - /// - /// 订阅者唯一标识(如 "RemoteClient_01"、"AI_Behavior_Engine") - /// 目标帧率(单位:fps,需大于0,否则视为无效需求) public void Register(string appId, int fps) { - // 新增或更新需求:不存在则创建,存在则更新目标帧率 _requirements.AddOrUpdate(appId, - addValueFactory: _ => new FrameRequirement { AppId = appId, TargetFps = fps }, - updateValueFactory: (_, oldRequirement) => - { - oldRequirement.TargetFps = fps; - return oldRequirement; - }); + _ => new FrameRequirement { AppId = appId, TargetFps = fps }, + (_, old) => { old.TargetFps = fps; return old; }); + + // 重置该用户的积分器,确保新策略从零开始 + _accumulators.TryRemove(appId, out _); } - /// - /// [新增] 注销订阅者的帧需求 - /// 功能:移除指定订阅者的配置,该订阅者将不再收到任何分发帧 - /// - /// 订阅者唯一标识 public void Unregister(string appId) { - // ConcurrentDictionary.TryRemove 是原子的、线程安全的 - // out _ 表示我们要丢弃移除出的对象,因为我们只关心移除动作本身 _requirements.TryRemove(appId, out _); + _accumulators.TryRemove(appId, out _); // 同步清理,防止内存泄漏 } - #endregion - - #region --- 帧决策生成 (Frame Decision Generation) --- + // --------------------------------------------------------- + // 核心决策逻辑 + // --------------------------------------------------------- /// - /// [热路径] 判定当前物理帧是否需要保留并分发 - /// 核心逻辑:并集采样,只要任意订阅者达到采样间隔,就保留该帧并分发至对应订阅者 + /// 混合决策:高速直通 + 低速精控 /// - /// 当前系统时间戳(单位:毫秒,建议使用 Environment.TickCount64) - /// 帧决策结果(包含是否保留、分发目标等信息) - public FrameDecision MakeDecision(long currentTick) + /// 兼容参数(内部逻辑不强依赖) + /// 兼容参数(已忽略,防止震荡) + public FrameDecision MakeDecision(long currentTick, int ignoredRealFps = 0) { - // 初始化决策对象,生成唯一序列号与时间戳 var decision = new FrameDecision { - Sequence = Interlocked.Increment(ref _globalSequence), // 原子递增,线程安全 + Sequence = Interlocked.Increment(ref _globalSequence), Timestamp = DateTime.Now }; - // 遍历所有订阅者需求,判定是否需要为该订阅者保留当前帧 foreach (var req in _requirements.Values) { - // 跳过无效需求(目标帧率≤0) if (req.TargetFps <= 0) continue; - // 计算该订阅者的采样间隔(毫秒):1000ms / 目标帧率 - long interval = 1000 / req.TargetFps; + //// ========================================================= + //// 【策略 A】 高速直通区 (> 20 FPS) + //// ========================================================= + //// 用户想要 21~25+ 帧,或者全速。 + //// 此时不做任何干预,相机来多少发多少,保留原始的 24-26 波动。 + //if (req.TargetFps > 20) + //{ + // decision.TargetAppIds.Add(req.AppId); + // req.LastCaptureTick = currentTick; // 更新活跃状态 + // continue; + //} - // 判定是否达到采样时间:当前时间 - 上次采样时间 ≥ 采样间隔(允许1s内相位对齐,自动合并) - if (currentTick - req.LastCaptureTick >= interval) + // ========================================================= + // 【策略 B】 低速精控区 (<= 20 FPS) -> 积分算法 + // ========================================================= + // 解决 "16帧" 问题,保证 1帧、5帧、15帧 的绝对精准 + + // 1. 获取积分 + int acc = _accumulators.GetOrAdd(req.AppId, 0); + + // 2. 累加:每来一帧,积攒 "TargetFps" 分 + acc += req.TargetFps; + + // 3. 判定:是否攒够了 25 分 (逻辑基准) + if (acc >= LOGICAL_BASE_FPS) { - // 加入分发目标列表 + // 发货 decision.TargetAppIds.Add(req.AppId); - // 更新该订阅者的上次采样时间,避免重复采样 + + // 扣除成本,保留余数 (余数是精度的关键) + acc -= LOGICAL_BASE_FPS; + req.LastCaptureTick = currentTick; } + + // 4. 防爆桶机制:如果累积太多(例如相机推流极快),限制封顶 + // 防止下一秒瞬间吐出太多帧 + if (acc > LOGICAL_BASE_FPS) acc = LOGICAL_BASE_FPS; + + // 5. 写回状态 + _accumulators[req.AppId] = acc; } - // 判定是否保留该帧:存在分发目标则保留(IsCaptured=true),否则丢弃 decision.IsCaptured = decision.TargetAppIds.Count > 0; - return decision; } - #endregion - - /// - /// [新增] 获取当前所有活跃的订阅需求快照 - /// + // --------------------------------------------------------- + // 辅助状态查询 + // --------------------------------------------------------- public List GetCurrentRequirements() { - // 将 ConcurrentDictionary 转换为列表返回 - return _requirements.Values.Select(r => new - { - r.AppId, - r.TargetFps, - // 还可以计算一个预计带宽占用,或者上次取帧时间 - LastActive = r.LastCaptureTick - }).ToList(); + 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/Drivers/BaseVideoSource.cs b/SHH.CameraSdk/Drivers/BaseVideoSource.cs index c584e37..0661809 100644 --- a/SHH.CameraSdk/Drivers/BaseVideoSource.cs +++ b/SHH.CameraSdk/Drivers/BaseVideoSource.cs @@ -114,6 +114,9 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable /// 上次 FPS 计算的 Tick 时间 private long _lastFpsCalcTick = 0; + // 提供一个最近一秒的输入帧率参考值 + public int NominalInputFps => (int)Math.Round(RealFps); + /// 实时 FPS(每秒更新一次) public double RealFps { get; private set; } = 0.0; diff --git a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs index 279e3b3..2c77a00 100644 --- a/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs +++ b/SHH.CameraSdk/Drivers/HikVision/HikVideoSource.cs @@ -297,7 +297,8 @@ public class HikVideoSource : BaseVideoSource { // 1. [核心流控] 询问基类控制器:这帧要不要? // 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。 - var decision = Controller.MakeDecision(Environment.TickCount64); + // 传入真实的输入帧率作为参考基准 + var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps); // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU if (!decision.IsCaptured) return;