帧率 API 允许应用将预期的帧率告知 Android 平台,适用于以 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()
的 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
传递给setFrameRate()
。
我们建议您始终对电影等长时间播放的视频使用 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()
而不是 preferredDisplayModeId
。setFrameRate()
函数更容易使用,因为应用无需搜索显示模式列表即可找到具有特定帧率的模式。
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()
。
视频播放应用的集成示例
我们建议视频播放应用按以下步骤集成刷新率切换功能
- 决定
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();
}