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

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

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

メディア セッションを選択するタイミング

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

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

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

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

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

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

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

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

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

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

これは、エラーを示すなど、プレーヤー自体から独立して PlaybackStateCompat を作成して維持する必要があった従来のアプローチとは異なります。

一意のセッション ID

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

アプリが複数のセッション インスタンスを同時に管理する場合は、各セッションのセッション 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() のサンプル実装は、セッションのデモアプリで確認できます。

カスタム レイアウトとカスタム コマンドを管理する

以降のセクションでは、カスタム コマンド ボタンのカスタム レイアウトをクライアントアプリにアドバタイズし、コントローラにカスタム コマンドの送信を許可する方法について説明します。

セッションのカスタム レイアウトを定義する

ユーザーに表示する再生コントロールをクライアント アプリに示すには、サービスの onCreate() メソッドで MediaSession を作成するときに、セッションのカスタム レイアウトを設定します。

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

使用可能なプレーヤー コマンドとカスタム コマンドを宣言する

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

MediaController がメディア セッションに接続するときに使用できるカスタム セッション コマンドを定義できます。そのためには、MediaSession.Callback.onConnect() をオーバーライドします。onConnect コールバック メソッドで MediaController からの接続リクエストを受け入れるとき、使用可能なコマンドのセットを構成して返します。

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

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

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.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)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback 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)
      );
    }
    ...
  }
}

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

ユーザー操作後にカスタム レイアウトを更新する

カスタム コマンドやプレーヤーとのその他のインタラクションの処理後に、コントローラ UI に表示されるレイアウトを更新できます。典型的な例としては、このボタンに関連付けられたアクションをトリガーした後にアイコンを変更する切り替えボタンがあります。レイアウトを更新するには、MediaSession.setCustomLayout を使用します。

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

再生コマンドの動作をカスタマイズする

Player インターフェースで定義されたコマンドの動作(play()seekToNext() など)をカスタマイズするには、PlayerForwardingPlayer でラップします。

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

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

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

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

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

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

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

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

メディアボタンへの応答

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

アプリは、MediaSession.Callback.onMediaButtonEvent(Intent) をオーバーライドすることでデフォルトの動作をオーバーライドできます。このような場合、アプリはすべての API 固有の処理を独自に処理できます(処理する必要があります)。

エラー処理と報告

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

致命的な再生エラー

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

この場合、再生ステータスは STATE_IDLE に遷移し、MediaController.getPlaybackError() は遷移の原因となった PlaybackException を返します。コントローラは PlayerException.errorCode を検査して、エラーの原因に関する情報を取得できます。

相互運用性を確保するため、致命的なエラーは、状態を STATE_ERROR に移行し、PlaybackException に従ってエラーコードとメッセージを設定することで、プラットフォーム セッションの PlaybackStateCompat に複製されます。

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

ローカライズされた有意な情報をユーザーに提供するには、セッションのビルド時に ForwardingPlayer を使用して、致命的な再生エラーのエラーコード、エラー メッセージ、エラーの追加情報をカスタマイズします。

Kotlin

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

Java

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

転送プレーヤーは実際のプレーヤーに Player.Listener を登録し、エラーを報告するコールバックをインターセプトします。その後、カスタマイズされた PlaybackException が、転送プレーヤーに登録されているリスナーに委任されます。これを機能させるには、転送プレーヤーが Player.addListenerPlayer.removeListener をオーバーライドして、カスタマイズされたエラーコード、メッセージ、または追加情報を送信するリスナーにアクセスできるようにします。

Kotlin

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

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    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)
        }
        // Apps can customize further error messages by adding more branches.
        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)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      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;
        // Apps can customize further error messages by adding more case statements.
        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);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

致命的でないエラー

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

Kotlin

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

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

// Interoperability: Sending a nonfatal error to the media notification controller 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));

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

// Interoperability: Sending a nonfatal error to the media notification controller 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);
}

メディア通知コントローラに送信された致命的でないエラーは、プラットフォーム セッションの PlaybackStateCompat に複製されます。これにより、エラーコードとエラー メッセージのみが PlaybackStateCompat に設定され、PlaybackStateCompat.stateSTATE_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.
              }
            });