フレーム レート

フレームレート API を使用すると、アプリは予想されるフレームレートを Android プラットフォームに通知でき、フレームレート API は Android 11(API レベル 30)以上を対象とするアプリで利用できるようになります。従来、ほとんどのデバイスが 1 つのディスプレイ リフレッシュ レート(一般に 60 Hz)にのみ対応していましたが、これは変わってきています。多くのデバイスで、90 Hz や 120 Hz などの追加のリフレッシュ レートがサポートされています。シームレスなリフレッシュ レートの切り替えをサポートするデバイスもあれば、通常は 1 秒間ほど黒い画面が短時間表示されるデバイスもあります。

API の主な目的は、サポートされているすべてのディスプレイ リフレッシュ レートをアプリがもっと活用できるようにすることです。たとえば、24 Hz の動画を再生するアプリが setFrameRate() を呼び出すと、デバイスがディスプレイのリフレッシュ レートを 60 Hz から 120 Hz に変更する可能性があります。この新しいリフレッシュ レートにより、60 Hz のディスプレイで同じ動画を再生する場合に必要な 3:2 プルダウンを必要とせずに、24 Hz の動画をスムーズに再生できます。これにより、ユーザー エクスペリエンスが向上します。

基本的な使用方法

Android では、サーフェスのアクセスとコントロールを行う複数の方法を公開しているので、setFrameRate() API にも複数のバージョンがあります。API の各バージョンは同じパラメータを受け取り、他のバージョンと同じように動作します。

アプリは、setFrameRate() を安全に呼び出すために、Display.getSupportedModes() を呼び出すことで取得できる、実際にサポートされているディスプレイのリフレッシュ レートを考慮する必要はありません。たとえば、デバイスが 60 Hz しかサポートしていない場合でも、アプリが希望するフレームレートで setFrameRate() を呼び出します。アプリのフレームレートにより適したリフレッシュ レートがないデバイスは、現在のディスプレイのリフレッシュ レートを維持します。

setFrameRate() の呼び出しによってディスプレイのリフレッシュ レートが変更されるかどうかを確認するには、DisplayManager.registerDisplayListener() または AChoreographer_registerRefreshRateCallback() を呼び出して、ディスプレイの変更通知を登録します。

setFrameRate() を呼び出すときは、整数に丸めるのではなく、正確なフレームレートを渡すことをおすすめします。たとえば、29.97 Hz で録画された動画をレンダリングする場合は、30 に丸めずに 29.97 を渡します。

動画アプリの場合、setFrameRate() に渡される互換性パラメータを Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE に設定して、アプリがプルダウンを使用して一致しないディスプレイのリフレッシュ レートに適応する(ジャダーが発生する)ことを Android プラットフォームに伝える追加のヒントを提供する必要があります。

場合によっては、動画サーフェスがフレームの送信を停止しても、画面にしばらく表示されることがあります。一般的なシナリオとしては、再生が動画の最後に達した場合や、ユーザーが再生を一時停止した場合などがあります。このような場合は、フレームレート パラメータを 0 に設定して setFrameRate() を呼び出し、サーフェスのフレームレート設定をデフォルト値に戻します。サーフェスを破棄するときや、ユーザーが別のアプリに切り替えたためにサーフェスが非表示になったときは、このようにフレームレートの設定をクリアする必要はありません。フレームレートの設定は、サーフェスが使用されずに表示されたままになっている場合にのみクリアしてください。

シームレスでないフレームレートの切り替え

一部のデバイスでは、リフレッシュ レートの切り替え時に、画面が 1 ~ 2 秒黒くなるなどの視覚的な中断が発生することがあります。通常、この問題はセットトップ ボックス、テレビ パネルなどのデバイスで発生します。このような視覚的な中断を避けるため、デフォルトでは、Surface.setFrameRate() API が呼び出されても Android フレームワークはモードを切り替えません。

長い動画の最初と最後に視覚的な中断を好むユーザーもいます。これにより、ディスプレイのリフレッシュ レートを動画のフレームレートに合わせることができ、映画の再生時に 3:2 プルダウンのジャダーなどのフレームレート変換アーティファクトを回避できます。

そのため、ユーザーとアプリの両方がオプトインした場合、シームレスではないリフレッシュ レートの切り替えを有効にできます。

映画などの長時間の動画には、常に CHANGE_FRAME_RATE_ALWAYS を使用することをおすすめします。これは、動画のフレームレートを一致させるメリットが、リフレッシュ レートを変更する際に発生する中断よりも大きいためです。

その他の推奨事項

一般的なシナリオに関する推奨事項は次のとおりです。

複数のサーフェス

Android プラットフォームは、フレームレート設定が異なる複数のサーフェスがあるシナリオを正しく処理するように設計されています。アプリにフレームレートが異なる複数のサーフェスがある場合は、各サーフェスの正しいフレームレートで setFrameRate() を呼び出します。デバイスで複数のアプリが同時に実行されている場合でも、分割画面モードやピクチャー イン ピクチャー モードを使用している場合でも、各アプリは独自のサーフェスに対して setFrameRate() を安全に呼び出すことができます。

プラットフォームがアプリのフレームレートに変更されない

デバイスが setFrameRate() の呼び出しでアプリが指定したフレームレートをサポートしている場合でも、デバイスがディスプレイをそのリフレッシュ レートに切り替えないことがあります。たとえば、優先度の高いサーフェスではフレームレートの設定が異なる場合や、デバイスがバッテリー セーバー モードになっている場合(バッテリーを節約するためにディスプレイのリフレッシュ レートに制限を設定している場合)があります。デバイスが通常の状況で切り替わる場合でも、デバイスがアプリのフレームレート設定にディスプレイのリフレッシュ レートを切り替えない場合、アプリは正しく動作しなければなりません。

ディスプレイのリフレッシュ レートがアプリのフレームレートと一致しない場合の対応方法は、アプリが決定します。動画の場合、フレームレートはソース動画のフレームレートに固定され、動画コンテンツを表示するにはプルダウンが必要になります。ゲームは、推奨フレームレートを維持するのではなく、ディスプレイのリフレッシュ レートで実行しようとすることを選択できます。アプリは、プラットフォームの動作に基づいて setFrameRate() に渡す値を変更してはなりません。プラットフォームがアプリのリクエストに合わせて調整されない場合でも、アプリの優先フレームレートに設定されたままにする必要があります。これにより、デバイスの条件が変更されて追加のディスプレイ リフレッシュ レートが使用可能になった場合、プラットフォームはアプリの優先フレームレートに切り替えるための正しい情報を取得できます。

アプリがディスプレイのリフレッシュ レートで実行されない場合、または実行できない場合、アプリは、プレゼンテーション タイムスタンプを設定するためのプラットフォームのメカニズムのいずれかを使用して、各フレームのプレゼンテーション タイムスタンプを指定する必要があります。

これらのタイムスタンプを使用すると、プラットフォームがアプリ フレームを早すぎるタイミングで表示しなくなり、不要なジャダーが発生しなくなります。フレーム プレゼンテーション タイムスタンプの正しい使用方法は少し複雑です。ゲームについては、フレーム ペーシング ガイドでジャダーを回避する方法をご確認ください。また、Android フレーム ペーシング ライブラリの使用もご検討ください。

場合によっては、プラットフォームが setFrameRate() でアプリが指定したフレームレートの倍数に切り替えることがあります。たとえば、アプリが 60 Hz で setFrameRate() を呼び出し、デバイスがディスプレイを 120 Hz に切り替えることがあります。この問題が発生する理由の 1 つとして、別のアプリのサーフェスでフレームレートが 24 Hz に設定されていることが考えられます。この場合、ディスプレイを 120 Hz で実行すると、60 Hz サーフェスと 24 Hz サーフェスの両方をプルダウンなしで実行できます。

ディスプレイがアプリのフレームレートの倍数で動作している場合、アプリは不要なジャダーを避けるために、各フレームのプレゼンテーション タイムスタンプを指定する必要があります。ゲームでは、Android Frame Pacing ライブラリがフレーム表示タイムスタンプを正しく設定するのに役立ちます。

setFrameRate() と preferredDisplayModeId

WindowManager.LayoutParams.preferredDisplayModeId は、アプリがプラットフォームにフレームレートを示す別の方法です。ディスプレイ解像度などの他のディスプレイ モード設定を変更するのではなく、ディスプレイのリフレッシュ レートのみを変更したいアプリもあります。通常は、preferredDisplayModeId ではなく setFrameRate() を使用します。setFrameRate() 関数は、アプリが特定のフレームレートのモードを見つけるために表示モードのリストを検索する必要がないため、使いやすくなっています。

setFrameRate() を使用すると、異なるフレームレートで実行されている複数のサーフェスがあるシナリオで、互換性のあるフレームレートを選択する機会が増えます。たとえば、Pixel 4 で 2 つのアプリが分割画面モードで実行されており、1 つのアプリが 24 Hz の動画を再生し、もう 1 つのアプリがスクロール可能なリストをユーザーに表示しているシナリオを考えてみましょう。Google Pixel 4 は 60 Hz と 90 Hz の 2 つのディスプレイ リフレッシュ レートをサポートしています。preferredDisplayModeId API を使用すると、動画サーフェスは 60 Hz または 90 Hz のいずれかを選択します。24 Hz で setFrameRate() を呼び出すことで、動画サーフェスはソース動画のフレームレートに関するより多くの情報をプラットフォームに提供し、プラットフォームはディスプレイのリフレッシュ レートとして 90 Hz を選択できます。これは、このシナリオでは 60 Hz よりも優れています。

ただし、次のようなシナリオでは、setFrameRate() ではなく preferredDisplayModeId を使用する必要があります。

  • アプリで解像度やその他の表示モードの設定を変更する場合は、preferredDisplayModeId を使用します。
  • プラットフォームは、モード切り替えが軽量で、ユーザーに気づかれにくい場合にのみ、setFrameRate() の呼び出しに応じてディスプレイ モードを切り替えます。アプリが、モードの切り替えに大きな負荷がかかる場合でも(Android TV デバイスなど)、ディスプレイのリフレッシュ レートを切り替えることを優先する場合は、preferredDisplayModeId を使用します。
  • アプリのフレームレートの倍数で実行されるディスプレイを処理できないアプリ(各フレームにプレゼンテーション タイムスタンプを設定する必要がある)は、preferredDisplayModeId を使用する必要があります。

setFrameRate() と preferredRefreshRate

WindowManager.LayoutParams#preferredRefreshRate は、アプリのウィンドウで優先フレームレートを設定します。このレートは、ウィンドウ内のすべてのサーフェスに適用されます。アプリは、デバイスがサポートするリフレッシュ レートに関係なく、setFrameRate() と同様に、優先フレームレートを指定して、スケジューラにアプリの意図するフレームレートのヒントを提供する必要があります。

setFrameRate() を使用するサーフェスでは、preferredRefreshRate は無視されます。一般的には、可能であれば setFrameRate() を使用します。

preferredRefreshRate と preferredDisplayModeId

アプリが優先リフレッシュ レートのみを変更したい場合は、preferredDisplayModeId ではなく preferredRefreshRate を使用することが推奨されます。

setFrameRate() の呼び出し頻度が高くなりすぎないようにする

setFrameRate() 呼び出しはパフォーマンスの面でそれほどコストがかかりませんが、アプリは setFrameRate() をフレームごとに呼び出したり、1 秒間に複数回呼び出したりすることを避けるべきです。setFrameRate() の呼び出しは、ディスプレイのリフレッシュ レートの変更につながる可能性が高く、切り替え中にフレーム落ちが発生する可能性があります。正しいフレームレートを事前に把握し、setFrameRate() を 1 回呼び出す必要があります。

ゲームや動画以外のアプリでの使用

setFrameRate() API の主なユースケースは動画ですが、他のアプリにも使用できます。たとえば、60 Hz を超えないように実行する(消費電力を抑えてプレイ時間を長くする)ゲームは、Surface.setFrameRate(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT) を呼び出すことができます。このようにして、デフォルトで 90 Hz で動作するデバイスは、ゲームがアクティブな間は 60 Hz で動作します。これにより、ディスプレイが 90 Hz で動作しているときにゲームが 60 Hz で動作した場合に発生するジャダーを回避できます。

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.getMatchContentFrameRateUserPreferenceMATCH_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();
}