使用 MediaSessionService 進行背景播放

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

使用 MediaSessionService

如要啟用背景播放功能,請在獨立的服務中加入 Player 和 MediaSession。這樣一來,即使應用程式不在前景,裝置也能繼續提供媒體。

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

在 Service 中託管播放器時,應使用 MediaSessionService。如要這麼做,請建立擴充 MediaSessionService 的類別,並在其中建立媒體工作階段。

使用 MediaSessionService 可讓外部用戶端 (例如 Google 助理、系統媒體遙控器、周邊裝置上的媒體按鈕,或 Wear OS 等配對裝置) 探索服務、連線至服務,以及控制播放,完全不必存取應用程式的 UI 活動。事實上,多個用戶端應用程式可以同時連線至同一個 MediaSessionService,每個應用程式都有自己的 MediaController。

實作服務生命週期

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

  • 當第一個控制器即將連線,且服務已例項化並啟動時,系統會呼叫 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 的 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 個例項時,系統就會建立通知。

所有通知更新都會根據 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>

實作繼續播放回呼

當藍牙裝置或 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 會在回呼完成時準備播放器並開始播放。

系統會在啟動期間呼叫這個方法,在裝置重新啟動後建立 Android 系統 UI 繼續通知,並將 isForPlayback 設為 false。如果是多媒體通知,建議您在 MediaMetadata 欄位 (例如目前項目的 title 和 artworkData 或 artworkUri) 填入本機可用的值,因為可能還無法存取網路。您也可以在 MediaMetadata.extras 中加入 MediaConstants.EXTRAS_KEY_COMPLETION_STATUS 和 MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE,指出恢復播放的位置。

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

常見的情境是在應用程式 UI 中使用 MediaController 控制播放及顯示播放清單。同時,工作階段會向外部用戶端公開,例如 Android 媒體遙控器和行動裝置或電視上的 Google 助理、手錶上的 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。