使用 MediaSessionService 实现后台播放

通常情况下,希望在应用不在前台时播放媒体。例如,音乐播放器通常会在用户锁定设备或使用其他应用时继续播放音乐。Media3 库提供了一系列接口,允许您支持后台播放。

使用 MediaSessionService

要启用后台播放,您应该将 PlayerMediaSession 包含在一个单独的 服务 中。这使得即使您的应用不在前台,设备也可以继续提供媒体服务。

The MediaSessionService allows the media session to run separately
  from the app's activity
图 1MediaSessionService 允许媒体会话独立于应用的 Activity 运行

在服务中托管播放器时,您应该使用 MediaSessionService。为此,创建一个扩展 MediaSessionService 的类,并在其中创建您的媒体会话。

使用 MediaSessionService 使 Google 助理、系统媒体控件或 Wear OS 等伴侣设备等外部客户端能够发现您的服务,连接到它并控制播放,而无需完全访问您的应用的 UI Activity。实际上,可以有多个客户端应用同时连接到同一个 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" />

您还必须在清单中声明您的 Service 类,并使用 MediaSessionService 的意图过滤器。

<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 的 Activity 或 Fragment 中,您可以使用 MediaController 在 UI 和媒体会话之间建立链接。您的 UI 使用媒体控制器将命令从您的 UI 发送到会话内的播放器。有关创建和使用 MediaController 的详细信息,请参阅 创建 MediaController 指南。

处理 UI 命令

通过其 MediaSession.CallbackMediaSession 接收来自控制器的命令。初始化 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.ProviderDefaultMediaNotificationProvider.Builder 创建,或者通过创建提供程序接口的自定义实现。使用 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;
}

如果您已存储其他参数(如播放速度、重复模式或随机播放模式),则 onPlaybackResumption() 是在 Media3 准备播放器并在回调完成时开始播放之前使用这些参数配置播放器的理想位置。

高级控制器配置和向后兼容性

一个常见的情况是在应用 UI 中使用 MediaController 来控制播放和显示播放列表。同时,该会话向外部客户端(如移动设备或电视上的 Android 媒体控件和助手、手表的 Wear OS 以及汽车中的 Android Auto)公开。Media3 会话演示应用 是一个实现此类场景的应用示例。

这些外部客户端可能会使用旧版 AndroidX 库的 API(如 MediaControllerCompat)或 Android 框架的 API(如 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();
}

会话演示应用有一个 汽车模块,演示了对需要单独 APK 的汽车操作系统的支持。