8.2.1 播放器的三个问题:何时解,何时显,对谁齐
"播放视频" 四个字,说出来毫不费力,做起来步步是坑。把这件直觉之事翻译成工程语言,其实是三个必须逐一作答的问题:何时解码、何时显示、对谁对齐。这一节不负责给出全部答案——它负责把问题立起来,立得足够清楚,让后面三节的解法各有归属。
第一问:何时解码——两个死法之间走钢丝
解码线程该跑多快?两个极端都是死路。
一路狂奔,解码速度甩开显示速度:Frame 是重型资产,1080p 一帧 YUV420 约 3MB,解出几百帧无处安放,内存先爆。ffplay 的对策是容量水线——压缩包队列(PacketQueue)上限 15MB,超了读线程就停;已解码帧队列(FrameQueue)更苛刻,视频只留 3 帧、音频只留 9 个采样块的窗口 [2]。视频帧贵,就给它最小的库存。
走走停停,解码速度跟不上显示节奏:渲染侧无帧可用,画面定格,观众看到的是卡顿。
两个死法之间的活路,我们其实已经见过:8.1.4 的队列解耦与条件变量节拍。解码线程不看时钟、不猜节奏,只看队列水位——队列满则挂起等待,被消费则唤醒续产。何时解码的答案不在时间里,在水位里。这个答案还有下半截:水位不仅是内存闸门,更是抗网络抖动的缓冲池,那是 8.2.3 的课题。
第二问:何时显示——应显时刻的累计艺术
帧解出来了,该在哪一刻上屏?第五章 5.3.1 给出的 PTS 是答案的一半:帧上写着 "我属于哪个时刻"。另一半在播放器手里:怎么把 "属于的时刻" 兑现在真实的墙钟上。
ffplay 的做法是一台应显时刻机 [2]:维护一个 frame_timer,每显示一帧,就把这一帧的显示间隔累加进去——frame_timer += delay,得到下一帧的应显时刻。此后每个渲染周期只做一件事:拿当前真实时间与应显时刻比,没到就等,过了就处理。朴素得近乎简陋,却引出一个深刻的设计点:显示节奏是累计出来的,不是查询出来的。系统定时器有抖动、渲染回调有迟到,若每帧都拿 PTS 直接对墙钟,误差会随噪声乱跳;累计成一条连续的应显时间轴,噪声才有机会被下一级机制消化。
消化机制就是 8.2.2 的丢帧与追帧——当应显时刻已被真实时间甩在身后,是咬牙上屏,还是壮士断腕?那是同步策略的辖区,本节只立此一问。
第三问:对谁对齐——两条时钟的必然漂移
前两问假设只有一条视频流,可播放器面对的现实是:音频和视频是两条独立的时钟。它们各有 PTS、各有解码线程、各走各的应显时刻机。两台走时精准的石英钟放一起都会漂移,何况两条穿过不同队列、不同解码耗时的渲染管线。放任自流,音画偏差只会单调扩大——8.3 的实测将给出数字:自由漂移几分钟后,偏差能积到数百毫秒,人耳人眼早已无法容忍。
出路只有一条:钦定一条主时钟,另一条向它对齐。问题是钦定谁——音频做主,视频追;视频做主,音频追;或者谁都不做主,共同听命于外部墙钟?三种选择引出三套截然不同的追随机制,容错能力与工程代价各不相同。
三个问题到此立毕:水位管解码节拍,累计管显示时刻,主时钟管音画对齐。其中最深的是第三问——前两问的解法各家大同小异,第三问的答案却定义了一台播放器的性格。下一节,音画同步的三主时钟策略。