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 ライブラリは、プレーヤーの状態を使用してメディア セッションを自動的に更新します。そのため、プレーヤーからセッションへのマッピングを手動で処理する必要はありません。
これは、エラーを示すなど、プレーヤー自体から独立して 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
インスタンスが必要です。デフォルトでは、URI が定義されている場合、新しく追加されたアイテムは player.addMediaItem
などのプレーヤー メソッドに自動的に転送されます。
プレーヤーに追加された 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
を 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
メソッドを呼び出します。
アプリは、MediaSession.Callback.onMediaButtonEvent(Intent)
をオーバーライドすることでデフォルトの動作をオーバーライドできます。このような場合、アプリはすべての API 固有の処理を独自に処理できます。また、処理する必要があります。
エラー処理と報告
セッションから出力され、コントローラに報告されるエラーには 2 種類あります。致命的なエラーは、再生を中断するセッション プレーヤーの技術的な再生障害を報告します。致命的なエラーは、発生すると自動的にコントローラに報告されます。非致命的なエラーは、技術的でないエラーまたはポリシーエラーで、再生を中断せず、アプリによってコントローラに手動で送信されます。
致命的な再生エラー
致命的な再生エラーは、プレーヤーからセッションに報告され、Player.Listener.onPlayerError(PlaybackException)
と Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)
を介してコントローラに報告されます。
この場合、再生ステータスは STATE_IDLE
に遷移し、MediaController.getPlaybackError()
は遷移の原因となった PlaybackException
を返します。コントローラは PlayerException.errorCode
を検査して、エラーの原因に関する情報を取得できます。
相互運用性を確保するため、致命的なエラーは、状態を STATE_ERROR
に移行し、PlaybackException
に従ってエラーコードとメッセージを設定することで、プラットフォーム セッションの PlaybackStateCompat
に複製されます。
致命的なエラーのカスタマイズ
ローカライズされた有意な情報をユーザーに提供するには、セッションの作成時に 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();
転送プレーヤーは実際のプレーヤーに Player.Listener
を登録し、エラーを報告するコールバックをインターセプトします。その後、カスタマイズされた PlaybackException
が、転送プレーヤーに登録されているリスナーに委任されます。これを機能させるには、転送プレーヤーが Player.addListener
と Player.removeListener
をオーバーライドして、カスタマイズされたエラーコード、メッセージ、または追加情報を送信するリスナーにアクセスできるようにします。
Kotlin
class ErrorForwardingPlayer(private val context: Context, player: Player) : ForwardingPlayer(player) { private val listeners: MutableList<Player.Listener> = mutableListOf() private var customizedPlaybackException: PlaybackException? = null init { player.addListener(ErrorCustomizationListener()) } override fun addListener(listener: Player.Listener) { listeners.add(listener) } override fun removeListener(listener: Player.Listener) { listeners.remove(listener) } override fun getPlayerError(): PlaybackException? { return customizedPlaybackException } private inner class ErrorCustomizationListener : Player.Listener { override fun onPlayerErrorChanged(error: PlaybackException?) { customizedPlaybackException = error?.let { customizePlaybackException(it) } listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) } } override fun onPlayerError(error: PlaybackException) { listeners.forEach { it.onPlayerError(customizedPlaybackException!!) } } private 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) } // Apps can customize further error messages by adding more branches. 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) } override fun onEvents(player: Player, events: Player.Events) { listeners.forEach { it.onEvents(player, events) } } // Delegate all other callbacks to all listeners without changing arguments like onEvents. } }
Java
private static class ErrorForwardingPlayer extends ForwardingPlayer { private final Context context; private List<Player.Listener> listeners; @Nullable private PlaybackException customizedPlaybackException; public ErrorForwardingPlayer(Context context, Player player) { super(player); this.context = context; listeners = new ArrayList<>(); player.addListener(new ErrorCustomizationListener()); } @Override public void addListener(Player.Listener listener) { listeners.add(listener); } @Override public void removeListener(Player.Listener listener) { listeners.remove(listener); } @Nullable @Override public PlaybackException getPlayerError() { return customizedPlaybackException; } private class ErrorCustomizationListener implements Listener { @Override public void onPlayerErrorChanged(@Nullable PlaybackException error) { customizedPlaybackException = error != null ? customizePlaybackException(error, context) : null; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onPlayerErrorChanged(customizedPlaybackException); } } @Override public void onPlayerError(PlaybackException error) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException)); } } private PlaybackException customizePlaybackException( PlaybackException error, Context context) { 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; // Apps can customize further error messages by adding more case statements. 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); } @Override public void onEvents(Player player, Events events) { for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onEvents(player, events); } } // Delegate all other callbacks to all listeners without changing arguments like onEvents. } }
致命的でないエラー
技術的な例外から発生していない致命的でないエラーは、アプリからすべてのコントローラまたは特定のコントローラに送信できます。
Kotlin
val sessionError = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired), ) // Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError) // Interoperability: Sending a nonfatal error to the media notification controller 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)); // Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError); // Interoperability: Sending a nonfatal error to the media notification controller 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); }
メディア通知コントローラに送信された致命的でないエラーは、プラットフォーム セッションの PlaybackStateCompat
に複製されます。これにより、エラーコードとエラー メッセージのみが PlaybackStateCompat
に設定され、PlaybackStateCompat.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. } });