フレーム レート

フレームレート API を使用すると、アプリは目的のフレームレートを Android プラットフォームに通知できます。この API は、Android 11(API レベル 30)以降をターゲットとするアプリで使用できます。従来、ほとんどのデバイスは単一のディスプレイ リフレッシュ レート(通常は 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 は同じパラメータを受け取り、他のバージョンと同じように機能します。

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 Frame Pacing ライブラリの使用をご検討ください。

場合によっては、アプリが 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 つのアプリが分割画面モードで実行されているとします。この場合、一方のアプリが 24 Hz の動画を再生し、他方がユーザーにスクロール可能なリストが表示されているとします。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();
}