🎬 第一步:获取节拍——从 SurfaceFlinger 获取 VSync 周期
VideoFrameScheduler 要工作,首先得知道屏幕的刷新节奏,也就是 VSync 的周期(例如 60Hz 屏幕的周期是 16.6ms)。
- 核心接口:
VideoFrameScheduler类中有一个关键方法getVsyncPeriod()。 - 信息源:这个方法的实现,会通过其内部持有的
sp<ISurfaceComposer> mComposer服务,向SurfaceFlinger发起跨进程调用(Binder),查询当前主显示屏(main display)的 VSync 周期 。 - 获取时机:这个查询操作通常在
VideoFrameScheduler初始化(init())或定期更新(updateVsync())时进行,以确保使用的 VSync 信息是最新的 。
🎯 第二步:校准节拍——内部 PLL 的精算与预测
单纯拿到 VSync 周期还不够,因为视频的帧率和屏幕的刷新率很难完美匹配(比如 24fps 的视频要在 60Hz 的屏上显示)。因此,VideoFrameScheduler 内部实现了一个精巧的软件锁相环(PLL,Phase-Locked Loop) 。
PLL 的核心是一个名为 addSample 的方法,它的工作流程如下:
- 采样:每当
Renderer准备渲染一帧时,会将一个理想渲染时间(基于音视频同步计算出的renderTime)作为“样本”喂给 PLL 。 - 滤波与预测:PLL 内部维护着一个历史样本数组
mTimes[kHistorySize](默认记录最近 8 个样本)。它会分析这些样本,通过拟合算法动态调整,预测出一个最稳定、最接近真实 VSync 周期的值。这个过程可以滤除因解码延迟或网络抖动带来的时间噪声。 - 输出稳定节拍:经过 PLL 校准后,
getPeriod()方法就能返回一个平滑、可靠的 VSync 周期值,用于指导后续的帧调度 。
📐 第三步:踩点登场——计算出完美的“出场时间”
最后一步,也是用户最关心的,如何将理想时间点校准到屏幕的物理刷新周期上。这个魔法发生在 schedule(nsecs_t renderTime) 方法中 。
当 NuPlayer::Renderer 调用 schedule() 时,会发生以下“时间整形”过程:
flowchart LR
A[理想渲染时间点<br>(来自音视频同步)] --> B[VideoFrameScheduler<br>.schedule]
B --> C{核心计算}
subgraph C [PLL校准与VSync对齐]
direction TB
C1[“通过PLL获取<br>稳定的VSync周期”]
C2[“计算下一个<br>VSync时间点”]
C3[“将理想时间<br>对齐到该VSync点”]
end
C1 --> C2 --> C3
C --> D[校准后的渲染时间点<br>(与VSync对齐)]
D --> E[NuPlayer::Renderer<br>按此时间渲染]
这个计算过程确保了:无论理想时间点落在哪里,最终被执行的渲染时间点都恰好落在屏幕的 VSync 脉冲上。这样,当 SurfaceFlinger 在下一个 VSync 信号到来时进行合成,画面就是完整的,彻底避免了因渲染和刷新时机错位导致的“tearing”(画面撕裂)现象。
💡 为什么需要这个复杂的交互?
你可能会有疑问,为什么不直接用理想时间渲染?这是因为屏幕是物理设备,有其固定的刷新节奏。VideoFrameScheduler 的介入,实际上是在音视频同步的“准确性”和屏幕显示的“物理性”之间,架起了一座桥梁。
它利用从 SurfaceFlinger 获取的 VSync 信息 ,结合 PLL 算法 ,将精确但可能“不合拍”的时间点,转换为与硬件完美契合的显示时机。这样一来,NuPlayer 的 Renderer 模块就可以放心地依赖这个输出来做最终的渲染调度 ,实现了我们在上一轮讨论的“视频渲染线程根据计算出的 realTimeUs 决定等待或立即渲染”的逻辑。
总结来说,VideoFrameScheduler 通过向 SurfaceFlinger 查询 VSync 周期,并在内部用 PLL 进行精算和预测,成功地将音视频同步的“逻辑时间”与屏幕刷新的“物理时间”对齐,是连接多媒体引擎和图形系统的关键一环。