视频抖动缓冲与组帧 · 把破碎的包流重建成帧

收到的包乱序、有缺、时快时慢,得先缓冲、排序、拼成完整帧,才能解码。这一章读 video/frame_buffer.h:抖动缓冲怎么吸收网络抖动、组帧怎么等齐一帧的所有包,以及「缓冲大小」在流畅和延迟之间的根本权衡。

作者 David更新于 第 59 篇(共 112 篇)

视频抖动缓冲与组帧 · 把破碎的包流重建成帧

接收侧的第一场硬仗。网络交给你的,是一串乱序、有缺口、时快时慢的 RTP 包——绝不能直接喂解码器。得先缓冲、排序、拼成完整的帧,还要平滑掉网络的抖动。这是抖动缓冲(jitter buffer)+ 组帧的活,video/frame_buffer.h 的 FrameBuffer 是核心。

网络抖动:包到得忽快忽慢

先看要对抗什么。发送端明明是匀速发的(每帧间隔均匀),但经过网络,包的到达时间变得参差不齐——这就是抖动(jitter):

  • 有的包一路顺畅、早到;有的排了队、晚到。
  • 丢的包可能重传后姗姗来迟。

如果收到就立即解码播放,画面就会忽快忽慢地抽搐。抖动缓冲的使命,就是把参差的到达,重新变回匀速的播放。

组帧:等齐一帧的所有包

抖动缓冲干两件交织的事。第一件是组帧:

一帧被切成了多个包,得等它们都到齐才能拼成完整的一帧交给解码——缺一个包,这帧就不完整,解不了。 FrameBuffer 收到包后,按 RTP 时间戳把同一帧的包归拢,靠序号连续 + marker 位判断“这帧的包收齐了没”。收齐 → 拼成一帧,可以往下走;没齐(有缺口)→ 等——等重传补上、或等到超时放弃(触发关键帧请求)。所以抖动缓冲天然是个“等待区”:它容忍一定的乱序和缺失,给重传 / FEC 争取补救的时间。

抖动吸收:攒一点再放

第二件事是吸收抖动:

抖动缓冲故意攒住一小段已完成的帧,延迟一点点再播——用这点缓冲“垫底”,抹平网络到达的参差。 想象水库:上游来水忽大忽小(抖动的到达),水库蓄一点水,下游就能匀速放水(平滑的播放)。缓冲的深度(攒多少)是动态自适应的:网络抖动大,就多攒点(缓冲深),扛得住更晚到的包;网络稳,就少攒点(缓冲浅),延迟更低。这个深度由观测到的抖动统计决定,FrameBuffer 和 VCMTiming 配合算出“每帧该在何时播”。

根本权衡:流畅 vs 延迟

抖动缓冲藏着实时媒体一个绕不开的权衡:

缓冲越深,越流畅(能容忍更大抖动、更多重传),但延迟越大;缓冲越浅,延迟越低,但一有抖动就卡。 这是流畅和低延迟的直接对立:

  • 深缓冲(如 500ms)—— 什么抖动、丢包重传都从容,画面丝滑,但你和对方的对话有半秒延迟,像卫星电话。
  • 浅缓冲(如 50ms)—— 延迟极低,对话跟手,但网络稍抖就卡顿 / 丢帧。

没有两全,只能按场景取舍:实时对话优先低延迟(浅缓冲,忍受偶尔卡顿),单向观看可以深缓冲(反正不用互动)。WebRTC 的抖动缓冲动态调整深度——在“当前网络能给的流畅”和“可接受的延迟”之间,实时找那个最优点。这个权衡,是所有实时媒体系统的宿命,音频的 NetEq(第 7 部分)面对的是同一个问题。

小结与下一章

接收侧第一关:抖动缓冲 + 组帧(video/frame_buffer.h)。组帧——按时间戳归拢一帧的多个包,靠序号 + marker判断收齐,等重传补缺。吸收抖动——攒一小段帧再匀速播,把网络参差的到达抹平,缓冲深度动态自适应。核心权衡:缓冲深则流畅但延迟高、浅则低延迟但易卡,按场景取舍。

帧拼齐了,但视频帧之间有参考依赖(帧间预测)——解码前必须理清“这帧依赖哪些帧、能不能解”。下一章看帧引用查找。👉 帧引用查找。

参考资料

  • [1] libwebrtc 源码.video/frame_buffer.h、modules/video_coding/(组帧、抖动估计 jitter_estimator.h)。
  • [2] libwebrtc 源码.modules/video_coding/timing/(与 timing 配合)。
  • [3] 关联音频 NetEq——同类抖动缓冲问题。

本页目录