MediaSessionService によるバックグラウンド再生

アプリがフォアグラウンドにないときにメディアを再生するほうが望ましい場合がよくあります。たとえば、一般的に音楽プレーヤーは、ユーザーがデバイスをロックしたり別のアプリを使用したりしても、音楽の再生を続けます。Media3 ライブラリには、バックグラウンド再生をサポートできる一連のインターフェースが用意されています。

MediaSessionService を使用する

バックグラウンド再生を有効にするには、PlayerMediaSession を個別の Service 内に含める必要があります。これにより、アプリがフォアグラウンドにないときでも、デバイスはメディアの配信を継続できます。

MediaSessionService を使用すると、メディア セッションをアプリのアクティビティとは別に実行できるようになります。
図 1: MediaSessionService により、メディア セッションをアプリのアクティビティとは別に実行できるようになります。

Service 内でプレーヤーをホストする場合は、MediaSessionService を使用する必要があります。そのためには、MediaSessionService を拡張するクラスを作成し、その内部にメディア セッションを作成します。

MediaSessionService を使用すると、アプリの UI アクティビティにまったくアクセスすることなく、Google アシスタントなどの外部クライアント、システム メディア コントロール、Wear OS などのコンパニオン デバイスがサービスの検出、サービスへの接続、再生の制御を行うことができます。実際、複数のクライアント アプリが同時に同じ MediaSessionService に接続され、各アプリが独自の MediaController を持つこともあります。

サービス ライフサイクルを実装する

サービスの 3 つのライフサイクル メソッドを実装する必要があります。

  • onCreate() は、最初のコントローラが接続しようとし、サービスがインスタンス化されて開始されたときに呼び出されます。これは、PlayerMediaSession をビルドするのに最適です。
  • 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 のサポートを示す Automotive モジュールがあります。