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

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

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

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

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

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

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

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

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

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

一意のセッション ID

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

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

IllegalStateExceptionIllegalStateException: 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)
    .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
        .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 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 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 プロパティを使用すると、どのメディア コントローラがリクエストを行っているかを追跡できます。これにより、システム、独自のアプリ、他のクライアント アプリのいずれから発信されたコマンドであっても、そのコマンドに応じてアプリの動作を調整できます。

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

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

Kotlin

val player = (logic to build a Player instance)

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

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

Java

ExoPlayer player = (logic to build a Player instance)

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

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

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

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

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

Kotlin

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  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

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  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 を調べて、エラーの原因に関する情報を取得できます。

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

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

ローカライズされた意味のある情報をユーザーに提供するために、セッションのビルド時に 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();

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

Kotlin

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
  }

  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

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