フレーム レート

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

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

基本的な使用方法

Android では、サーフェスのアクセスとコントロールを行う複数の方法を公開しているので、setFrameRate() API にも複数のバージョンがあります。各バージョンの 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 秒黒くなるなどの視覚的な中断が発生することがあります。これは通常、セットトップ ボックス、テレビパネル、同様のデバイスで発生します。デフォルトでは、このような視覚的な中断を回避するために、Android フレームワークは Surface.setFrameRate() API が呼び出されたときにモードを切り替えません。

長い動画の開始時と終了時に視覚的な中断を好むユーザーもいます。これにより、ディスプレイの更新レートを動画のフレームレートに合わせることができ、映画の再生時の 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 は、アプリがフレームレートをプラットフォームに示すもう 1 つの方法です。ディスプレイ解像度などの他のディスプレイ モードの設定を変更するのではなく、ディスプレイのリフレッシュ レートのみを変更するアプリもあります。通常は、preferredDisplayModeId ではなく setFrameRate() を使用します。setFrameRate() 関数は、特定のフレームレートのモードを探すためにディスプレイ モードのリストを検索する必要がないため、使いやすい関数です。

setFrameRate() を使用すると、異なるフレームレートで実行されている複数のサーフェスがあるシナリオで、プラットフォームが互換性のあるフレームレートを選択できる機会が増えます。たとえば、Google 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() 呼び出しはパフォーマンスの面でそれほどコストがかかるものではありませんが、アプリではフレームごとに、または 1 秒間に複数回 setFrameRate() を呼び出さないようにする必要があります。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_ALWAYSsetFrameRate() を呼び出すアプリは使用しないことを強くおすすめします。

動画再生アプリの統合例

動画再生アプリに更新レートの切り替え機能を統合する場合は、次の手順をおすすめします。

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