帧率

帧率 API 允许应用将预期的帧率告知 Android 平台,适用于以 Android 11(API 级别 30)或更高版本为目标平台的应用。传统上,大多数设备仅支持单一显示刷新率,通常为 60Hz,但这种情况正在改变。许多设备现在支持其他刷新率,例如 90Hz 或 120Hz。一些设备支持无缝刷新率切换,而另一些设备则会短暂显示黑屏,通常持续一秒钟。

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

基本用法

Android 公开了多种访问和控制 Surface 的方式,因此 setFrameRate() API 有多个版本。每个版本的 API 都采用相同的参数,并且工作方式相同

应用无需考虑实际支持的显示刷新率(可通过调用 Display.getSupportedModes() 获取)即可安全地调用 setFrameRate()。例如,即使设备仅支持 60Hz,也请调用 setFrameRate() 并传入应用首选的帧率。对于与应用帧率没有更好匹配的设备,将保持当前的显示刷新率。

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

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

对于视频应用,传递给 setFrameRate() 的 compatibility 参数应设置为 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,以便向 Android 平台提供额外提示,表明应用将使用下拉方式适应不匹配的显示刷新率(这会导致画面抖动)。

在某些情况下,视频 Surface 会停止提交帧,但会在屏幕上保持可见一段时间。常见情况包括播放到达视频结尾或用户暂停播放。在这些情况下,调用 setFrameRate() 并将帧率参数设置为 0,将 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 library

在某些情况下,平台可能会切换到应用在 setFrameRate() 中指定的帧率的倍数。例如,应用可能调用 setFrameRate() 并传入 60Hz,而设备可能会将显示器切换到 120Hz。发生这种情况的一个原因是另一个应用具有帧率为 24Hz 的 Surface。在这种情况下,以 120Hz 运行显示器将允许 60Hz 和 24Hz 的 Surface 都运行,无需进行下拉操作。

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

setFrameRate() 与 preferredDisplayModeId 的比较

WindowManager.LayoutParams.preferredDisplayModeId 是应用向平台指示其帧率的另一种方式。有些应用只想更改显示刷新率,而不是更改其他显示模式设置,例如显示分辨率。通常,使用 setFrameRate() 而不是 preferredDisplayModeIdsetFrameRate() 函数更容易使用,因为应用无需搜索显示模式列表即可找到具有特定帧率的模式。

setFrameRate() 为平台提供了更多机会来选择兼容的帧率,特别是在有多个 Surface 以不同帧率运行的场景中。例如,考虑 Pixel 4 上两个应用在分屏模式下运行的场景,其中一个应用播放 24Hz 视频,另一个应用向用户显示可滚动的列表。Pixel 4 支持两种显示刷新率:60Hz 和 90Hz。使用 preferredDisplayModeId API,视频 Surface 被迫选择 60Hz 或 90Hz。通过调用 setFrameRate() 并传入 24Hz,视频 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();
}