使用 MediaSessionService 進行背景播放

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

使用 MediaSessionService

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

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

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

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

實作服務生命週期

您需要實作服務的兩種生命週期方法:

  • 當第一個控制器即將連線,且服務已例項化及啟動時,系統會呼叫 onCreate()。這是建構 PlayerMediaSession 的最佳位置。
  • 在服務停止時,系統會呼叫 onDestroy()。所有資源 (包括播放器和工作階段) 都需要釋放。

您可以選擇覆寫 onTaskRemoved(Intent),自訂使用者從近期工作中關閉應用程式時的行為。根據預設,如果播放作業正在進行,服務會繼續執行,否則會停止。

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

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

  // 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?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

對於 onTaskRemoved 的任何其他手動實作,您可以使用 isPlaybackOngoing() 檢查播放是否視為持續進行,以及是否已啟動前景服務。

提供媒體工作階段存取權

覆寫 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_SERVICEFOREGROUND_SERVICE_MEDIA_PLAYBACK 權限,才能執行播放前景服務:

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

您還必須在資訊清單中宣告 Service 類別,並使用 MediaSessionService 的意圖篩選器和包含 mediaPlaybackforegroundServiceType

<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 會建立 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();

通知生命週期

只要 Player 的播放清單中有 MediaItem 例項,系統就會建立通知。

所有通知更新都會根據 PlayerMediaSession 狀態自動發生。

前景服務執行期間無法移除通知。如要立即移除通知,您必須呼叫 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>

實作播放繼續回呼

當藍牙裝置或 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;
}

如果您已儲存其他參數 (例如播放速度、重複模式或隨機播放模式),在 Media3 準備播放器並在回呼完成時開始播放之前,您可以使用 onPlaybackResumption() 設定播放器參數。

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

常見的情況是,在應用程式 UI 中使用 MediaController 來控制播放功能並顯示播放清單。同時,工作階段會提供給外部用戶端,例如 Android 媒體控制項和行動裝置或電視上的 Google 助理、手錶的 Wear OS 和車輛的 Android Auto。Media3 工作階段示範應用程式就是實作這類情境的應用程式範例。

這些外部用戶端可能會使用 API,例如舊版 AndroidX 程式庫的 MediaControllerCompat,或是 Android 平台的 android.media.session.MediaController。Media3 可完全回溯相容於舊版程式庫,並提供與 Android 平台 API 的互通性。

使用媒體通知控制器

請務必瞭解,這些舊版和平台控制器共用相同的狀態,且無法由控制器自訂可見度 (例如可用的 PlaybackState.getActions()PlaybackState.getCustomActions())。您可以使用媒體通知控制器,設定平台媒體工作階段中的狀態集,以便與這些舊版和平台控制器相容。

舉例來說,應用程式可以提供 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 button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences 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 button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences 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 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 for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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