帧率

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

API 的主要目的是使应用能够更好地利用所有支持的显示刷新率。例如,播放 24Hz 视频的应用调用 setFrameRate() 可能会导致设备将显示刷新率从 60Hz 更改为 120Hz。这种新的刷新率使 24Hz 视频能够平滑播放,不会出现抖动,并且不需要 3:2 降速,而 3:2 降速是播放 60Hz 显示屏上的相同视频所需的。这将带来更好的用户体验。

基本用法

Android 公开了多种访问和控制表面的方法,因此存在多个版本的 setFrameRate() API。每个版本的 API 都采用相同的参数,并且与其他版本的工作方式相同。

应用无需考虑实际支持的显示刷新率(可以通过调用 Display.getSupportedModes() 获取),即可安全地调用 setFrameRate()。例如,即使设备只支持 60Hz,也请使用您的应用首选的帧速率调用 setFrameRate()。没有更适合应用帧速率的设备将保持当前显示刷新率。

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

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

对于视频应用,传递给 setFrameRate() 的兼容性参数应设置为 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,以便向 Android 平台提供额外提示,表明应用将使用降速来适应不匹配的显示刷新率(这将导致抖动)。

在某些情况下,视频表面将停止提交帧,但会在屏幕上可见一段时间。常见的情况包括播放到达视频结尾或用户暂停播放时。在这种情况下,将帧速率参数设置为 0 调用 setFrameRate(),以清除表面的帧速率设置并恢复为默认值。销毁表面或用户切换到其他应用时表面被隐藏,则无需像这样清除帧速率设置。只有在表面在不使用的情况下仍处于可见状态时,才清除帧速率设置。

非无缝帧速率切换

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

一些用户更愿意在较长视频的开头和结尾处出现视觉中断。这允许显示屏的刷新率与视频帧速率匹配,并避免帧速率转换伪像,例如电影播放时的 3:2 降速抖动。

出于这个原因,如果用户和应用都选择加入,则可以启用非无缝刷新率切换。

建议您始终使用 CHANGE_FRAME_RATE_ALWAYS 用于长时间运行的视频(如电影)。这是因为匹配视频帧速率的益处大于更改刷新率时发生的中断。

其他建议

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

多个表面

Android 平台旨在正确处理具有不同帧速率设置的多个表面的场景。当您的应用具有具有不同帧速率的多个表面时,请使用每个表面的正确帧速率调用 setFrameRate()。即使设备同时运行多个应用,使用分屏或画中画模式,每个应用也可以安全地为自己的表面调用 setFrameRate()

平台不会切换到应用的帧速率

即使设备支持应用在调用 setFrameRate() 时指定的帧速率,设备也可能不会将显示屏切换到该刷新率。例如,优先级更高的表面可能具有不同的帧速率设置,或者设备可能处于省电模式(限制显示刷新率以节省电池)。即使设备在正常情况下确实切换了显示刷新率,应用在设备没有将显示刷新率切换到应用的帧速率设置时也必须能够正常工作。

应用需要决定在显示刷新率与应用帧速率不匹配时如何响应。对于视频,帧速率固定为源视频的帧速率,并且需要降速来显示视频内容。游戏可能会选择尝试以显示刷新率运行,而不是保持其首选帧速率。应用不应根据平台的操作更改传递给 setFrameRate() 的值。它应始终设置为应用首选的帧速率,无论应用如何处理平台不调整以匹配应用请求的情况。这样,如果设备条件发生变化,允许使用其他显示刷新率,平台将拥有正确的信息来切换到应用首选的帧速率。

在应用无法或不能以显示刷新率运行的情况下,应用应使用平台提供的机制之一为每帧指定呈现时间戳。

使用这些时间戳可以阻止平台过早呈现应用帧,从而避免不必要的抖动。正确使用帧呈现时间戳有点棘手。对于游戏,请参阅我们的 帧速率控制指南,以详细了解如何避免抖动,并考虑使用 Android 帧速率控制库

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

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

setFrameRate() 与 preferredDisplayModeId

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

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

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

  • 如果应用想要更改分辨率或其他显示模式设置,请使用 preferredDisplayModeId
  • 平台仅在模式切换很轻巧且不太可能被用户注意到时,才会响应对 setFrameRate() 的调用切换显示模式。如果应用希望即使需要进行大量模式切换(例如在 Android TV 设备上)也能切换显示刷新率,请使用 preferredDisplayModeId
  • 无法处理显示屏以应用帧速率的倍数运行的应用(需要在每帧上设置呈现时间戳)应使用 preferredDisplayModeId

setFrameRate() 与 preferredRefreshRate

WindowManager.LayoutParams#preferredRefreshRate 在应用的窗口上设置首选帧速率,并且该速率适用于窗口中的所有表面。应用应指定其首选帧速率,无论设备支持的刷新率如何,类似于 setFrameRate(),以向调度程序提供有关应用预期帧速率的更佳提示。

对于使用 setFrameRate() 的表面,会忽略 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. 显示 UX 以通知用户。请注意,我们建议您实现一种方法,允许用户取消此 UX 并跳过步骤 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();
}