帧速率

帧速率 API 可让应用告知 Android 平台其预期帧速率,该 API 适用于以 Android 11(API 级别 30)或更高版本为目标平台的应用。传统上,大多数设备仅支持一种显示屏刷新频率,通常为 60Hz,但这已经发生了变化。许多设备现在支持额外的刷新频率,如 90Hz 或 120Hz。一些设备支持无缝刷新率开关,而另一些设备则短暂显示黑屏,通常会持续一秒钟。

该 API 的主要目的是让应用能够更好地利用所有受支持的屏幕刷新频率。例如,播放调用 setFrameRate() 的 24Hz 视频的应用可能会导致设备将显示刷新率从 60Hz 更改为 120Hz。这种新的刷新频率可实现流畅、无抖动的播放 24Hz 视频,无需像在 60Hz 显示屏上播放同一视频一样进行 3:2 下拉。这样可以带来更好的用户体验。

基本用法

Android 提供了多种访问和控制 Surface 的方法,因此 setFrameRate() API 有多个版本。该 API 的每个版本均采用相同的参数,其工作方式也与其他 API 相同:

为了安全地调用 setFrameRate(),应用不需要考虑实际支持的屏幕刷新率(可通过调用 Display.getSupportedModes() 获取该刷新率)。例如,即使设备仅支持 60Hz,也请使用应用首选的帧速率调用 setFrameRate()。与应用的帧速率不匹配的设备将保持当前的显示刷新率。

如需查看调用 setFrameRate() 是否会导致屏幕刷新率发生变化,请通过调用 DisplayManager.registerDisplayListener()AChoreographer_registerRefreshRateCallback() 注册显示变化通知。

调用 setFrameRate() 时,最好传入确切的帧速率,而不是舍入为整数。例如,在渲染以 29.97Hz 录制的视频时,传入 29.97,而不是四舍五入为 30。

对于视频应用,传递给 setFrameRate() 的兼容性参数应设置为 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,以向 Android 平台额外提示,应用将使用下拉功能来适应不匹配的屏幕刷新频率(这会导致抖动)。

在某些情况下,视频 surface 会停止提交帧,但在屏幕上在一段时间内仍会显示。常见场景包括播放到达视频末尾或用户暂停播放。在这些情况下,请在帧速率参数设置为 0 的情况下调用 setFrameRate(),以将 Surface 的帧速率设置清除为默认值。当销毁 Surface 或 Surface 因用户切换到其他应用而处于隐藏状态时,无需清除此类帧速率设置。请仅在 Surface 保持可见状态且未使用时清除帧速率设置。

非流畅帧速率切换

在某些设备上,刷新频率切换时可能会出现画面中断,例如黑屏一两秒。这种情况通常发生在机顶盒、平板电视及类似设备上。默认情况下,Android 框架不会在调用 Surface.setFrameRate() API 时切换模式,以避免此类视觉中断。

有些用户更喜欢在长视频开头和结尾看到中断。这样,屏幕的刷新率就能与视频帧速率保持一致,并避免帧速率转换失真,例如在播放影片时采用 3:2 下拉抖动。

因此,如果用户和应用都选择启用,则可以启用非无缝刷新率开关:

我们建议您始终对长时间运行的视频(例如电影)使用 CHANGE_FRAME_RATE_ALWAYS。这是因为匹配视频帧速率的好处超过了更改刷新频率时发生的中断。

其他建议

针对常见场景,请遵循以下建议。

多个 surface

Android 平台经过精心设计,可正确处理多个 surface 具有不同帧速率设置的场景。当应用有多个具有不同帧速率的 surface 时,请针对每个 surface 调用 setFrameRate() 并提供正确的帧速率。即使设备使用分屏或画中画模式同时运行多个应用,每个应用也可以安全地针对自己的 surface 调用 setFrameRate()

平台不会改变应用的帧速率

即使设备支持应用在 setFrameRate() 调用中指定的帧速率,在某些情况下设备也不会将显示屏切换到该刷新率。例如,优先级较高的 surface 可能具有不同的帧速率设置,或者设备可能处于省电模式(对显示屏刷新频率设置限制以节省电池电量)。即使设备在正常情况下会切换,当设备未将显示刷新率切换为应用的帧速率设置时,应用也必须正常工作。

当显示刷新率与应用帧速率不一致时,如何响应由应用决定。对于视频,帧速率固定为源视频的帧速率,并且需要下拉才能显示视频内容。游戏可能会选择尝试以显示屏刷新频率运行,而不是保持其首选帧速率。应用不应根据平台的用途更改它传递给 setFrameRate() 的值。无论应用如何处理平台无法根据应用请求进行调整的情况,它都应保持为应用的首选帧速率。这样,如果设备条件发生变化,允许使用额外的显示刷新频率,平台就可以获得正确的信息来切换到应用的首选帧速率。

如果应用不能或无法以显示刷新率运行,则应通过某个平台设置呈现时间戳的机制来为每个帧指定呈现时间戳:

使用这些时间戳可防止平台过早呈现应用帧,从而导致不必要的抖动。正确使用帧呈现时间戳有点复杂。对于游戏,请参阅我们的帧同步指南以详细了解如何避免抖动,并考虑使用 Android Frame Pacing 库

在某些情况下,平台可能会切换为 setFrameRate() 中指定的应用帧速率的倍数。例如,应用可能会以 60Hz 的频率调用 setFrameRate(),而设备可能会将显示屏切换到 120Hz。可能发生这种情况的一个原因是,另一个应用的 Surface 的帧速率设置为 24Hz。在这种情况下,如果以 120Hz 的频率运行屏幕,60Hz 表面和 24Hz 表面无需下拉即可运行。

当屏幕以应用的帧速率倍数运行时,应用应为每个帧指定呈现时间戳,以避免不必要的抖动。对于游戏,Android Frame Pacing 库有助于正确设置帧呈现时间戳。

setFrameRate() 与 preferredDisplayModeId 对比

应用可以通过 WindowManager.LayoutParams.preferredDisplayModeId 向平台指明其帧速率。某些应用只想更改显示屏刷新频率,而不是更改显示屏分辨率等其他显示模式设置。一般情况下,请使用 setFrameRate() 而非 preferredDisplayModeIdsetFrameRate() 函数更易于使用,因为应用无需搜索显示模式列表即可查找具有特定帧速率的模式。

如果有多个 surface 以不同的帧速率运行,setFrameRate() 可让平台有更多机会选择兼容的帧速率。例如,设想以下场景:两个应用在 Pixel 4 上以分屏模式运行,其中一个应用播放 24Hz 视频,另一个应用向用户显示可滚动列表。Pixel 4 支持两种屏幕刷新频率:60Hz 和 90Hz。使用 preferredDisplayModeId API,视频界面会强制选择 60Hz 或 90Hz。通过以 24Hz 的刷新率调用 setFrameRate(),视频 Surface 可为平台提供有关源视频的帧速率的更多信息,使平台能够选择 90Hz 的屏幕刷新率,在此场景中,优于 60Hz。

不过,在某些情况下,应使用 preferredDisplayModeId 而非 setFrameRate(),例如以下情况:

  • 如果应用想要更改分辨率或其他显示模式设置,请使用 preferredDisplayModeId
  • 如果模式切换是轻量级且不太可能被用户察觉,平台只会为了响应对 setFrameRate() 的调用来切换显示模式。如果应用即使需要大量模式开关也倾向于切换显示屏刷新率(例如,在 Android TV 设备上),请使用 preferredDisplayModeId
  • 如果应用无法处理以应用的倍数的帧速率运行的显示屏(需要在每一帧上设置呈现时间戳),则应使用 preferredDisplayModeId

setFrameRate() 与 preferredRefreshRate

WindowManager.LayoutParams#preferredRefreshRate 可在应用窗口上设置首选帧速率,该速率适用于窗口中的所有 surface。应用应指定其首选帧速率而不考虑设备支持的刷新频率(类似于 setFrameRate()),以便调度程序更好地提示应用的预期帧速率。

对于使用 setFrameRate() 的 Surface,系统会忽略 preferredRefreshRate。通常,应尽可能使用 setFrameRate()

PreferredRefreshRate 与 preferredDisplayModeId 对比

如果应用只想更改首选刷新频率,最好使用 preferredRefreshRate,而不是 preferredDisplayModeId

避免过于频繁地调用 setFrameRate()

尽管就性能而言,setFrameRate()调用的成本并不高,但应用应避免每一帧调用setFrameRate()或每秒调用多次。调用 setFrameRate() 可能会导致显示刷新率发生变化,而这可能会导致过渡期间出现帧丢失。您应该提前确定正确的帧速率,并调用一次 setFrameRate()

游戏或其他非视频应用的使用情况

虽然视频是 setFrameRate() API 的主要用例,但也可以用于其他应用。例如,如果游戏的运行频率不高于 60Hz(以减少耗电量并实现更长的游戏会话),则可以调用 Surface.setFrameRate(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)。这样一来,默认以 90Hz 运行的设备在游戏运行期间将改为以 60Hz 运行,从而避免游戏以 60Hz 运行而显示屏以 90Hz 运行时时发生抖动。

使用 FRAME_RATE_COMPATIBILITY_FIXED_SOURCE

FRAME_RATE_COMPATIBILITY_FIXED_SOURCE 仅适用于视频应用。对于非视频用途,请使用 FRAME_RATE_COMPATIBILITY_DEFAULT

选择更改帧速率的策略

  • 强烈建议应用在显示长时间运行的视频(如电影)时,调用 setFrameRate(fps, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ALWAYS),其中 fps 是指视频的帧速率。
  • 如果您希望视频播放持续几分钟或更短,我们强烈建议您不要使用 CHANGE_FRAME_RATE_ALWAYS 调用 setFrameRate() 的应用。

视频播放应用的集成示例

我们建议您按照以下步骤在视频播放应用中集成刷新率开关:

  1. 确定 changeFrameRateStrategy
    1. 如果要播放长时间运行的视频(例如电影),请使用 MATCH_CONTENT_FRAMERATE_ALWAYS
    2. 如果要播放短视频(例如移动预告片),请使用 CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
  2. 如果 changeFrameRateStrategyCHANGE_FRAME_RATE_ONLY_IF_SEAMLESS,请转到第 4 步。
  3. 通过检查以下两项情况是否成立,检测是否即将发生非无缝刷新率切换:
    1. 无法从当前刷新率(我们称之为 C)切换到视频的帧速率(我们称之为 V)的流畅模式。如果 C 和 V 不同,并且 Display.getMode().getAlternativeRefreshRates 不包含 V 的倍数,则会出现这种情况。
    2. 用户已选择启用非流畅刷新率变更。您可以通过检查 DisplayManager.getMatchContentFrameRateUserPreference 是否返回 MATCH_CONTENT_FRAMERATE_ALWAYS 来检测这一点
  4. 如果能够顺畅切换,请执行以下操作:
    1. 调用 setFrameRate 并向其传递 fpsFRAME_RATE_COMPATIBILITY_FIXED_SOURCEchangeFrameRateStrategy,其中 fps 是视频的帧速率。
    2. 开始播放视频
  5. 如果即将发生非流畅模式更改,请执行以下操作:
    1. 显示用户体验以通知用户。请注意,我们建议您实现一种方式,让用户能够关闭此用户体验并跳过第 5.d 步中的额外延迟。这是因为,对于切换时间较短的显示屏,我们建议的延迟时间超过了必要程度。
    2. 调用 setFrameRate 并向其传递 fpsFRAME_RATE_COMPATIBILITY_FIXED_SOURCECHANGE_FRAME_RATE_ALWAYS,其中 fps 是视频的帧速率。
    3. 等待 onDisplayChanged 回调。
    4. 等待 2 秒钟,以便模式切换完成。
    5. 开始播放视频

仅支持无缝切换的伪代码如下所示:

SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
transaction.setFrameRate(surfaceControl,
    contentFrameRate,
    FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
    CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
transaction.apply();
beginPlayback();

支持无缝和非无缝切换的伪代码如下所示:

SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
if (isSeamlessSwitch(contentFrameRate)) {
  transaction.setFrameRate(surfaceControl,
      contentFrameRate,
      FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
      CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
  transaction.apply();
  beginPlayback();
} else if (displayManager.getMatchContentFrameRateUserPreference()
      == MATCH_CONTENT_FRAMERATE_ALWAYS) {
  showRefreshRateSwitchUI();
  sleep(shortDelaySoUserSeesUi);
  displayManager.registerDisplayListener(…);
  transaction.setFrameRate(surfaceControl,
      contentFrameRate,
      FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
      CHANGE_FRAME_RATE_ALWAYS);
  transaction.apply();
  waitForOnDisplayChanged();
  sleep(twoSeconds);
  hideRefreshRateSwitchUI();
  beginPlayback();
}