メディア セッションは、オーディオ プレーヤーや動画プレーヤーをユニバーサルに操作する方法を提供します。Media3 のデフォルト プレーヤーは、Player
インターフェースを実装する ExoPlayer
クラスです。メディア セッションをプレーヤーに接続すると、アプリはメディアの再生を外部にアドバタイズし、外部ソースから再生コマンドを受信できます。
コマンドは、ヘッドセットの再生ボタンやテレビのリモコンなどの物理的なボタンから送信されることもあります。また、Google アシスタントに対して「一時停止」を指示するなど、メディア コントローラを備えたクライアント アプリからリクエストされる場合もあります。メディア セッションは、これらのコマンドをメディアアプリのプレーヤーに委任します。
メディア セッションを選択する場合
MediaSession
を実装すると、ユーザーが再生を制御できるようになります。
- ヘッドフォンを使用する。メディアの再生や一時停止、次または前のトラックへの移動など、ヘッドフォンで実行できるボタンやタップ操作はよくあります。
- Google アシスタントに話しかける。一般的なパターンでは、「OK Google, 一時停止して」と話しかけて、現在デバイスで再生中のメディアを一時停止します。
- Wear OS スマートウォッチから操作する。これにより、スマートフォンでの再生中に、最も一般的な再生コントロールに簡単にアクセスできます。
- メディア コントロールを使用する。このカルーセルには、実行中の各メディア セッションのコントロールが表示されます。
- テレビ物理的な再生ボタン、プラットフォームの再生コントロール、電源管理によるアクションを許可します(たとえば、テレビ、サウンドバー、または A/V レシーバーがオフになったり、入力が切り替わった場合は、アプリで再生を停止する必要があります)。
- 再生に影響を与える必要のあるその他の外部プロセス。
これは多くのユースケースに適しています。特に、次の場合は MediaSession
の使用を強くおすすめします。
- 映画やライブテレビなどの長尺動画コンテンツをストリーミングしている。
- ポッドキャストやミュージック プレイリストなどの長時間の音声コンテンツをストリーミングしている。
- TV アプリを作成している。
ただし、すべてのユースケースが MediaSession
に適しているわけではありません。次の場合は、Player
のみを使用することをおすすめします。
- ユーザーのエンゲージメントとインタラクションが極めて重要なショート動画を表示している。
- アクティブな動画が 1 つではない(ユーザーがリストをスクロールしていて、複数の動画が画面に同時に表示される場合など)。
- ユーザーに積極的に視聴してほしい、1 回限りの紹介動画または説明動画を再生している。
- コンテンツがプライバシーに配慮されていて、外部プロセスがメディア メタデータにアクセスしないようにするには(ブラウザのシークレット モードなど)
ユースケースが上記のいずれにも当てはまらない場合は、ユーザーが積極的にコンテンツを操作していないときにアプリが再生を継続しても問題ないかどうかを検討してください。答えが「はい」であれば、MediaSession
を選択することをおすすめします。使用できない場合は、代わりに Player
を使用することをおすすめします。
メディア セッションを作成する
メディア セッションは、それが管理するプレーヤーとともに存在します。メディア セッションは、Context
オブジェクトと Player
オブジェクトを使用して構築できます。メディア セッションとそれに関連付けられたプレーヤーを所有する Service
の 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 ライブラリは、プレーヤーの状態を使用してメディア セッションを自動的に更新します。そのため、プレーヤーからセッションへのマッピングを手動で処理する必要はありません。
これは、プレーヤー自体とは独立して 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 スマートウォッチへのアクセス権を付与したりすることもできます。外部クライアントは、メディア コントローラを使用してメディアアプリに再生コマンドを発行できます。これらはメディア セッションによって受信され、最終的にメディア プレーヤーにコマンドが委任されます。
コントローラがメディア セッションに接続しようとすると、onConnect()
メソッドが呼び出されます。提供された ControllerInfo
を使用して、リクエストを承認するか拒否するかを決定できます。接続リクエストを受け入れる例については、使用可能なコマンドを宣言するをご覧ください。
接続後、コントローラからセッションに再生コマンドを送信できます。その後、セッションはこれらのコマンドをプレーヤーにデリゲートします。Player
インターフェースで定義された再生コマンドとプレイリスト コマンドは、セッションによって自動的に処理されます。
他のコールバック メソッドでは、カスタム再生コマンドや再生リストの変更などのリクエストを処理できます。これらのコールバックにも同様に ControllerInfo
オブジェクトが含まれているため、コントローラごとに各リクエストへの応答方法を変更できます。
再生リストを変更する
メディア セッションは、プレイリストに関する ExoPlayer のガイドで説明されているように、プレーヤーのプレイリストを直接変更できます。コントローラが COMMAND_SET_MEDIA_ITEM
または COMMAND_CHANGE_MEDIA_ITEMS
のいずれかを使用できる場合、コントローラもプレイリストを変更できます。
プレイリストに新しいアイテムを追加する場合、プレーヤーでは通常、それらのアイテムを再生可能にするために、定義済みの URI を持つ MediaItem
インスタンスが必要になります。デフォルトでは、新しく追加されたアイテムは、player.addMediaItem
などのプレーヤー メソッドに URI が定義されている場合、自動的に転送されます。
プレーヤーに追加された 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
からカスタム コマンド リクエストを受信するには、Callback
の onCustomCommand()
メソッドをオーバーライドします。
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()
など)の動作をカスタマイズするには、Player
を ForwardingPlayer
でラップします。
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 の詳細を独自に処理できる(または処理する必要があります)。