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

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

MediaSessionService を使用する

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

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

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

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

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

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

  • onCreate() は、最初のコントローラが接続され、サービスがインスタンス化されて開始されるときに呼び出されます。Player と MediaSession を構築するのに最適な場所です。
  • onDestroy() は、サービスが停止されるときに呼び出されます。プレーヤーやセッションを含むすべてのリソースを解放する必要があります。

必要に応じて onTaskRemoved(Intent) をオーバーライドして、ユーザーが最近のタスクからアプリを閉じたときの動作をカスタマイズできます。デフォルトでは、再生が進行中の場合はサービスが実行されたままになり、それ以外の場合は停止します。

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

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();
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

バックグラウンドで再生を継続する代わりに、ユーザーがアプリを閉じたときにサービスを停止することもできます。

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

onTaskRemoved のその他の手動実装では、isPlaybackOngoing() を使用して、再生が進行中と見なされ、フォアグラウンド サービスが開始されているかどうかを確認できます。

メディア セッションへのアクセスを提供する

onGetSession() メソッドをオーバーライドして、サービスの作成時に構築されたメディア セッションへのアクセス権を他のクライアントに付与します。

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

マニフェストでサービスを宣言する

アプリが再生フォアグラウンド サービスを実行するには、FOREGROUND_SERVICE 権限と FOREGROUND_SERVICE_MEDIA_PLAYBACK 権限が必要です。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

また、マニフェストで Service クラスを宣言する必要があります。その際、MediaSessionService のインテント フィルタと、mediaPlayback を含む foregroundServiceType を使用します。

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

MediaController を使用して再生を制御する

プレーヤー UI を含むアクティビティまたはフラグメントで、MediaController を使用して UI とメディア セッションの間のリンクを確立できます。UI は、メディア コントローラを使用して、UI からセッション内のプレーヤーにコマンドを送信します。MediaController の作成と使用の詳細については、MediaController を作成するガイドをご覧ください。

MediaController コマンドを処理する

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();

通知のライフサイクル

Player のプレイリストに MediaItem 個のインスタンスが追加されると、すぐに通知が作成されます。

すべての通知の更新は、Player と MediaSession の状態に基づいて自動的に行われます。

フォアグラウンド サービスが実行されている間は、通知を削除できません。通知をすぐに削除するには、Player.release() を呼び出すか、Player.clearMediaItems() を使用してプレイリストを消去する必要があります。

プレーヤーが一時停止、停止、または失敗し、10 分以上ユーザー操作がない場合、サービスは自動的にフォアグラウンド サービスの状態から移行し、システムによって破棄されます。再生の再開を実装すると、ユーザーがサービス ライフサイクルを再開して、後で再生を再開できるようになります。

通知のカスタマイズ

現在再生中のアイテムに関するメタデータは、MediaItem.MediaMetadata を変更することでカスタマイズできます。既存のアイテムのメタデータを更新する場合は、Player.replaceMediaItem を使用して、再生を中断することなくメタデータを更新できます。

Android メディア コントロールのカスタム メディアボタンの設定を行うことで、通知に表示されるボタンの一部をカスタマイズすることもできます。詳しくは、Android メディア コントロールのカスタマイズをご覧ください。

通知自体をさらにカスタマイズするには、DefaultMediaNotificationProvider.Builder を使用するか、プロバイダ インターフェースのカスタム実装を作成して、MediaNotification.Provider を作成します。setMediaNotificationProvider を使用して、プロバイダを MediaSessionService に追加します。

再生の再開

MediaSessionService が終了した後、デバイスが再起動された後でも、再生の再開を提供して、ユーザーがサービスを再起動し、中断したところから再生を再開できるようにすることができます。デフォルトでは、再生の再開はオフになっています。つまり、サービスが実行されていない場合、ユーザーは再生を再開できません。この機能を有効にするには、メディアボタン レシーバを宣言し、onPlaybackResumption メソッドを実装する必要があります。

Media3 メディアボタン レシーバーを宣言する

まず、マニフェストで 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: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(
      () -> {
        // Your app is responsible for storing the playlist, metadata (like title
        // and artwork) of the current item and the start position to use here.
        MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
        settableFuture.set(resumptionPlaylist);
      },
      MoreExecutors.directExecutor());
  return settableFuture;
}

再生速度、リピートモード、シャッフル モードなどの他のパラメータを保存している場合、onPlaybackResumption() は、Media3 がプレーヤーを準備してコールバックの完了時に再生を開始する前に、これらのパラメータでプレーヤーを構成するのに適した場所です。

このメソッドは、起動時に呼び出され、isForPlayback が false に設定されたデバイスの再起動後に Android システム UI の再開通知を作成します。リッチ通知の場合、ネットワーク アクセスがまだ利用できない可能性があるため、現在のアイテムの title、artworkData、artworkUri などの MediaMetadata フィールドに、ローカルで利用可能な値を入力することをおすすめします。MediaMetadata.extras に MediaConstants.EXTRAS_KEY_COMPLETION_STATUS と MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE を追加して、再開再生位置を示すこともできます。

高度なコントローラ構成と下位互換性

一般的なシナリオは、アプリの UI で MediaController を使用して再生を制御し、プレイリストを表示することです。同時に、セッションは、Android メディア コントロールやモバイルまたはテレビのアシスタント、スマートウォッチの Wear OS、車の Android Auto などの外部クライアントに公開されます。Media3 のセッション デモアプリは、このようなシナリオを実装するアプリの例です。

これらの外部クライアントは、以前の AndroidX ライブラリの MediaControllerCompat や Android プラットフォームの android.media.session.MediaController などの API を使用している可能性があります。Media3 は、以前のライブラリと完全に下位互換性があり、Android プラットフォーム API との相互運用性を提供します。

信頼できるコントローラを特定する

どのアプリでも、メディア セッションまたはライブラリに接続しようとすることができます。システム コントローラ、メディア コンテンツ制御権限を持つコントローラ、独自のアプリへのアクセスを制限する場合は、ControllerInfo.isTrusted() を使用して基本的なアクセス チェックを行うことができます。また、以下のセクションで説明するように、メディア通知コントローラや Android Auto コントローラなど、より具体的なコントローラを識別することもできます。

メディア通知コントローラを使用する

これらのレガシー コントローラとプラットフォーム コントローラは同じ状態を共有し、コントローラごとに可視性をカスタマイズすることはできません(たとえば、使用可能な PlaybackState.getActions() と PlaybackState.getCustomActions())。メディア通知コントローラを使用すると、これらのレガシー コントローラとプラットフォーム コントローラとの互換性を確保するために、プラットフォーム メディア セッションで設定された状態を構成できます。

たとえば、アプリは MediaSession.Callback.onConnect() の実装を提供して、次のようにプラットフォーム セッション専用に使用可能なコマンドとメディアボタンの設定を指定できます。

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 button preferences and commands to configure the platform session.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 button preferences and commands to configure the platform session.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

Android Auto にカスタム コマンドの送信を許可する

MediaLibraryService を使用してモバイルアプリで Android Auto をサポートする場合、Android Auto コントローラには適切な利用可能なコマンドが必要です。そうでない場合、Media3 はそのコントローラからの受信カスタム コマンドを拒否します。

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

セッション デモアプリには、Automotive OS のサポートを示す自動車モジュールがあり、別の APK が必要です。