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

メディア セッションは、オーディオ プレーヤーまたは動画プレーヤーを普遍的な方法で操作します。Media3 では、デフォルト プレーヤーは Player インターフェースを実装する ExoPlayer クラスです。メディア セッションをプレーヤーに接続すると、アプリはメディアの再生を外部でアドバタイズし、外部ソースから再生コマンドを受信できます。

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

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

MediaSession を実装すると、ユーザーが再生をコントロールできるようになります。

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

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

これは、たとえばエラーを示すために、プレーヤー自体から独立して PlaybackStateCompat を作成して維持する必要があった従来のアプローチからの脱却です。

一意のセッション ID

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

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

エラー メッセージ IllegalStateException: Session ID must be unique. ID= とともにアプリがクラッシュする IllegalStateException が表示された場合は、同じ 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: 「title」や「artist」などの構造化メタデータ。

まったく新しい再生リストのカスタマイズ オプションを増やすには、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 の仕様を独自に処理できます(処理する必要があります)。