アプリがフォアグラウンドで実行されていないときにメディアを再生することが望ましい場合もあります。たとえば、音楽プレーヤーは通常、ユーザーがデバイスをロックしているときや、別のアプリを使用しているときでも音楽の再生を続けます。Media3 ライブラリには、バックグラウンド再生をサポートできる一連のインターフェースが用意されています。
MediaSessionService を使用する
バックグラウンド再生を有効にするには、Player
と MediaSession
を個別の Service 内に含める必要があります。これにより、アプリがフォアグラウンドにない場合でも、デバイスはメディアの配信を継続できます。
Service 内でプレーヤーをホストする場合は、MediaSessionService
を使用する必要があります。これを行うには、MediaSessionService
を拡張するクラスを作成し、その中にメディア セッションを作成します。
MediaSessionService
を使用すると、Google アシスタント、システム メディア コントロール、Wear OS などのコンパニオン デバイスなどの外部クライアントが、アプリの UI アクティビティにまったくアクセスすることなく、サービスを検出して接続し、再生をコントロールできます。実際には、1 つの MediaSessionService
が同時に複数のクライアント アプリから接続されている可能性があり、各アプリには固有の MediaController
があります。
サービスのライフサイクルを実装する
サービスの次の 3 つのライフサイクル メソッドを実装する必要があります。
onCreate()
は、最初のコントローラが接続しようとしているときに呼び出され、サービスがインスタンス化されて開始されます。Player
とMediaSession
をビルドするのに最適な場所です。onTaskRemoved(Intent)
は、ユーザーが最近のタスクからアプリを閉じたときに呼び出されます。再生が進行中の場合、アプリはサービスをフォアグラウンドで実行し続けるように選択できます。プレーヤーが一時停止されている場合、サービスはフォアグラウンドにないため、停止する必要があります。onDestroy()
は、サービスが停止されるときに呼び出されます。プレーヤーやセッションを含むすべてのリソースを解放する必要があります。
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // Create your player and media session in the onCreate lifecycle event override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } // The user dismissed the app from the recent tasks override fun onTaskRemoved(rootIntent: Intent?) { val player = mediaSession?.player!! if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == Player.STATE_ENDED) { // Stop the service if not playing, continue playing in the background // otherwise. stopSelf() } } // Remember to release the player and media session in onDestroy override fun onDestroy() { mediaSession?.run { player.release() release() mediaSession = null } super.onDestroy() } }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // Create your Player and MediaSession in the onCreate lifecycle event @Override public void onCreate() { super.onCreate(); ExoPlayer player = new ExoPlayer.Builder(this).build(); mediaSession = new MediaSession.Builder(this, player).build(); } // The user dismissed the app from the recent tasks @Override public void onTaskRemoved(@Nullable Intent rootIntent) { Player player = mediaSession.getPlayer(); if (!player.getPlayWhenReady() || player.getMediaItemCount() == 0 || player.getPlaybackState() == Player.STATE_ENDED) { // Stop the service if not playing, continue playing in the background // otherwise. stopSelf(); } } // Remember to release the player and media session in onDestroy @Override public void onDestroy() { mediaSession.getPlayer().release(); mediaSession.release(); mediaSession = null; super.onDestroy(); } }
バックグラウンドで再生を継続する代わりに、ユーザーがアプリを閉じた場合に、アプリはどのような場合でもサービスを停止できます。
Kotlin
override fun onTaskRemoved(rootIntent: Intent?) { val player = mediaSession.player if (player.playWhenReady) { // Make sure the service is not in foreground. player.pause() } stopSelf() }
Java
@Override public void onTaskRemoved(@Nullable Intent rootIntent) { Player player = mediaSession.getPlayer(); if (player.getPlayWhenReady()) { // Make sure the service is not in foreground. player.pause(); } stopSelf(); }
メディア セッションへのアクセス権を付与する
onGetSession()
メソッドをオーバーライドして、サービス作成時に作成されたメディア セッションに他のクライアントがアクセスできるようにします。
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // [...] lifecycle methods omitted override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // [...] lifecycle methods omitted @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } }
マニフェストでサービスを宣言する
アプリがフォアグラウンド サービスを実行するには、権限が必要です。マニフェストに FOREGROUND_SERVICE
権限を追加します。API 34 以降をターゲットとしている場合は、FOREGROUND_SERVICE_MEDIA_PLAYBACK
も追加します。
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
また、マニフェストで MediaSessionService
のインテント フィルタを使用して Service
クラスを宣言する必要があります。
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
アプリが Android 10(API レベル 29)以降を搭載したデバイスで実行される場合は、mediaPlayback
を含む foregroundServiceType
を定義する必要があります。
MediaController
を使用して再生を制御する
プレーヤー UI を含むアクティビティまたはフラグメントで、MediaController
を使用して UI とメディア セッションのリンクを確立できます。UI はメディア コントローラを使用して、UI からセッション内のプレーヤーにコマンドを送信します。MediaController
の作成と使用の詳細については、MediaController
を作成するをご覧ください。
UI コマンドを処理する
MediaSession
は、MediaSession.Callback
を介してコントローラからコマンドを受け取ります。MediaSession
を初期化すると、MediaController
がプレーヤーに送信するすべてのコマンドを自動的に処理する MediaSession.Callback
のデフォルト実装が作成されます。
通知
MediaSessionService
は、ほとんどの場合に機能する MediaNotification
を自動的に作成します。デフォルトでは、公開された通知はMediaStyle
通知で、メディア セッションの最新情報で常に更新され、再生コントロールが表示されます。MediaNotification
はセッションを認識し、同じセッションに接続されている他のアプリの再生を制御するために使用できます。
たとえば、MediaSessionService
を使用する音楽ストリーミング アプリでは、MediaSession
の設定に基づいて再生中の現在のメディア アイテムのタイトル、アーティスト、アルバムアートを再生コントロールとともに表示する MediaNotification
を作成します。
必要なメタデータは、メディアで指定することも、次のスニペットのようにメディア アイテムの一部として宣言することもできます。
Kotlin
val mediaItem = MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build() ) .build() mediaController.setMediaItem(mediaItem) mediaController.prepare() mediaController.play()
Java
MediaItem mediaItem = new MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( new MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build()) .build(); mediaController.setMediaItem(mediaItem); mediaController.prepare(); mediaController.play();
アプリは、Android メディア コントロールのコマンドボタンをカスタマイズできます。詳しくは、Android メディア コントロールのカスタマイズをご覧ください。
通知のカスタマイズ
通知をカスタマイズするには、DefaultMediaNotificationProvider.Builder
を使用して MediaNotification.Provider
を作成するか、プロバイダ インターフェースのカスタム実装を作成します。setMediaNotificationProvider
を使用して、プロバイダを MediaSessionService
に追加します。
再生の再開
メディアボタンとは、Android デバイスや周辺デバイスにあるハードウェア ボタンのことです。たとえば、Bluetooth ヘッドセットの再生ボタンや一時停止ボタンなどです。Media3 は、サービスが実行されているときにメディアボタン入力を処理します。
Media3 メディアボタン レシーバを宣言する
Media3 には、アプリの終了後やデバイスの再起動後でも、ユーザーが再生を再開できるようにする API が含まれています。デフォルトでは、再生の再開はオフになっています。つまり、サービスが実行されていないときにユーザーが再生を再開することはできません。有効にするには、まずマニフェストで MediaButtonReceiver
を宣言します。
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
再生の再開コールバックを実装する
Bluetooth デバイスまたは Android システム UI の再開機能によって再生の再開がリクエストされると、onPlaybackResumption()
コールバック メソッドが呼び出されます。
Kotlin
override fun onPlaybackResumption( mediaSession: MediaSession, controller: ControllerInfo ): ListenableFuture<MediaItemsWithStartPosition> { val settable = SettableFuture.create<MediaItemsWithStartPosition>() scope.launch { // Your app is responsible for storing the playlist and the start position // to use here val resumptionPlaylist = restorePlaylist() settable.set(resumptionPlaylist) } return settable }
Java
@Override public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption( MediaSession mediaSession, ControllerInfo controller ) { SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create(); settableFuture.addListener(() -> { // Your app is responsible for storing the playlist and the start position // to use here MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist(); settableFuture.set(resumptionPlaylist); }, MoreExecutors.directExecutor()); return settableFuture; }
再生速度、リピート モード、シャッフル モードなどの他のパラメータを保存している場合は、Media3 がプレーヤーを準備し、コールバックが完了したときに再生を開始する前に、onPlaybackResumption()
でこれらのパラメータを使用してプレーヤーを構成することをおすすめします。
高度なコントローラ構成と下位互換性
一般的なシナリオは、アプリ UI で MediaController
を使用して再生を制御し、再生リストを表示することです。同時に、セッションは、モバイルやテレビの Android メディア コントロールやアシスタント、スマートウォッチの Wear OS、自動車の Android Auto などの外部クライアントに公開されます。Media3 のセッション デモアプリは、このようなシナリオを実装するアプリの例です。
これらの外部クライアントは、以前の AndroidX ライブラリの MediaControllerCompat
や Android フレームワークの android.media.session.MediaController
などの API を使用する場合があります。Media3 は以前のライブラリと完全に下位互換性があり、Android フレームワーク API との相互運用性を提供します。
メディア通知コントローラを使用する
これらのレガシー コントローラまたはフレームワーク コントローラは、フレームワークの PlaybackState.getActions()
と PlaybackState.getCustomActions()
から同じ値を読み取ることを理解することが重要です。フレームワーク セッションのアクションとカスタム アクションを決定するには、アプリでメディア通知コントローラを使用して、使用可能なコマンドとカスタム レイアウトを設定します。サービスはメディア通知コントローラをセッションに接続します。セッションは、コールバックの onConnect()
から返された ConnectionResult
を使用して、フレームワーク セッションのアクションとカスタム アクションを構成します。
モバイル専用のシナリオでは、アプリは MediaSession.Callback.onConnect()
の実装を提供して、フレームワーク セッションに固有の使用可能なコマンドとカスタム レイアウトを設定できます。次に例を示します。
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { if (session.isMediaNotificationController(controller)) { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() val playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build() // Custom layout and available commands to configure the legacy/framework session. return AcceptedResultBuilder(session) .setCustomLayout( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward)) ) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default custom layout for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { if (session.isMediaNotificationController(controller)) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); Player.Commands playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS .buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build(); // Custom layout and available commands to configure the legacy/framework session. return new AcceptedResultBuilder(session) .setCustomLayout( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward))) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands without default custom layout for all other controllers. return new AcceptedResultBuilder(session).build(); }
Android Auto にカスタム コマンドの送信を許可する
MediaLibraryService
を使用してモバイルアプリで Android Auto をサポートする場合、Android Auto コントローラには適切な使用可能なコマンドが必要です。そうしないと、Media3 は、そのコントローラからの受信カスタム コマンドを拒否します。
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { val sessionCommands = ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available session commands to accept incoming custom commands from Auto. return AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default custom layout for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available commands to accept incoming custom commands from Auto. return new AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands without default custom layout for all other controllers. return new AcceptedResultBuilder(session).build(); }
セッションのデモアプリには、個別の APK を必要とする Automotive OS のサポートを示す自動車モジュールがあります。