通常,您可能希望在应用不在前台时播放媒体。例如,当用户锁定设备或使用其他应用时,音乐播放器通常会继续播放音乐。Media3 库提供了一系列接口,可让您支持后台播放。
使用 MediaSessionService
如需启用后台播放,您应在单独的 Service 中包含 Player 和 MediaSession。这样,即使您的应用不在前台,设备也可以继续播放媒体内容。
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。