应用可通过 Frame Rate API 将其预期帧速率告知 Android 平台,该 API 可在以 Android 11(API 级别 30)或更高版本为目标平台的应用中使用。过去,大多数设备仅支持单一屏幕刷新率(通常为 60Hz),但这种情况正在发生变化。现在,许多设备都支持其他刷新率,例如 90Hz 或 120Hz。有些设备支持无缝切换刷新率,而有些设备则会短暂显示黑屏,通常持续一秒。
该 API 的主要用途是让应用能够更好地利用所有受支持的显示屏刷新率。例如,如果应用播放 24Hz 视频并调用 setFrameRate(),可能会导致设备将屏幕刷新率从 60Hz 更改为 120Hz。这种新的刷新率可实现 24Hz 视频的流畅、无抖动播放,无需像在 60Hz 显示屏上播放同一视频那样进行 3:2 下拉。这种做法能带来更出色的用户体验。
基本用法
Android 提供了多种访问和控制 Surface 的方法,因此 setFrameRate() API 有多个版本。每个版本的 API 都采用相同的参数,并且工作方式与其他版本相同:
Surface.setFrameRate()SurfaceControl.Transaction.setFrameRate()ANativeWindow_setFrameRate()ASurfaceTransaction_setFrameRate()
应用无需考虑实际支持的显示刷新率(可通过调用 Display.getSupportedModes() 获取),即可安全地调用 setFrameRate()。例如,即使设备仅支持 60Hz,也请使用应用首选的帧速率调用 setFrameRate()。
如果设备没有与应用帧速率更匹配的刷新率,则会保持当前的显示刷新率。
如需查看对 setFrameRate() 的调用是否会导致显示刷新率发生变化,请通过调用 DisplayManager.registerDisplayListener() 或 AChoreographer_registerRefreshRateCallback() 注册显示更改通知。
调用 setFrameRate() 时,最好传入确切的帧速率,而不是四舍五入为整数。例如,在渲染以 29.97Hz 录制的视频时,请传入 29.97,而不是四舍五入为 30。
对于视频应用,传递给 setFrameRate() 的兼容性参数应设置为 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,以便向 Android 平台提供额外提示,表明应用将使用下拉来适应不匹配的显示刷新率(这会导致抖动)。
在某些情况下,视频 Surface 会停止提交帧,但仍会在屏幕上显示一段时间。常见场景包括播放到视频结尾或用户暂停播放。在这些情况下,请调用 setFrameRate(),并将帧速率参数设置为 0,以将 Surface 的帧速率设置清除回默认值。在销毁 Surface 时,或者在 Surface 因用户切换到其他应用而被隐藏时,无需像这样清除帧速率设置。仅当 Surface 保持可见但未被使用时,才清除帧速率设置。
非流畅帧速率切换
在某些设备上,刷新率切换可能会导致视觉中断,例如黑屏一两秒。这种情况通常发生在机顶盒、电视面板和类似设备上。默认情况下,Android 框架在调用 Surface.setFrameRate() API 时不会切换模式,以避免此类视觉中断。
有些用户更喜欢在较长视频的开头和结尾处看到视觉中断。这样,显示屏的刷新率便可与视频帧速率相匹配,并避免出现帧速率转换伪影,例如电影播放时的 3:2 下拉抖动。
因此,如果用户和应用都选择启用,则可以启用非无缝刷新率切换:
- 用户:如需选择启用,用户可以启用匹配内容帧速率用户设置。
- 应用:如需选择启用,应用可以将
CHANGE_FRAME_RATE_ALWAYS传递到setFrameRate()中。
对于电影等长时间播放的视频,我们建议您始终使用 CHANGE_FRAME_RATE_ALWAYS。这是因为,匹配视频帧速率带来的好处大于更改刷新率时发生的中断。
其他建议
请针对常见场景遵循以下建议。
多个平台
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(),而不要使用 preferredDisplayModeId。setFrameRate() 函数更易于使用,因为应用无需搜索显示模式列表即可找到具有特定帧速率的模式。
setFrameRate() 让平台有更多机会在多个 Surface 以不同帧速率运行的情况下选择兼容的帧速率。例如,假设有两个应用在 Pixel 4 上以分屏模式运行,其中一个应用正在播放 24Hz 视频,另一个应用正在向用户显示可滚动列表。Pixel 4 支持两种屏幕刷新率:60Hz 和 90Hz。使用 preferredDisplayModeId API 时,视频 Surface 会被迫选择 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()。
视频播放应用的集成示例
我们建议您按照以下步骤在视频播放应用中集成刷新率切换功能:
- 确定
changeFrameRateStrategy:- 如果播放的是电影等长时间运行的视频,请使用
MATCH_CONTENT_FRAMERATE_ALWAYS - 如果播放的是短视频(例如电影预告片),请使用
CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
- 如果播放的是电影等长时间运行的视频,请使用
- 如果
changeFrameRateStrategy为CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS,请前往第 4 步。 - 通过检查以下两个事实是否都成立,检测是否即将发生非无缝刷新率切换:
- 无法从当前刷新率(我们称之为 C)无缝切换到视频的帧速率(我们称之为 V)。如果 C 和 V 不同,并且
Display.getMode().getAlternativeRefreshRates不包含 V 的倍数,就会出现这种情况。 - 用户已选择启用非无缝刷新率更改。您可以通过检查
DisplayManager.getMatchContentFrameRateUserPreference是否返回MATCH_CONTENT_FRAMERATE_ALWAYS来检测到这种情况
- 无法从当前刷新率(我们称之为 C)无缝切换到视频的帧速率(我们称之为 V)。如果 C 和 V 不同,并且
- 如果切换过程将是无缝的,请执行以下操作:
- 调用
setFrameRate并向其传递fps、FRAME_RATE_COMPATIBILITY_FIXED_SOURCE和changeFrameRateStrategy,其中fps是视频的帧速率。 - 开始播放视频
- 调用
- 如果即将发生非无缝模式更改,请执行以下操作:
- 显示用户体验以通知用户。请注意,我们建议您实现一种方法,让用户可以关闭此用户体验,并跳过第 5.d 步中的额外延迟。这是因为,在切换时间更快的显示屏上,我们建议的延迟时间比实际需要的延迟时间更长。
- 调用
setFrameRate并向其传递fps、FRAME_RATE_COMPATIBILITY_FIXED_SOURCE和CHANGE_FRAME_RATE_ALWAYS,其中fps是视频的帧速率。 - 等待
onDisplayChanged回调。 - 等待 2 秒钟,让模式切换完成。
- 开始播放视频
仅支持无缝切换的伪代码如下所示:
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();
}