修复由于流控导致的帧率降低,由于摄像头过来的帧不是平均的 40ms 一帧,所以需要容错

This commit is contained in:
2025-12-26 13:38:28 +08:00
parent 365e63c21a
commit 93782bcdf1
3 changed files with 73 additions and 64 deletions

View File

@@ -1,113 +1,118 @@
namespace SHH.CameraSdk; namespace SHH.CameraSdk;
/// <summary> /// <summary>
/// 帧控制器(帧调度核心 /// 帧控制器(混合模式最终版
/// 功能:管理订阅者的帧需求,基于需求动态判定每帧的处理命运(保留/丢弃、分发目标) /// 策略:
/// 核心逻辑:采用“并集采样”策略,满足任意订阅者的帧率需求即保留帧,避免重复采样浪费资源 /// 1. 高速直通 (> 20 FPS):直接放行,保留硬件原始流畅度与波动。
/// 2. 低速精控 (<= 20 FPS):使用积分算法进行精准降采样。
/// </summary> /// </summary>
public class FrameController public class FrameController
{ {
#region --- (Private Resources & States) --- // 需求字典
/// <summary> 订阅者帧需求集合(线程安全) </summary>
/// <remarks> Key订阅者AppIdValue该订阅者的帧需求配置 </remarks>
private readonly ConcurrentDictionary<string, FrameRequirement> _requirements = new(); private readonly ConcurrentDictionary<string, FrameRequirement> _requirements = new();
/// <summary> 全局决策序列号(原子递增,确保决策唯一标识) </summary> // 积分累加器(仅用于 <= 20 FPS 的精准控制)
private readonly ConcurrentDictionary<string, int> _accumulators = new();
private long _globalSequence = 0; private long _globalSequence = 0;
#endregion // 逻辑基准分母:假设标准输入是 25 帧
// 用于低帧率时的积分计算基准
private const int LOGICAL_BASE_FPS = 25;
#region --- (Requirement Management) --- // ---------------------------------------------------------
// 注册与注销
// ---------------------------------------------------------
/// <summary>
/// 注册/更新订阅者的帧需求
/// 功能:新增订阅者需求或更新已有订阅者的目标帧率
/// </summary>
/// <param name="appId">订阅者唯一标识(如 "RemoteClient_01"、"AI_Behavior_Engine"</param>
/// <param name="fps">目标帧率单位fps需大于0否则视为无效需求</param>
public void Register(string appId, int fps) public void Register(string appId, int fps)
{ {
// 新增或更新需求:不存在则创建,存在则更新目标帧率
_requirements.AddOrUpdate(appId, _requirements.AddOrUpdate(appId,
addValueFactory: _ => new FrameRequirement { AppId = appId, TargetFps = fps }, _ => new FrameRequirement { AppId = appId, TargetFps = fps },
updateValueFactory: (_, oldRequirement) => (_, old) => { old.TargetFps = fps; return old; });
{
oldRequirement.TargetFps = fps; // 重置该用户的积分器,确保新策略从零开始
return oldRequirement; _accumulators.TryRemove(appId, out _);
});
} }
/// <summary>
/// [新增] 注销订阅者的帧需求
/// 功能:移除指定订阅者的配置,该订阅者将不再收到任何分发帧
/// </summary>
/// <param name="appId">订阅者唯一标识</param>
public void Unregister(string appId) public void Unregister(string appId)
{ {
// ConcurrentDictionary.TryRemove 是原子的、线程安全的
// out _ 表示我们要丢弃移除出的对象,因为我们只关心移除动作本身
_requirements.TryRemove(appId, out _); _requirements.TryRemove(appId, out _);
_accumulators.TryRemove(appId, out _); // 同步清理,防止内存泄漏
} }
#endregion // ---------------------------------------------------------
// 核心决策逻辑
#region --- (Frame Decision Generation) --- // ---------------------------------------------------------
/// <summary> /// <summary>
/// [热路径] 判定当前物理帧是否需要保留并分发 /// 混合决策:高速直通 + 低速精控
/// 核心逻辑:并集采样,只要任意订阅者达到采样间隔,就保留该帧并分发至对应订阅者
/// </summary> /// </summary>
/// <param name="currentTick">当前系统时间戳(单位:毫秒,建议使用 Environment.TickCount64</param> /// <param name="currentTick">兼容参数(内部逻辑不强依赖</param>
/// <returns>帧决策结果(包含是否保留、分发目标等信息)</returns> /// <param name="ignoredRealFps">兼容参数(已忽略,防止震荡)</param>
public FrameDecision MakeDecision(long currentTick) public FrameDecision MakeDecision(long currentTick, int ignoredRealFps = 0)
{ {
// 初始化决策对象,生成唯一序列号与时间戳
var decision = new FrameDecision var decision = new FrameDecision
{ {
Sequence = Interlocked.Increment(ref _globalSequence), // 原子递增,线程安全 Sequence = Interlocked.Increment(ref _globalSequence),
Timestamp = DateTime.Now Timestamp = DateTime.Now
}; };
// 遍历所有订阅者需求,判定是否需要为该订阅者保留当前帧
foreach (var req in _requirements.Values) foreach (var req in _requirements.Values)
{ {
// 跳过无效需求目标帧率≤0
if (req.TargetFps <= 0) continue; 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); decision.TargetAppIds.Add(req.AppId);
// 更新该订阅者的上次采样时间,避免重复采样
// 扣除成本,保留余数 (余数是精度的关键)
acc -= LOGICAL_BASE_FPS;
req.LastCaptureTick = currentTick; 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; decision.IsCaptured = decision.TargetAppIds.Count > 0;
return decision; return decision;
} }
#endregion // ---------------------------------------------------------
// 辅助状态查询
/// <summary> // ---------------------------------------------------------
/// [新增] 获取当前所有活跃的订阅需求快照
/// </summary>
public List<dynamic> GetCurrentRequirements() public List<dynamic> GetCurrentRequirements()
{ {
// 将 ConcurrentDictionary 转换为列表返回 return _requirements.Values.Select(r => new { r.AppId, r.TargetFps, LastActive = r.LastCaptureTick }).ToList<dynamic>();
return _requirements.Values.Select(r => new
{
r.AppId,
r.TargetFps,
// 还可以计算一个预计带宽占用,或者上次取帧时间
LastActive = r.LastCaptureTick
}).ToList<dynamic>();
} }
} }

View File

@@ -114,6 +114,9 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable
/// <summary> 上次 FPS 计算的 Tick 时间 </summary> /// <summary> 上次 FPS 计算的 Tick 时间 </summary>
private long _lastFpsCalcTick = 0; private long _lastFpsCalcTick = 0;
// 提供一个最近一秒的输入帧率参考值
public int NominalInputFps => (int)Math.Round(RealFps);
/// <summary> 实时 FPS每秒更新一次 </summary> /// <summary> 实时 FPS每秒更新一次 </summary>
public double RealFps { get; private set; } = 0.0; public double RealFps { get; private set; } = 0.0;

View File

@@ -297,7 +297,8 @@ public class HikVideoSource : BaseVideoSource
{ {
// 1. [核心流控] 询问基类控制器:这帧要不要? // 1. [核心流控] 询问基类控制器:这帧要不要?
// 之前失效是因为操作的是子类被遮蔽的 Controller现在复用基类 Controller逻辑就通了。 // 之前失效是因为操作的是子类被遮蔽的 Controller现在复用基类 Controller逻辑就通了。
var decision = Controller.MakeDecision(Environment.TickCount64); // 传入真实的输入帧率作为参考基准
var decision = Controller.MakeDecision(Environment.TickCount64, (int)RealFps);
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU // 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
if (!decision.IsCaptured) return; if (!decision.IsCaptured) return;