使用 MediaSessionService 在后台播放

人们通常希望在应用不在前台时播放媒体。对于 例如,如果用户锁定了设备,音乐播放器通常会继续播放音乐 或使用其他应用Media3 库提供了一系列 可让您支持后台播放功能的接口。

使用 MediaSessionService

如需启用后台播放功能,应包含 PlayerMediaSession 位于单独的 Service 中。 这样,即使您的应用未打开,设备也能继续传送媒体内容 前景。

<ph type="x-smartling-placeholder">
</ph> MediaSessionService 允许媒体会话单独运行
  从应用的 activity
图 1MediaSessionService 允许媒体 与应用的 activity 分开运行

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

使用 MediaSessionService 可允许 Google 等外部客户端 支持 Google 助理、系统媒体控件或 Wear OS 等配套设备 您的服务、连接到该服务并控制播放,所有这些操作都无需访问您的 应用的界面活动事实上,您可以连接多个客户端应用 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" />

您还必须使用 intent 过滤器在清单中声明 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 控制播放

在包含播放器界面的 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 媒体 控件

通知自定义

要自定义通知,请创建一个 MediaNotification.ProviderDefaultMediaNotificationProvider.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 系统界面恢复功能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 会话演示应用 是实现此类场景的应用示例。

这些外部客户端可能会使用旧版的 MediaControllerCompat 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 需要相应的可用命令,否则 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 的支持。