MediaSession を使用して再生を制御およびアドバタイズする

MediaSession は、音声や動画のプレーヤーを操作するための汎用的な手段を提供します。Media3 では、デフォルトのプレーヤーは Player インターフェースを実装する ExoPlayer クラスです。メディア セッションをプレーヤーに接続すると、アプリはメディア再生を外部に通知し、外部ソースから再生コマンドを受信できます。

コマンドは、ヘッドセットやテレビのリモコンの再生ボタンなどの物理ボタンから発信されることがあります。また、メディア コントローラを備えたクライアント アプリから発生することもあります(Google アシスタントに「一時停止」を指示するなど)。メディア セッションは、これらのコマンドをメディアアプリのプレーヤーに委任します。

メディア セッションを選択する場合

MediaSession を実装すると、ユーザーは再生を制御できるようになります。

  • ヘッドフォンから。ヘッドフォンには、メディアの再生や一時停止、次の曲や前の曲への移動を行うためのボタンやタッチ操作が用意されていることがよくあります。
  • Google アシスタントに話しかける。一般的なパターンは、「OK Google, 一時停止して」と言って、デバイスで現在再生中のメディアを一時停止することです。
  • Wear OS スマートウォッチから。これにより、スマートフォンで再生中に、最も一般的な再生コントロールに簡単にアクセスできます。
  • メディア コントロールを使用する。このカルーセルには、実行中のメディア セッションごとにコントロールが表示されます。
  • [テレビ] を選択します。物理的な再生ボタン、プラットフォームの再生コントロール、電源管理(テレビ、サウンドバー、A/V レシーバーの電源がオフになったり、入力が切り替わったりした場合、アプリでの再生を停止するなど)による操作を許可します。
  • Android Auto のメディア コントロールを使用する。これにより、運転中に安全に再生を制御できます。
  • 再生に影響を与える必要のある他の外部プロセス。

これは多くのユースケースに最適です。特に、次のような場合は MediaSession の使用を強く検討してください。

  • 映画やライブテレビなどの長尺動画コンテンツをストリーミングしている。
  • ポッドキャストや音楽プレイリストなどの長尺のオーディオ コンテンツをストリーミングしている。
  • TV アプリを開発している。

ただし、すべてのユースケースが MediaSession に適しているわけではありません。次のような場合は、Player のみを使用します。

  • ショート フォーム コンテンツを表示している。外部制御やバックグラウンド再生は必要ない。
  • ユーザーがリストをスクロールしているときに、複数の動画が同時に画面に表示されるなど、アクティブな動画が 1 つだけではない場合。
  • 1 回限りの紹介動画や説明動画を再生している。ユーザーが外部の再生コントロールを必要とせずに積極的に視聴することを想定している。
  • コンテンツがプライバシーに配慮したものであり、外部プロセスがメディア メタデータにアクセスすることを望まない(ブラウザのシークレット モードなど)。

ユースケースが上記のいずれにも当てはまらない場合は、ユーザーがコンテンツを積極的に操作していないときにアプリで再生を継続してもよいかどうかを検討してください。答えが「はい」の場合は、MediaSession を選択することをおすすめします。そうでない場合は、代わりに Player を使用することをおすすめします。

メディア セッションを作成する

メディア セッションは、それが管理するプレーヤーとともに存在します。Context オブジェクトと Player オブジェクトを使用してメディア セッションを構築できます。メディア セッションの作成と初期化は、Activity または Fragment の onStart() または onResume() ライフサイクル メソッド、またはメディア セッションと関連プレーヤーを所有する Service の onCreate() メソッドなど、必要なときに行う必要があります。

メディア セッションを作成するには、次のように Player を初期化して MediaSession.Builder に渡します。

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

状態の自動処理

Media3 ライブラリは、プレーヤーの状態を使用してメディア セッションを自動的に更新します。そのため、プレーヤーからセッションへのマッピングを手動で処理する必要はありません。

これは、プラットフォーム メディア セッションとは異なります。プラットフォーム メディア セッションでは、たとえばエラーを示すために、プレーヤー自体とは別に PlaybackState を作成して維持する必要がありました。

一意のセッション ID

デフォルトでは、MediaSession.Builder はセッション ID として空の文字列を使用してセッションを作成します。これは、アプリが単一のセッション インスタンスのみを作成することを意図している場合に十分です。これは最も一般的なケースです。

アプリが複数のセッション インスタンスを同時に管理する場合は、各セッションのセッション ID が一意であることを確認する必要があります。セッション ID は、MediaSession.Builder.setId(String id) でセッションをビルドするときに設定できます。

IllegalStateException が IllegalStateException: Session ID must be unique. ID= というエラー メッセージでアプリをクラッシュさせている場合は、同じ ID を持つ以前に作成されたインスタンスがリリースされる前に、セッションが予期せず作成された可能性があります。プログラミング エラーによってセッションが漏洩しないように、このようなケースは例外をスローすることで検出され、通知されます。

他のクライアントに制御を許可する

メディア セッションは再生を制御するための鍵となります。これにより、外部ソースからのコマンドをメディアの再生を行うプレーヤーにルーティングできます。これらのソースは、ヘッドセットやテレビのリモコンの再生ボタンなどの物理ボタンや、Google アシスタントに「一時停止」を指示するなどの間接的なコマンドです。同様に、通知やロック画面のコントロールを容易にするために Android システムへのアクセスを許可したり、ウォッチフェイスから再生をコントロールできるように Wear OS スマートウォッチへのアクセスを許可したりすることもできます。外部クライアントは、メディア コントローラを使用してメディアアプリに再生コマンドを発行できます。これらのコマンドはメディア セッションで受信され、最終的にメディア プレーヤーに委任されます。

MediaSession と MediaController の間のインタラクションを示す図。
図 1: メディア コントローラは、外部ソースからメディア セッションへのコマンドの受け渡しを容易にします。

コントローラがメディア セッションに接続しようとすると、onConnect() メソッドが呼び出されます。提供された ControllerInfo を使用して、リクエストを承認するか拒否するかを決定できます。接続リクエストを承認する例については、カスタム コマンドを宣言するセクションをご覧ください。

接続後、コントローラはセッションに再生コマンドを送信できます。セッションは、これらのコマンドをプレーヤーに委任します。Player インターフェースで定義された再生コマンドと再生リスト コマンドは、セッションによって自動的に処理されます。

他のコールバック メソッドを使用すると、カスタム コマンドのリクエストやプレイリストの変更などを処理できます。これらのコールバックにも同様に ControllerInfo オブジェクトが含まれているため、コントローラごとに各リクエストへの応答方法を変更できます。

プレイリストを変更する

メディア セッションは、プレイリストの ExoPlayer ガイドで説明されているように、プレーヤーのプレイリストを直接変更できます。コントローラが COMMAND_SET_MEDIA_ITEM または COMMAND_CHANGE_MEDIA_ITEMS を使用できる場合、コントローラはプレイリストを変更することもできます。

通常、再生リストに新しいアイテムを追加する際、プレーヤーは再生可能なアイテムにするために定義済みの URI を持つ MediaItem インスタンスを必要とします。デフォルトでは、新たに追加されたアイテムに URI が定義されている場合、そのアイテムは player.addMediaItem などのプレーヤー メソッドに自動的に転送されます。

プレーヤーに追加された MediaItem インスタンスをカスタマイズする場合は、onAddMediaItems() をオーバーライドできます。この手順は、定義された URI なしでメディアをリクエストするコントローラをサポートする場合に必要です。代わりに、通常、MediaItem には、リクエストされたメディアを記述するために次のフィールドの 1 つ以上が設定されています。

  • MediaItem.id: メディアを識別する汎用 ID。
  • MediaItem.RequestMetadata.mediaUri: カスタム スキーマを使用する可能性があり、プレーヤーで直接再生できるとは限らないリクエスト URI。
  • MediaItem.RequestMetadata.searchQuery: テキスト検索クエリ(Google アシスタントなど)。
  • MediaItem.MediaMetadata: 「タイトル」や「アーティスト」などの構造化メタデータ。

完全に新しいプレイリストのカスタマイズ オプションをさらに追加するには、onSetMediaItems() をオーバーライドして、プレイリストの開始アイテムと位置を定義することもできます。たとえば、リクエストされた 1 つのアイテムを再生リスト全体に展開し、元々リクエストされたアイテムのインデックスから再生を開始するようプレーヤーに指示できます。この機能を含む onSetMediaItems()の実装例 は、セッション デモアプリにあります。

メディアボタンの設定を管理する

システム UI、Android Auto、Wear OS などのコントローラは、ユーザーに表示するボタンについて独自の判断を下すことができます。ユーザーに公開する再生コントロールを指定するには、MediaSession でメディア ボタンの設定を指定します。これらの設定は、CommandButton インスタンスの順序付きリストで構成され、それぞれがユーザー インターフェースのボタンの設定を定義します。

コマンドボタンを定義する

CommandButton インスタンスは、メディア ボタンの設定を定義するために使用されます。各ボタンは、目的の UI 要素の 3 つの側面を定義します。

  1. アイコン: 外観を定義します。CommandButton.Builder を作成するときに、アイコンを事前定義された定数のいずれかに設定する必要があります。これは実際のビットマップや画像リソースではありません。汎用定数は、コントローラが独自の UI 内で一貫したルック アンド フィールを実現するために適切なリソースを選択するのに役立ちます。事前定義されたアイコン定数のいずれもユースケースに適合しない場合は、代わりに setCustomIconResId を使用できます。
  2. Command: ユーザーがボタンを操作したときにトリガーされるアクションを定義します。Player.Command には setPlayerCommand を使用し、事前定義またはカスタムの SessionCommand には setSessionCommand を使用できます。
  3. コントローラ UI でボタンを配置する場所を定義する Slot。このフィールドは省略可能で、アイコンとコマンドに基づいて自動的に設定されます。たとえば、ボタンをデフォルトの「オーバーフロー」領域ではなく、UI の「進む」ナビゲーション領域に表示するように指定できます。

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

メディア ボタンの設定が解決されると、次のアルゴリズムが適用されます。

  1. メディアボタンの設定の各 CommandButton について、ボタンを最初に使用可能な許可されたスロットに配置します。
  2. 中央、前方、後方のスロットのいずれかにボタンが入力されていない場合は、そのスロットにデフォルトのボタンを追加します。

CommandButton.DisplayConstraints を使用すると、UI 表示の制約に応じてメディア ボタンの設定がどのように解決されるかのプレビューを生成できます。

メディアボタンの設定を行う

メディア ボタンの設定を最も簡単に設定する方法は、MediaSession をビルドするときにリストを定義することです。または、MediaSession.Callback.onConnect をオーバーライドして、接続された各コントローラのメディアボタン設定をカスタマイズすることもできます。

Kotlin

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Java

MediaSession mediaSession =
    new MediaSession.Builder(context, player)
        .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
        .build();

ユーザー インタラクション後にメディアボタンの設定を更新する

プレーヤーとのインタラクションを処理した後、コントローラ UI に表示されるボタンを更新することがあります。一般的な例としては、このボタンに関連付けられたアクションをトリガーした後にアイコンとアクションが変化する切り替えボタンがあります。メディア ボタンの設定を更新するには、MediaSession.setMediaButtonPreferences を使用して、すべてのコントローラまたは特定のコントローラの設定を更新します。

Kotlin

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(ImmutableList.of(likeButton, removeFromFavoritesButton));

カスタム コマンドを追加してデフォルトの動作をカスタマイズする

利用可能なプレーヤー コマンドはカスタム コマンドで拡張できます。また、受信したプレーヤー コマンドとメディア ボタンをインターセプトして、デフォルトの動作を変更することもできます。

カスタム コマンドを宣言して処理する

メディア アプリは、たとえばメディア ボタンの設定で使用できるカスタム コマンドを定義できます。たとえば、ユーザーがメディア アイテムをお気に入りのアイテムのリストに保存できるボタンを実装できます。MediaController はカスタム コマンドを送信し、MediaSession.Callback はそれを受信します。

カスタム コマンドを定義するには、MediaSession.Callback.onConnect() をオーバーライドして、接続された各コントローラで使用可能なカスタム コマンドを設定する必要があります。

Kotlin

private class CustomMediaSessionCallback : MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  override fun onConnectAsync(
    session: MediaSession,
    controller: ControllerInfo,
  ): ListenableFuture<ConnectionResult> {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return Futures.immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
}

Java

private static class CustomMediaSessionCallback implements MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  @Override
  public ListenableFuture<ConnectionResult> onConnectAsync(
      MediaSession session, ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return Futures.immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
}

MediaController からカスタム コマンド リクエストを受け取るには、Callback の onCustomCommand() メソッドをオーバーライドします。

Kotlin

private class CustomCallback : MediaSession.Callback {
  // ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle,
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
    }
    // ...
    return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
  }
}

Java

private static class CustomCallback implements MediaSession.Callback {
  // ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
      MediaSession session,
      ControllerInfo controller,
      SessionCommand customCommand,
      Bundle args) {
    if (customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
    }
    // ...
    return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
  }
}

Callback メソッドに渡される MediaSession.ControllerInfo オブジェクトの packageName プロパティを使用すると、どのメディア コントローラがリクエストを行っているかを追跡できます。これにより、システム、アプリ自体、他のクライアント アプリから発信されたコマンドに応じて、アプリの動作を調整できます。

デフォルトのプレーヤー コマンドをカスタマイズする

デフォルトのコマンドと状態処理はすべて、MediaSession の Player に委任されます。Player インターフェースで定義されたコマンド(play() や seekToNext() など)の動作をカスタマイズするには、Player を ForwardingSimpleBasePlayer でラップしてから MediaSession に渡します。

Kotlin

val forwardingPlayer =
  object : ForwardingSimpleBasePlayer(player) {
    // Customizations
  }

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();

ForwardingSimpleBasePlayer の詳細については、カスタマイズに関する ExoPlayer ガイドをご覧ください。

プレーヤー コマンドのリクエスト元コントローラを特定する

MediaController によって Player メソッドの呼び出しが開始された場合、MediaSession.controllerForCurrentRequest を使用して送信元を特定し、現在のリクエストの ControllerInfo を取得できます。

Kotlin

private class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) {
  private lateinit var session: MediaSession

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Java

private static final class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  private MediaSession session;

  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

メディアボタンの処理をカスタマイズする

メディアボタンとは、Android デバイスや周辺デバイスにあるハードウェア ボタンのことです。たとえば、Bluetooth ヘッドセットの再生/一時停止ボタンなどです。Media3 は、メディアボタン イベントがセッションに届くと、そのイベントを処理し、セッション プレーヤーの適切な Player メソッドを呼び出します。

対応する Player メソッドですべてのメディアボタン イベントを処理することをおすすめします。より高度なユースケースでは、メディア ボタンのイベントを MediaSession.Callback.onMediaButtonEvent(Intent) でインターセプトできます。

エラー処理とレポート

セッションがコントローラに送信してレポートするエラーには、次の 2 種類があります。致命的なエラーは、再生を中断するセッション プレーヤーの技術的な再生エラーを報告します。致命的なエラーが発生すると、コントローラに自動的に報告されます。致命的でないエラーは、再生を中断しない技術的エラーまたはポリシー エラーであり、アプリによってコントローラに手動で送信されます。

致命的な再生エラー

致命的な再生エラーは、プレーヤーによってセッションに報告され、コントローラに報告されて Player.Listener.onPlayerError(PlaybackException) と Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException) を介して呼び出されます。

このような場合、再生状態は STATE_IDLE に移行し、MediaController.getPlaybackError() は移行の原因となった PlaybackException を返します。コントローラは PlayerException.errorCode を調べて、エラーの原因に関する情報を取得できます。

カスタム プレーヤー エラーの設定

プレーヤーから報告された致命的なエラーに加えて、アプリケーションは MediaSession.setPlaybackException(PlaybackException) を使用して MediaSession レベルでカスタム PlaybackException を設定できます。これにより、アプリケーションは接続されたコントローラにエラー状態を通知できます。例外は、接続されているすべてのコントローラに対して設定することも、特定の ControllerInfo に対して設定することもできます。

アプリがこの API を使用して PlaybackException を設定する場合:

  • 接続されている MediaController インスタンスに通知されます。コントローラの Listener.onPlayerError(PlaybackException) コールバックと Listener.onPlayerErrorChanged(@Nullable PlaybackException) コールバックが、提供された例外とともに呼び出されます。

  • MediaController.getPlayerError() メソッドは、アプリによって設定された PlaybackException を返します。

  • 影響を受けるコントローラの再生状態が Player.STATE_IDLE に変わります。

  • 使用可能なコマンドは削除され、すでに付与されている場合は COMMAND_GET_TIMELINE などの読み取りコマンドのみが残ります。たとえば、Timeline の状態は、例外がコントローラに適用されたときの状態にフリーズされます。COMMAND_PLAY など、プレーヤーの状態を変更しようとするコマンドは、特定のコントローラの再生例外がアプリによって削除されるまで削除されます。

以前に設定したカスタム PlaybackException をクリアして、通常のプレーヤー状態レポートを復元するには、アプリで MediaSession.setPlaybackException(/* playbackException= */ null) または MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null) を呼び出すことができます。

致命的なエラーのカスタマイズ

ユーザーにローカライズされた有益な情報を提供するために、実際のプレーヤーから発生した致命的な再生エラーのエラーコード、エラー メッセージ、エラー エクストラをカスタマイズできます。これは、セッションのビルド時に ForwardingPlayer を使用することで実現できます。

Kotlin

val session = MediaSession.Builder(context, ErrorForwardingPlayer(context, player)).build()

Java

MediaSession session =
    new MediaSession.Builder(context, new ErrorForwardingPlayer(context, player)).build();

転送プレーヤーは ForwardingSimpleBasePlayer を使用してエラーをインターセプトし、エラーコード、メッセージ、エキストラをカスタマイズできます。同様に、元のプレーヤーに存在しない新しいエラーを生成することもできます。

Kotlin

private class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon().setPlayerError(customizePlaybackException(state.playerError!!)).build()
    }
    return state
  }

  private fun customizePlaybackException(error: PlaybackException): PlaybackException {
    val buttonLabel: String
    val errorMessage: String
    when (error.errorCode) {
      PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
        buttonLabel = context.getString(R.string.err_button_label_restart_stream)
        errorMessage = context.getString(R.string.err_msg_behind_live_window)
      }
      else -> {
        buttonLabel = context.getString(R.string.err_button_label_ok)
        errorMessage = context.getString(R.string.err_message_default)
      }
    }
    val extras = Bundle()
    extras.putString("button_label", buttonLabel)
    return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon().setPlayerError(customizePlaybackException(state.playerError)).build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    String buttonLabel;
    String errorMessage;
    switch (error.errorCode) {
      case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
        buttonLabel = context.getString(R.string.err_button_label_restart_stream);
        errorMessage = context.getString(R.string.err_msg_behind_live_window);
        break;
      default:
        buttonLabel = context.getString(R.string.err_button_label_ok);
        errorMessage = context.getString(R.string.err_message_default);
        break;
    }
    Bundle extras = new Bundle();
    extras.putString("button_label", buttonLabel);
    return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
  }
}

致命的でないエラー

技術的な例外に起因しない致命的でないエラーは、アプリからすべてのコントローラまたは特定のコントローラに送信できます。

Kotlin

val sessionError =
  SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired),
  )

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
mediaSession.mediaNotificationControllerInfo?.let { mediaSession.sendError(it, sessionError) }

Java

SessionError sessionError =
    new SessionError(
        SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
        context.getString(R.string.error_message_authentication_expired));

// Option 1: Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Option 2: Sending a nonfatal error to the media notification controller only
// to set the error code and error message in the playback state of the platform
// media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

致命的でないエラーがメディア通知コントローラに送信されると、エラーコードとエラー メッセージがプラットフォーム メディア セッションに複製されますが、PlaybackState.state は STATE_ERROR に変更されません。

非致命的なエラーを受け取る

MediaController は、MediaController.Listener.onError を実装することで致命的ではないエラーを受け取ります。

Kotlin

val future =
  MediaController.Builder(context, sessionToken)
    .setListener(
      object : MediaController.Listener {
        override fun onError(controller: MediaController, sessionError: SessionError) {
          // Handle nonfatal error.
        }
      }
    )
    .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });