使用 MediaSessionService 在后台播放

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

使用 MediaSessionService

如需启用后台播放功能,您应在单独的 Service 中包含 PlayerMediaSession。这样,即使您的应用未在前台运行,设备也可以继续传送媒体。

MediaSessionService 允许媒体会话与应用的 Activity 分开运行
图 1MediaSessionService 允许媒体会话与应用的 activity 分开运行

在 Service 中托管播放器时,您应使用 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) {
      // 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) {
      // 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 的 intent 过滤器在清单中声明 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 控制播放

在包含播放器界面的 activity 或 fragment 中,您可以使用 MediaController 在界面和媒体会话之间建立关联。您的界面使用媒体控制器在会话内将命令从界面发送到播放器。如需详细了解如何创建和使用 MediaController,请参阅创建 MediaController 指南。

处理界面命令

MediaSession 通过其 MediaSession.Callback 从控制器接收命令。初始化 MediaSession 会创建 MediaSession.Callback 的默认实现,该实现会自动处理 MediaController 发送给播放器的所有命令。

通知

MediaSessionService 会自动为您创建一个在大多数情况下都应该有效的 MediaNotification。默认情况下,已发布的通知是 MediaStyle 通知,它会使用媒体会话的最新信息进行更新,并显示播放控件。MediaNotification 可识别您的会话,可用于控制连接到同一会话的任何其他应用的播放。

例如,使用 MediaSessionService 的音乐在线播放应用会根据 MediaSession 配置创建 MediaNotification,用于显示当前媒体项的标题、音乐人和专辑封面,以及播放控件。

所需的元数据可以在媒体中提供,或声明为媒体项的一部分,如以下代码段所示:

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

如果您已存储其他参数(例如播放速度、重复模式或随机播放模式),那么在 Media3 准备播放器并在回调完成后开始播放之前,最好使用 onPlaybackResumption() 来使用这些参数配置播放器。

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

一种常见场景是在应用界面中使用 MediaController 来控制播放和显示播放列表。同时,该会话会公开给外部客户端,例如移动设备或电视上的 Android 媒体控件和 Google 助理、适用于手表的 Wear OS 和车载 Android Auto。Media3 会话演示应用就是实现此类场景的应用示例。

这些外部客户端可以使用 API,例如旧版 AndroidX 库的 MediaControllerCompat 或 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();
}

会话演示应用包含一个汽车模块,用于演示对需要单独 APK 的 Automotive OS 的支持。