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
または 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 として空の文字列を使用してセッションを作成します。アプリが単一のセッション インスタンスのみを作成することを想定している場合(最も一般的なケース)は、これで十分です。
アプリが複数のセッション インスタンスを同時に管理する場合は、各セッションのセッション 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
を使用できます。 - Command: ユーザーがボタンを操作したときにトリガーされるアクションを定義します。
Player.Command
にはsetPlayerCommand
を使用し、事前定義またはカスタムのSessionCommand
にはsetSessionCommand
を使用できます。 - コントローラの 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();
メディア ボタンの設定が解決されると、次のアルゴリズムが適用されます。
- メディアボタンの設定の各
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 ガイドのカスタマイズをご覧ください。
プレーヤー コマンドのリクエスト元コントローラを特定する
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.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. } });