RK3588S + SRS + Qt 播放器:RTMP 直播流卡顿与高延迟问题排查记录

环境说明

  • 推流端:RK3588S,V4L2 采集 → RKNN NPU(YOLOv8s 目标检测)→ MPP VPU 硬编 H.264 → RTMP 推流
  • 服务端:SRS 4.x,局域网部署
  • 播放端:Windows 11,Qt5 + FFmpeg + D3D11VA 硬解,自研播放器 RambosPlayer
  • 流参数:640×480,15fps,GOP=8,纯视频流(无音频)

问题一:画面卡顿,实际渲染帧率退化至 1.7fps

现象

UI 显示帧率 15fps,但画面肉眼可见卡顿,约每 533ms "闪"一次,每次闪过多帧。

排查

在 VideoRenderer::onTimer() 加渲染时间戳日志,发现帧的渲染时刻集中在同一毫秒内:

  [21:25:09.605] render pts=5.343
  [21:25:09.605] render pts=5.410
  [21:25:09.606] render pts=5.477
  ...(8帧全在 <5ms 内渲完)
  [21:25:10.139] render pts=5.943  ← 冻屏 533ms 后

原因链路:SRS 默认开启 gop_cache,将一整个 GOP(8帧)缓存后一次性推给客户端 → FFmpeg 收包后立即入队 → VideoRenderer 的 1ms QTimer 在极短时间内连续取帧渲染 → 8帧渲完后队列空,冻屏等下一个 GOP。

帧率统计是准确的(150帧 / 10秒 = 15fps),但时序完全错误。

解决方案

无音频时钟的纯视频直播流,引入 Live Pacing 节拍控制:

  if (audioClock < 0.0) {
      if (livePacingStartPts_ >= 0.0) {
          double expectedMs = (pts - livePacingStartPts_) * 1000.0;
          qint64 actualMs   = livePacingTimer_.elapsed();
          double holdMs     = expectedMs - actualMs;
          if (holdMs > 2.0) {
              pendingFrame_ = frame;
              return;
          }
      }
      livePacingTimer_.start();
      livePacingStartPts_ = pts;
  }

锚点选择:使用短程锚点(每帧渲染后更新),而非全局第一帧锚点。

原因:AlertGateway 实际编码帧率约 14.6fps,PTS timebase 标称 15fps,偏差约 1%。若使用全局锚点,holdMs = (pts - startPts) * 1000 - elapsed 中的误差会线性积累——播放 10 分钟,holdMs 多出约 6 秒,等同于引入了 6
秒延迟。短程锚点每次只计算相邻帧间隔,偏差不积累。


问题二:RTMP 连接延迟约 6 秒

现象

调用 avformat_open_input + avformat_find_stream_info 耗时约 6 秒才返回,之后画面才出现。

排查

avformat_find_stream_info 对直播流的默认探测窗口为 5 秒,用于收集足够多的包来分析流参数。SRS 的 gop_cache 在连接建立时会向客户端推送一个完整的缓存 GOP,FFmpeg 将这段突发数据纳入探测期,实际等待时间更长。

解决方案

连接选项(传入 avformat_open_input):

  av_dict_set(&opts, "rtmp_buffer", "0", 0);   // SRS 服务端关闭预缓冲
  av_dict_set(&opts, "fflags",  "nobuffer", 0); // FFmpeg 接收层不缓冲

打开成功后直接设字段:

  fmtCtx->flags |= AVFMT_FLAG_NOBUFFER;
  fmtCtx->max_analyze_duration = 0;  // 收到 SPS/PPS 后立即返回

reconnect() 重连路径同步应用相同设置,防止断线重连后回退。

SRS 配置:

  min_latency  on;
  gop_cache    off;
  queue_length 0.1;

效果:连接延迟从 ~6s 降至 ~0.5s,剩余 0.5s 为 1 个 GOP 的固有传输延迟(物理下限)。


问题三:长时间播放后延迟持续升高

现象

播放约 5 分钟后,视频与实时画面的延迟明显增大,重新连接后恢复。

排查

在 DemuxThread::run() 和 VideoRenderer::onTimer() 分别加入诊断统计,每隔固定周期输出:

lag = wallElapsed - ptsElapsed

  • lag > 0:PTS 推进慢于挂钟,正常状态
  • lag < 0:PTS 推进快于挂钟,帧从 TCP 缓冲突发读取,存在积压

18 分钟会话日志节选:

  [+5s]   DemuxThread lag = +74ms
  [+60s]  DemuxThread lag = +1ms
  [+90s]  DemuxThread lag = -47ms
  [+18min] DemuxThread lag = -1519ms

lag 以稳定斜率线性下降,排除 TCP 突发积压(那种情况斜率不会如此恒定)。

计算偏差率:1519ms / 1068s = 0.142%,对应实际编码帧率:

f = 1 / (66.67ms × (1 - 0.00142)) ≈ 15.021fps

AlertGateway 实际编码帧率约 15.02fps,而 PTS timebase 标称 15fps,正是这 0.14% 的偏差导致 PTS 时间轴比挂钟持续"超前"。

结论

这个负 lag 本身不构成延迟问题。VideoRenderer 帧间隔全程稳定在 66.6ms,全程无队列空窗告警,用户主观感受无延迟。问题根源是全局锚点在该偏差下的累积效应,短程锚点方案已从根本上消除该问题。


总结

问题一:画面卡顿 1.7fps

  • 根因:SRS gop_cache 批量投递,VideoRenderer 瞬间吞完一整个 GOP
  • 方案:Live Pacing 短程锚点节拍控制

问题二:连接延迟 6s

  • 根因:find_stream_info 默认 5s 探测窗口
  • 方案:nobuffer + max_analyze_duration=0

问题三:长时间延迟漂移

  • 根因:全局锚点 × 编码器帧率偏差线性积累
  • 方案:改为短程锚点,每帧重置基准

核心修改量:videorenderer.cpp 约 20 行,demuxthread.cpp 约 10 行。


附:诊断方法

直播流延迟问题难以定位,推荐在以下两处埋点 lag = wallElapsed - ptsElapsed:

  • DemuxThread:每隔 5s 输出一次,判断是否存在 TCP 层积压
  • VideoRenderer:每隔 N 帧输出一次,验证渲染时序是否与 PTS 对齐

两处 lag 同步为负且斜率恒定 → 编码器 timebase 偏差;DemuxThread lag 快速变负而 VideoRenderer lag 滞后 → TCP 缓冲积压。两种情况根因不同,解法完全不同,这个指标能快速区分。
github地址:https://github.com/johnjiamzhong-project/RambosPlayer

posted @ 2026-06-16 22:29  rambos1996  阅读(20)  评论(0)    收藏  举报