当应用不在前台时,通常需要播放媒体。例如,当用户锁定设备或正在使用另一个应用时,音乐播放器通常会继续播放音乐。Media3 库提供了一系列接口,让您可以支持后台播放。
使用 MediaSessionService
要启用后台播放,您应将 Player
和 MediaSession
包含在单独的 Service 中。这使得设备即使在您的应用不在前台时也能继续提供媒体服务。

MediaSessionService
允许媒体会话独立于应用的 activity 运行在 Service 中托管播放器时,您应使用 MediaSessionService
。为此,创建一个扩展 MediaSessionService
的类,并在其中创建您的媒体会话。
使用 MediaSessionService
使得 Google 助理、系统媒体控件、外围设备上的媒体按钮或 Wear OS 等配套设备等外部客户端能够发现您的服务、连接到它并控制播放,所有这些都无需访问您的应用 UI activity。实际上,可以有多个客户端应用同时连接到同一个 MediaSessionService
,每个应用都有自己的 MediaController
。
实现服务生命周期
您需要实现服务的两个生命周期方法:
onCreate()
在第一个控制器即将连接且服务被实例化和启动时调用。这是构建Player
和MediaSession
的最佳位置。onDestroy()
在服务停止时调用。所有资源,包括播放器和会话,都需要被释放。
您可以选择性地覆盖 onTaskRemoved(Intent)
以自定义当用户从最近任务中关闭应用时发生的情况。默认情况下,如果播放正在进行,服务将继续运行,否则将停止。
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() } // 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(); } // 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?) { pauseAllPlayersAndStopSelf() }
Java
@Override public void onTaskRemoved(@Nullable Intent rootIntent) { pauseAllPlayersAndStopSelf(); }
对于任何其他 onTaskRemoved
的手动实现,您可以使用 isPlaybackOngoing()
来检查播放是否被视为正在进行且前台服务已启动。
提供媒体会话访问
覆盖 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
和 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
控制播放
在包含播放器 UI 的 Activity 或 Fragment 中,您可以使用 MediaController
在 UI 和媒体会话之间建立链接。您的 UI 使用媒体控制器将命令从 UI 发送到会话中的播放器。有关创建和使用 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 系统 UI 的恢复功能请求播放恢复时,会调用 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, metadata (like title // and artwork) of the current item 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, 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 准备播放器并在回调完成后开始播放之前,使用这些参数配置播放器的理想位置。
此方法在设备重新启动后,启动时调用,用于创建 Android 系统 UI 恢复通知。为了获得更丰富的通知,建议用本地可用的值填充当前项的 MediaMetadata
字段,例如 title
和 artworkData
或 artworkUri
,因为网络访问可能尚不可用。您还可以将 MediaConstants.EXTRAS_KEY_COMPLETION_STATUS
和 MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE
添加到 MediaMetadata.extras
中,以指示恢复播放位置。
高级控制器配置和向后兼容性
一个常见的场景是在应用 UI 中使用 MediaController
来控制播放和显示播放列表。同时,会话会暴露给外部客户端,例如移动设备或电视上的 Android 媒体控件和助理、手表的 Wear OS 以及汽车中的 Android Auto。Media3 会话演示应用就是实现这种场景的一个示例。
这些外部客户端可能使用旧版 AndroidX 库的 MediaControllerCompat
或 Android 平台的 android.media.session.MediaController
等 API。Media3 与旧版库完全向后兼容,并提供与 Android 平台 API 的互操作性。
使用媒体通知控制器
重要的是要理解,这些旧版和平台控制器共享相同的状态,并且可见性无法通过控制器自定义(例如可用的 PlaybackState.getActions()
和 PlaybackState.getCustomActions()
)。您可以使用媒体通知控制器来配置平台媒体会话中的状态,以与这些旧版和平台控制器兼容。
例如,应用可以提供 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 button preferences and commands to configure the platform session. return AcceptedResultBuilder(session) .setMediaButtonPreferences( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward)) ) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default button preferences 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 button preferences and commands to configure the platform session. return new AcceptedResultBuilder(session) .setMediaButtonPreferences( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward))) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands with default button preferences 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 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 for all other controllers. return new AcceptedResultBuilder(session).build(); }
会话演示应用有一个汽车模块,它演示了对 Automotive OS 的支持,这需要单独的 APK。