使用 MediaSessionService 進行背景播放

通常,當應用程式不在前景運作時,建議播放媒體。適用對象 一般而言,音樂播放器會在使用者鎖定時繼續播放音樂 或正在使用其他應用程式Media3 程式庫提供一系列 可直接在背景播放的介面。

使用 MediaSessionService

如要啟用背景播放功能,應包含 Player 和 獨立 Service 中的 MediaSession。 這可讓裝置在未加入應用程式的情況下,繼續放送媒體 前景

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

在 Service 中代管玩家時,應使用 MediaSessionService。 為此,請先建立可擴充 MediaSessionService` 的類別,然後建立 加入媒體工作階段

使用 MediaSessionService 讓 Google 這類的外部用戶端都能使用 探索 Google 助理、系統媒體控制項,或 Wear OS 等隨附裝置 連線至 Google 服務時,您無需存取 應用程式的 UI 活動事實上,網站可能會連結多個用戶端應用程式 同時新增至相同的 MediaSessionService,每個應用程式都有專屬 MediaController

實作服務生命週期

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

  • 第一個控制器即將連線時,系統會呼叫 onCreate(), 並將服務執行個體化這項產品是建構 Player 和建構應用程式的最佳平台 MediaSession
  • 當使用者從應用程式通知中關閉應用程式時,系統會呼叫 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" />

您也必須使用意圖篩選器在資訊清單中宣告 Service 類別 (共 MediaSessionService 個)。

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
    </intent-filter>
</service>

您必須定義 foregroundServiceType敬上 如果應用程式是在搭載 Android 的裝置上執行,則包含 mediaPlayback 10 (API 級別 29) 以上版本。

使用 MediaController 控製播放功能

在包含播放器 UI 的活動或片段中,您可以建立連結 使用 MediaController 可在使用者介面和媒體工作階段之間來回切換。您的 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 媒體 控制項

自訂通知

如要自訂通知,請 MediaNotification.Provider敬上 合作對象:DefaultMediaNotificationProvider.Builder 或建立自訂實作供應商介面。新增 透過以下方式將供應商加到您的「MediaSessionService」: setMediaNotificationProvider

繼續播放

媒體按鈕是 Android 裝置和其他週邊裝置的硬體按鈕 裝置,例如藍牙耳機上的播放或暫停按鈕。媒體 3 服務執行期間,為您處理媒體按鈕輸入內容。

宣告 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;
}

如果你儲存了播放速度、重複模式或 隨機播放模式,onPlaybackResumption() 很適合用來設定玩家 在 Media3 準備播放器並開始播放 回呼作業完成。

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

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

這些外部用戶端可能會使用舊版 API 的 MediaControllerCompat 等 API AndroidX 程式庫或 Android 的 android.media.session.MediaController 這個架構的重點在於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 模組 用於證明支援 Automotive OS 需要獨立的 APK。