使用 MediaSessionService 在后台播放

通常,在应用不在前台运行时播放媒体内容是可取的做法。例如,当用户锁定设备或使用其他应用时,音乐播放器通常会继续播放音乐。Media3 库提供了一系列接口,可让您支持后台播放。

使用 MediaSessionService

如需启用后台播放,您应在单独的 Service 中包含 PlayerMediaSession。这样一来,即使您的应用不在前台运行,设备也能继续提供媒体内容。

MediaSessionService 允许媒体会话独立于应用的 activity 运行
图 1MediaSessionService 允许媒体会话独立于应用的 activity 运行

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

使用 MediaSessionService 可以让 Google 助理、系统媒体控件等外部客户端或 Wear OS 等配套设备发现您的服务、连接到该服务并控制播放,而根本无需访问应用的界面 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 类,并为其设置 intent 过滤器 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 控制播放

在包含播放器界面的 activity 或 fragment 中,您可以使用 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();

应用可以自定义 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 系统界面恢复功能请求恢复播放时,系统会调用 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 准备好播放器并在回调完成时开始播放之前,再使用这些参数配置播放器。

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

一个常见场景是在应用界面中使用 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 的支持。