使用 MediaSessionService 進行背景播放

通常在應用程式未在前景執行時,播放媒體會比較理想。舉例來說,當使用者鎖定裝置或使用其他應用程式時,音樂播放器通常會繼續播放音樂。Media3 程式庫提供一系列介面,可讓您支援背景播放功能。

使用 MediaSessionService

如要啟用背景播放功能,請將 PlayerMediaSession 包含在個別的 Service 中。如此一來,即使應用程式不在前景運作,裝置仍可繼續提供媒體。

MediaSessionService 可讓媒體工作階段與應用程式活動分開執行
圖 1MediaSessionService 可讓媒體工作階段與應用程式活動分開執行

在服務中代管玩家時,請使用 MediaSessionService。如要這麼做,請建立可擴充 MediaSessionService 的類別,並在其中建立媒體工作階段。

使用 MediaSessionService 後,Google 助理等外部用戶端、系統媒體控制項或 Wear OS 等隨附裝置就能探索您的服務、連線至服務,並控制播放作業,完全不需要存取應用程式的 UI 活動。事實上,多個用戶端應用程式可以同時連線至同一個 MediaSessionService,每個應用程式都有自己的 MediaController

實作服務生命週期

您必須實作服務的三個生命週期方法:

  • 當第一個控制器即將連線,且服務已例項化及啟動時,系統會呼叫 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) 以上版本的裝置上執行時,您必須定義包含 mediaPlaybackforegroundServiceType

使用 MediaController 控制播放

在含有播放器 UI 的活動或片段中,您可以使用 MediaController 在 UI 和媒體工作階段之間建立連結。您的 UI 會使用媒體控制器,將 UI 中的指令傳送至工作階段中的播放器。如要進一步瞭解如何建立及使用 MediaController,請參閱「建立 MediaController」指南。

處理 UI 指令

MediaSession 會透過其 MediaSession.Callback 接收來自控制器的指令。初始化 MediaSession 會建立 MediaSession.Callback 的預設實作項目,自動處理 MediaController 傳送至播放器的所有指令。

通知

MediaSessionService 會自動為您建立 MediaNotification,在大多數情況下都能正常運作。根據預設,發布的通知是 MediaStyle 通知,會持續更新媒體工作階段的最新資訊,並顯示播放控制項。MediaNotification 會偵測您的工作階段,並可用於控制與同一工作階段連結的任何其他應用程式的播放作業。

舉例來說,使用 MediaSessionService 的音樂串流應用程式會建立 MediaNotification,根據 MediaSession 設定,顯示目前播放的媒體項目標題、藝人和專輯封面,以及播放控制項。

您可以在媒體中提供必要的中繼資料,也可以在媒體項目中宣告中繼資料,如以下程式碼片段所示:

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 裝置和其他周邊裝置上的硬體按鈕,例如藍牙耳機上的播放或暫停按鈕。在服務執行期間,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>

實作播放繼續回呼

當藍牙裝置或 Android 系統 UI resumption feature 要求恢復播放時,系統會呼叫 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;
}

如果您已儲存其他參數,例如播放速度、重複模式或隨機播放模式,onPlaybackResumption() 是使用這些參數設定播放器的絕佳位置,在回呼完成時,Media3 會準備播放器並開始播放。

進階控制器設定和回溯相容性

常見的情況是,在應用程式 UI 中使用 MediaController 來控制播放功能並顯示播放清單。同時,工作階段會提供給外部用戶端,例如 Android 媒體控制項和行動裝置或電視上的 Google 助理、手錶的 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();
}

工作階段示範應用程式含有汽車模組,可展示 Automotive OS 的支援功能,但需要使用獨立的 APK。