使用 MediaSessionService 進行背景播放

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

使用 MediaSessionService

如要啟用背景播放功能,應在獨立的 Service 中包含 PlayerMediaSession。即使應用程式不在前景,裝置仍可繼續提供媒體服務。

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 的 Activity 或 Fragment 中,您可以使用 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 媒體控制項

如要進一步自訂通知本身,請建立 MediaNotification.Provider,並使用 DefaultMediaNotificationProvider.Builder,或建立提供者介面的自訂實作項目。使用 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 繼續播放功能要求繼續播放時,系統會呼叫 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, metadata (like title
    // and artwork) of the current item 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, 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 準備播放器並在回呼完成時開始播放前,使用這些參數設定播放器。

這個方法會在啟動期間呼叫,以便在裝置重新啟動後建立 Android 系統 UI 繼續通知。如要顯示豐富通知,建議您使用本地可用的值填入目前項目的 MediaMetadata 欄位,例如 titleartworkDataartworkUri,因為網路存取可能尚未開放。您也可以在 MediaMetadata.extras 中新增 MediaConstants.EXTRAS_KEY_COMPLETION_STATUSMediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE,指出要從哪個位置繼續播放。

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

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

這些外部用戶端可能會使用舊版 AndroidX 程式庫的 MediaControllerCompat 或 Android 平台的 android.media.session.MediaController 等 API。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。