开发者通常希望当应用不在前台时播放媒体。例如,当用户锁定设备或正在使用其他应用时,音乐播放器通常会继续播放音乐。Media3 库提供了一系列接口,可让您支持后台播放。
使用 MediaSessionService
如需启用后台播放功能,您应在单独的 Service 中包含 Player
和 MediaSession
。这样,即使您的应用未在前台运行,设备也可以继续传送媒体。
在 Service 中托管播放器时,您应使用 MediaSessionService
。为此,请创建一个扩展 MediaSessionService
的类,并在其中创建媒体会话。
使用 MediaSessionService
可让外部客户端(例如 Google 助理)、系统媒体控件或配套设备(例如 Wear OS)发现您的服务、连接到该服务并控制播放,而所有这些都无需访问应用的界面 activity。实际上,可以将多个客户端应用同时连接到同一 MediaSessionService
,每个应用都有自己的 MediaController
。
实现服务生命周期
您需要为服务实现三种生命周期方法:
- 在第一个控制器即将连接且服务已实例化并启动时,系统会调用
onCreate()
。它是构建Player
和MediaSession
的最佳位置。 - 当用户从最近任务中关闭应用时,系统会调用
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" />
您还必须使用 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)及更高版本的设备上运行时,您必须定义包含 mediaPlayback
的 foregroundServiceType
。
使用 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 的支持。