使用 MediaSessionService 進行背景播放

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

使用 MediaSessionService

如要啟用背景播放功能,請在個別 Service 中加入 PlayerMediaSession。如此一來,即使應用程式不在前景,裝置仍會繼續提供媒體。

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

在「服務」中代管播放器時,您應使用 MediaSessionService。為此,請建立擴充 MediaSessionService 的類別,並在其中建立媒體工作階段。

使用 MediaSessionService 可讓外部用戶端 (例如 Google 助理、系統媒體控制項或 Wear OS) 隨附裝置探索、連結服務以及控製播放,而且完全不需要存取應用程式的使用者介面活動。事實上,多個用戶端應用程式可以同時連結至相同的 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 繼續功能要求播放繼續播放時,系統會呼叫 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 工作階段試用版應用程式就是實作這類情境的應用程式範例。

這些外部用戶端可能會使用舊版 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 支援。