使用 MediaSessionService 在后台播放

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

使用 MediaSessionService

如需启用后台播放,您应在单独的 Service 中包含 Player 和 MediaSession。这样,即使您的应用不在前台,设备也可以继续播放媒体内容。

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

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

使用 MediaSessionService,外部客户端(例如 Google 助理、系统媒体控件、外围设备上的媒体按钮或 Wear OS 等配套设备)可以发现您的服务、连接到该服务并控制播放,而无需访问应用的界面 activity。事实上,可以有多个客户端应用同时连接到同一个 MediaSessionService,每个应用都有自己的 MediaController。

实现服务生命周期

您需要实现服务的两个生命周期方法:

  • 当第一个控制器即将连接且服务已实例化并启动时,系统会调用 onCreate()。这里是构建 Player 和 MediaSession 的最佳场所。
  • 当服务停止时,系统会调用 onDestroy()。所有资源(包括播放器和会话)都需要释放。

您可以选择替换 onTaskRemoved(Intent),以自定义用户从最近的任务中关闭应用时发生的情况。默认情况下,如果正在播放,服务会继续运行,否则会停止。

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

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

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

作为在后台持续播放的替代方案,您可以在用户关闭应用时停止服务:

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

对于 onTaskRemoved 的任何其他手动实现,您可以使用 isPlaybackOngoing() 检查播放是否被视为正在进行,以及前台服务是否已启动。

提供对媒体会话的访问权限

替换 onGetSession() 方法,以便其他客户端访问在创建服务时构建的媒体会话。

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

在清单中声明服务

应用需要 FOREGROUND_SERVICE 和 FOREGROUND_SERVICE_MEDIA_PLAYBACK 权限才能运行播放前台服务:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

您还必须在清单中声明 Service 类,并为其添加 MediaSessionService intent 过滤器和包含 mediaPlayback 的 foregroundServiceType。

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

使用 MediaController 控制播放

在包含播放器界面的 activity 或 fragment 中,您可以使用 MediaController 在界面与媒体会话之间建立链接。界面使用媒体控制器将命令从界面发送到会话中的播放器。如需详细了解如何创建和使用 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();

通知生命周期

当 Player 的播放列表中有 MediaItem 个实例时,系统会立即创建通知。

所有通知更新都会根据 Player 和 MediaSession 状态自动进行。

在前台服务运行期间,无法移除该通知。如需立即移除通知,您必须调用 Player.release() 或使用 Player.clearMediaItems() 清除播放列表。

如果播放器暂停、停止或失败超过 10 分钟,且用户没有进一步互动,服务会自动退出前台服务状态,以便系统将其销毁。您可以实现播放恢复功能,以便用户在稍后的时间点重启服务生命周期并恢复播放。

通知自定义

您可以通过修改 MediaItem.MediaMetadata 来自定义当前播放内容的元数据。如果您想更新现有媒体项的元数据,可以使用 Player.replaceMediaItem 更新元数据,而不会中断播放。

您还可以通过为 Android 媒体控件设置自定义媒体按钮偏好设置,自定义通知中显示的部分按钮。 详细了解如何自定义 Android 媒体控件。

如需进一步自定义通知本身,请使用 DefaultMediaNotificationProvider.Builder 或通过创建提供程序接口的自定义实现来创建 MediaNotification.Provider。使用 setMediaNotificationProvider 将提供方添加到 MediaSessionService。

恢复播放

在 MediaSessionService 终止后,甚至在设备重启后,您可以提供恢复播放功能,让用户重启服务并从停止的位置继续播放。默认情况下,恢复播放功能处于关闭状态。这意味着,当您的服务未运行时,用户无法恢复播放。如需选择启用此功能,您需要声明媒体按钮接收器并实现 onPlaybackResumption 方法。

声明 Media3 媒体按钮接收器

首先,在清单中声明 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: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(
      () -> {
        // Your app is responsible for storing the playlist, metadata (like title
        // and artwork) of the current item and the start position to use here.
        MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
        settableFuture.set(resumptionPlaylist);
      },
      MoreExecutors.directExecutor());
  return settableFuture;
}

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

在启动期间,如果设备重启后 isForPlayback 设置为 false,系统会调用此方法来创建 Android 系统界面恢复通知。对于富通知,建议使用本地可用的值填充当前内容的 MediaMetadata 字段(例如 title 和 artworkData 或 artworkUri),因为网络访问可能尚不可用。您还可以将 MediaConstants.EXTRAS_KEY_COMPLETION_STATUS 和 MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE 添加到 MediaMetadata.extras,以指示恢复播放位置。

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

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

这些外部客户端可能会使用旧版 AndroidX 库的 MediaControllerCompat 或 Android 平台的 android.media.session.MediaController 等 API。Media3 与旧版库完全向后兼容,并可与 Android 平台 API 互操作。

识别可信控制器

任何应用都可以尝试连接到您的媒体会话或媒体库。如果您想限制对系统控制器、具有媒体内容控制权限的控制器以及您自己的应用的访问,可以使用 ControllerInfo.isTrusted() 进行基本访问权限检查。或者,您也可以识别更具体的控制器,例如媒体通知控制器或 Android Auto 控制器,如以下部分所述。

使用媒体通知控制器

请务必了解,这些旧版控制器和平台控制器共享相同的状态,并且无法按控制器自定义可见性(例如可用的 PlaybackState.getActions() 和 PlaybackState.getCustomActions())。您可以使用媒体通知控制器配置在平台媒体会话中设置的状态,以便与这些旧版控制器和平台控制器兼容。

例如,应用可以提供 MediaSession.Callback.onConnect() 的实现,以便专门为平台会话设置可用命令和媒体按钮偏好设置,如下所示:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 button preferences and commands to configure the platform session.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 button preferences and commands to configure the platform session.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

授权 Android Auto 发送自定义命令

使用 MediaLibraryService 且通过移动应用支持 Android Auto 时,Android Auto 控制器需要适当的可用命令,否则 Media3 会拒绝来自该控制器的传入自定义命令:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

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