帧速率 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()
为了安全地调用 setFrameRate()
,应用不需要考虑实际支持的屏幕刷新率(可通过调用 Display.getSupportedModes()
获取该刷新率)。例如,即使设备仅支持 60Hz,也请使用应用偏好的帧速率调用 setFrameRate()
。与应用的帧速率不匹配的设备将保持当前的显示刷新率。
如需查看调用 setFrameRate()
是否会导致屏幕刷新率发生变化,请调用 DisplayManager.registerDisplayListener()
或 AChoreographer_registerRefreshRateCallback()
注册屏幕变化通知。
调用 setFrameRate()
时,最好传入确切的帧速率,而不是舍入为整数。例如,在渲染以 29.97Hz 录制的视频时,请传入 29.97,而不是四舍五入到 30。
对于视频应用,传递给 setFrameRate()
的兼容性参数应设置为 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
,以进一步提示 Android 平台,让应用使用下拉功能来适应不匹配的屏幕刷新频率(这会导致抖动)。
在某些情况下,视频界面会停止提交帧,但在屏幕上会继续显示一段时间。常见场景包括播放到达视频结尾或用户暂停播放。在这些情况下,请在调用 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 库。
在某些情况下,平台可能会切换到 setFrameRate()
中指定的应用的帧速率的倍数。例如,应用可能会以 60Hz 的频率调用 setFrameRate()
,而设备可能会将屏幕切换到 120Hz。可能发生这种情况的一个原因是,另一个应用的 Surface 的帧速率设置为 24Hz。在这种情况下,以 120Hz 的刷新频率运行屏幕将使 60Hz 表面和 24Hz 表面运行不需要下拉。
当屏幕以应用的倍数运行时,应用应为每个帧指定呈现时间戳,以避免不必要的抖动。对于游戏,Android Frame Pacing 库有助于正确设置帧呈现时间戳。
setFrameRate() 与 preferredDisplayModeId
WindowManager.LayoutParams.preferredDisplayModeId
是应用向平台指示其帧速率的另一种方式。某些应用只想更改显示屏刷新率,而不更改显示屏分辨率等其他显示模式设置。一般情况下,请使用 setFrameRate()
,而不是 preferredDisplayModeId
。setFrameRate()
函数更易于使用,因为应用无需搜索显示模式列表即可查找具有特定帧速率的模式。
如果有多个 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()
的应用。
视频播放应用的集成示例
我们建议您按照以下步骤在视频播放应用中集成刷新率开关:
- 确定
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();
}