MediaSession は、音声や動画のプレーヤーを操作するための汎用的な手段を提供します。Media3 では、デフォルトのプレーヤーは ExoPlayer
クラスで、Player
インターフェースを実装しています。メディア セッションをプレーヤーに接続すると、アプリはメディアの再生を外部に宣伝し、外部ソースから再生コマンドを受信できます。
コマンドは、ヘッドセットの再生ボタンやテレビのリモコンなどの物理ボタンから発信される場合があります。また、メディア コントローラを備えたクライアント アプリ(Google アシスタントに「一時停止」を指示するアプリなど)から送信されることもあります。メディア セッションは、これらのコマンドをメディアアプリのプレーヤーに委任します。
メディア セッションを選択するタイミング
MediaSession
を実装すると、ユーザーは再生を制御できます。
- ヘッドフォンで。多くの場合、ヘッドフォンにボタンやタップ操作があり、ユーザーがメディアの再生や一時停止、次のトラックや前のトラックへの移動を行うことができます。
- Google アシスタントに話しかける。一般的なパターンは、「OK Google, 一時停止して」と話しかけて、デバイスで現在再生中のメディアを一時停止することです。
- Wear OS スマートウォッチで。これにより、スマートフォンで再生中に、最も一般的な再生コントロールに簡単にアクセスできるようになります。
- メディア コントロール。このカルーセルには、実行中の各メディア セッションのコントロールが表示されます。
- [テレビ] に移動します。物理的な再生ボタン、プラットフォームの再生コントロール、電源管理によるアクションを許可します(たとえば、テレビ、サウンドバー、A/V レシーバーの電源がオフになった場合や入力が切り替わった場合、アプリで再生が停止する必要があります)。
- 再生に影響を与える必要がある他の外部プロセス。
これは多くのユースケースに適しています。特に、次の場合は MediaSession
の使用を強く検討してください。
- 映画やライブテレビなどの長尺動画コンテンツをストリーミングしている。
- ポッドキャストや音楽プレイリストなどの長尺オーディオ コンテンツをストリーミングしている。
- テレビアプリを作成している。
ただし、すべてのユースケースが 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 として空の文字列を持つセッションを作成します。アプリが 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
インスタンスが必要です。デフォルトでは、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 つの要素を定義します。
- アイコン: 外観を定義します。
CommandButton.Builder
を作成するときに、アイコンを事前定義された定数に設定する必要があります。これは実際のビットマップや画像リソースではありません。汎用定数は、コントローラが独自の UI 内で一貫した外観と操作感を実現するために適切なリソースを選択するのに役立ちます。事前定義されたアイコン定数でユースケースに合うものがない場合は、代わりにsetCustomIconResId
を使用できます。 - コマンド: ユーザーがボタンを操作したときにトリガーされるアクションを定義します。
Player.Command
にはsetPlayerCommand
を使用し、事前定義またはカスタムのSessionCommand
にはsetSessionCommand
を使用できます。 - スロット: コントローラ UI でボタンを配置する場所を定義します。このフィールドは省略可能で、[アイコン] と [コマンド] に基づいて自動的に設定されます。たとえば、デフォルトの「オーバーフロー」領域ではなく、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();
メディアボタンの設定が解決されると、次のアルゴリズムが適用されます。
- メディアボタンの設定の
CommandButton
ごとに、使用可能な最初のスロットにボタンを配置します。 - 中央、前方、後方のスロットのいずれかにボタンが配置されていない場合は、このスロットにデフォルトのボタンを追加します。
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
からカスタム コマンド リクエストを受信するには、Callback
で onCustomCommand()
メソッドをオーバーライドします。
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
プロパティを使用して、リクエストを行っているメディア コントローラを追跡できます。これにより、システム、独自のアプリ、または他のクライアント アプリから発信された特定のコマンドに応じて、アプリの動作を調整できます。
デフォルトのプレーヤー コマンドをカスタマイズする
デフォルト コマンドと状態処理はすべて、MediaSession
にある Player
に委任されます。Player
インターフェースで定義されたコマンドの動作(play()
や seekToNext()
など)をカスタマイズするには、Player
を ForwardingSimpleBasePlayer
でラップしてから 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 ガイドをご覧ください。
プレーヤー コマンドをリクエストしたコントローラを特定する
Player
メソッドの呼び出しが MediaController
によって開始された場合は、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.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. } });