您的应用必须在其清单文件中声明带有 intent 过滤器的 MediaBrowserService
。您可以选择您自己的服务名称;在下面的示例中,它是“MediaPlaybackService”。
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
注意: MediaBrowserService
的推荐实现是 MediaBrowserServiceCompat
,它在 media-compat 支持库 中定义。在本页中,“MediaBrowserService”一词指的是 MediaBrowserServiceCompat
的一个实例。
初始化媒体会话
当服务接收到 onCreate()
生命周期回调方法时,它应该执行以下步骤
- 创建并 初始化媒体会话
- 设置媒体会话回调
- 设置媒体会话令牌
下面的 onCreate()
代码演示了这些步骤
Kotlin
private const val MY_MEDIA_ROOT_ID = "media_root_id" private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id" class MediaPlaybackService : MediaBrowserServiceCompat() { private var mediaSession: MediaSessionCompat? = null private lateinit var stateBuilder: PlaybackStateCompat.Builder override fun onCreate() { super.onCreate() // Create a MediaSessionCompat mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply { // Enable callbacks from MediaButtons and TransportControls setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ) // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player stateBuilder = PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY_PAUSE ) setPlaybackState(stateBuilder.build()) // MySessionCallback() has methods that handle callbacks from a media controller setCallback(MySessionCallback()) // Set the session's token so that client activities can communicate with it. setSessionToken(sessionToken) } } }
Java
public class MediaPlaybackService extends MediaBrowserServiceCompat { private static final String MY_MEDIA_ROOT_ID = "media_root_id"; private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"; private MediaSessionCompat mediaSession; private PlaybackStateCompat.Builder stateBuilder; @Override public void onCreate() { super.onCreate(); // Create a MediaSessionCompat mediaSession = new MediaSessionCompat(context, LOG_TAG); // Enable callbacks from MediaButtons and TransportControls mediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player stateBuilder = new PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE); mediaSession.setPlaybackState(stateBuilder.build()); // MySessionCallback() has methods that handle callbacks from a media controller mediaSession.setCallback(new MySessionCallback()); // Set the session's token so that client activities can communicate with it. setSessionToken(mediaSession.getSessionToken()); } }
管理客户端连接
MediaBrowserService
有两种处理客户端连接的方法:onGetRoot()
控制对服务的访问,而 onLoadChildren()
使客户端能够构建和显示 MediaBrowserService
内容层次结构的菜单。
使用 onGetRoot()
控制客户端连接
onGetRoot()
方法返回内容层次结构的根节点。如果该方法返回 null,则连接将被拒绝。
要允许客户端连接到您的服务并浏览其媒体内容,onGetRoot() 必须返回一个非 null 的 BrowserRoot,它是一个表示您的内容层次结构的根 ID。
要允许客户端连接到您的 MediaSession 而无需浏览,onGetRoot() 仍然必须返回一个非 null 的 BrowserRoot,但根 ID 应该表示一个空的内容层次结构。
onGetRoot()
的典型实现可能如下所示
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): MediaBrowserServiceCompat.BrowserRoot { // (Optional) Control the level of access for the specified package name. // You'll need to write your own logic to do this. return if (allowBrowsing(clientPackageName, clientUid)) { // Returns a root ID that clients can use with onLoadChildren() to retrieve // the content hierarchy. MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null) } else { // Clients can connect, but this BrowserRoot is an empty hierarchy // so onLoadChildren returns nothing. This disables the ability to browse for content. MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null) } }
Java
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { // (Optional) Control the level of access for the specified package name. // You'll need to write your own logic to do this. if (allowBrowsing(clientPackageName, clientUid)) { // Returns a root ID that clients can use with onLoadChildren() to retrieve // the content hierarchy. return new BrowserRoot(MY_MEDIA_ROOT_ID, null); } else { // Clients can connect, but this BrowserRoot is an empty hierarchy // so onLoadChildren returns nothing. This disables the ability to browse for content. return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null); } }
在某些情况下,您可能希望控制谁可以连接到您的 MediaBrowserService
。一种方法是使用访问控制列表 (ACL),该列表指定允许哪些连接,或者列出哪些连接应该被禁止。有关如何实现允许特定连接的 ACL 的示例,请参阅 Universal Android Music Player 示例应用中的 PackageValidator 类。
您应该考虑根据进行查询的客户端类型提供不同的内容层次结构。特别是,Android Auto 限制了用户与音频应用交互的方式。有关更多信息,请参阅 为 Auto 播放音频。您可以在连接时查看 clientPackageName
以确定客户端类型,并根据客户端返回不同的 BrowserRoot
(或任何 rootHints
)。
使用 onLoadChildren()
传递内容
客户端连接后,它可以通过重复调用 MediaBrowserCompat.subscribe()
来遍历内容层次结构,从而构建 UI 的本地表示。 subscribe()
方法将回调 onLoadChildren()
发送到服务,该服务返回 MediaBrowser.MediaItem
对象列表。
每个 MediaItem 都有一个唯一的 ID 字符串,这是一个不透明的令牌。当客户端想要打开子菜单或播放项目时,它会传递 ID。您的服务负责将 ID 与相应的菜单节点或内容项目关联。
onLoadChildren()
的简单实现可能如下所示
Kotlin
override fun onLoadChildren( parentMediaId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> ) { // Browsing not allowed if (MY_EMPTY_MEDIA_ROOT_ID == parentMediaId) { result.sendResult(null) return } // Assume for example that the music catalog is already loaded/cached. val mediaItems = emptyList<MediaBrowserCompat.MediaItem>() // Check if this is the root menu: if (MY_MEDIA_ROOT_ID == parentMediaId) { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... } else { // Examine the passed parentMediaId to see which submenu we're at, // and put the children of that menu in the mediaItems list... } result.sendResult(mediaItems) }
Java
@Override public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { // Browsing not allowed if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) { result.sendResult(null); return; } // Assume for example that the music catalog is already loaded/cached. List<MediaItem> mediaItems = new ArrayList<>(); // Check if this is the root menu: if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... } else { // Examine the passed parentMediaId to see which submenu we're at, // and put the children of that menu in the mediaItems list... } result.sendResult(mediaItems); }
注意: MediaBrowserService
传递的 MediaItem
对象不应包含图标位图。而是使用 Uri
,方法是在构建每个项目的 MediaDescription
时调用 setIconUri()
。
有关如何实现 onLoadChildren()
的示例,请参阅 Universal Android Music Player 示例应用。
媒体浏览器服务的生命周期
Android 服务 的行为取决于它是启动还是绑定到一个或多个客户端。服务创建后,可以启动、绑定或同时启动和绑定。在所有这些状态下,它都完全可以运行,并且可以执行其设计要执行的工作。不同之处在于服务存在的时间长短。绑定服务直到所有绑定客户端取消绑定后才会被销毁。启动的服务可以显式停止和销毁(假设它不再绑定到任何客户端)。
当在另一个活动中运行的 MediaBrowser
连接到 MediaBrowserService
时,它会将活动绑定到服务,使服务处于绑定状态(但未启动)。此默认行为内置在 MediaBrowserServiceCompat
类中。
仅绑定(未启动)的服务在其所有客户端取消绑定时将被销毁。如果您的 UI 活动此时断开连接,则服务将被销毁。如果您尚未播放任何音乐,则不会出现问题。但是,当播放开始时,用户可能希望即使在切换应用后也能继续收听。您不希望在取消绑定 UI 以使用另一个应用时销毁播放器。
因此,您需要确保在服务开始播放时通过调用 startService()
来启动服务。启动的服务必须显式停止,无论它是否绑定。这确保您的播放器即使在控制 UI 活动取消绑定后也能继续运行。
要停止启动的服务,请调用 Context.stopService()
或 stopSelf()
。系统将尽快停止并销毁服务。但是,如果一个或多个客户端仍然绑定到服务,则停止服务的调用将延迟到所有客户端取消绑定为止。
MediaBrowserService
的生命周期由其创建方式、绑定到它的客户端数量以及它从媒体会话回调接收到的调用来控制。总结如下:
- 响应媒体按钮或活动绑定到它(通过其
MediaBrowser
连接后)时创建服务。 - 媒体会话
onPlay()
回调应包含调用startService()
的代码。这确保服务启动并继续运行,即使所有绑定到它的 UIMediaBrowser
活动都取消绑定。 onStop()
回调应调用stopSelf()
。如果服务已启动,则这会停止它。此外,如果没有任何活动绑定到它,则服务将被销毁。否则,服务将保持绑定状态,直到所有活动取消绑定。(如果在服务销毁之前接收到后续的startService()
调用,则挂起的停止将被取消。)
下图演示了如何管理服务的生命周期。变量 counter 跟踪绑定客户端的数量
将 MediaStyle 通知与前台服务一起使用
服务播放时,应在前台运行。这可以让系统知道服务正在执行有用的功能,如果系统内存不足,则不应将其终止。前台服务必须显示通知,以便用户知道它并可以选择控制它。onPlay()
回调应将服务置于前台。(请注意,这是“前台”的特殊含义。虽然 Android 将服务视为出于进程管理目的而处于前台,但对于用户而言,播放器在后台播放,而某些其他应用在屏幕上“前台”可见。)
服务在前台运行时,必须显示一个 通知,理想情况下带有一个或多个传输控件。通知还应包含会话元数据中的有用信息。
播放器开始播放时构建并显示通知。最好的做法是在 MediaSessionCompat.Callback.onPlay()
方法内部执行此操作。
以下示例使用 NotificationCompat.MediaStyle
(专为媒体应用而设计)。它演示了如何构建显示元数据和传输控件的通知。便捷方法 getController()
允许您直接从媒体会话创建媒体控制器。
Kotlin
// Given a media session and its context (usually the component containing the session) // Create a NotificationCompat.Builder // Get the session's metadata val controller = mediaSession.controller val mediaMetadata = controller.metadata val description = mediaMetadata.description val builder = NotificationCompat.Builder(context, channelId).apply { // Add the metadata for the currently playing track setContentTitle(description.title) setContentText(description.subtitle) setSubText(description.description) setLargeIcon(description.iconBitmap) // Enable launching the player by clicking the notification setContentIntent(controller.sessionActivity) // Stop the service when the notification is swiped away setDeleteIntent( MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_STOP ) ) // Make the transport controls visible on the lockscreen setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Add an app icon and set its accent color // Be careful about the color setSmallIcon(R.drawable.notification_icon) color = ContextCompat.getColor(context, R.color.primaryDark) // Add a pause button addAction( NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_PLAY_PAUSE ) ) ) // Take advantage of MediaStyle features setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.sessionToken) .setShowActionsInCompactView(0) // Add a cancel button .setShowCancelButton(true) .setCancelButtonIntent( MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_STOP ) ) ) } // Display the notification and place the service in the foreground startForeground(id, builder.build())
Java
// Given a media session and its context (usually the component containing the session) // Create a NotificationCompat.Builder // Get the session's metadata MediaControllerCompat controller = mediaSession.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); MediaDescriptionCompat description = mediaMetadata.getDescription(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); builder // Add the metadata for the currently playing track .setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setSubText(description.getDescription()) .setLargeIcon(description.getIconBitmap()) // Enable launching the player by clicking the notification .setContentIntent(controller.getSessionActivity()) // Stop the service when the notification is swiped away .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP)) // Make the transport controls visible on the lockscreen .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Add an app icon and set its accent color // Be careful about the color .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(context, R.color.primaryDark)) // Add a pause button .addAction(new NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE))) // Take advantage of MediaStyle features .setStyle(new MediaStyle() .setMediaSession(mediaSession.getSessionToken()) .setShowActionsInCompactView(0) // Add a cancel button .setShowCancelButton(true) .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))); // Display the notification and place the service in the foreground startForeground(id, builder.build());
使用 MediaStyle 通知时,请注意以下 NotificationCompat 设置的行为
- 使用
setContentIntent()
时,单击通知后,服务会自动启动,这是一个方便的功能。 - 在锁屏等“不受信任”的情况下,通知内容的默认可见性为
VISIBILITY_PRIVATE
。您可能希望在锁屏上看到传输控件,因此VISIBILITY_PUBLIC
是最佳选择。 - 设置背景颜色时要小心。在 Android 5.0 或更高版本中的普通通知中,颜色仅应用于小型应用图标的背景。但在 Android 7.0 之前的 MediaStyle 通知中,颜色用于整个通知背景。测试您的背景颜色。要柔和一些,避免使用极其明亮或荧光的颜色。
仅当您使用 NotificationCompat.MediaStyle 时,这些设置才可用
- 使用
setMediaSession()
将通知与您的会话关联。这允许第三方应用和配套设备访问和控制会话。 - 使用
setShowActionsInCompactView()
在通知的标准尺寸内容视图中最多添加 3 个操作。(此处指定暂停按钮。) - 在 Android 5.0(API 级别 21)及更高版本中,一旦服务不再在前台运行,您可以滑动取消通知以停止播放器。在早期版本中,您无法执行此操作。为了允许用户在 Android 5.0(API 级别 21)之前移除通知并停止播放,您可以通过调用
setShowCancelButton(true)
和setCancelButtonIntent()
在通知的右上角添加一个取消按钮。
添加暂停和取消按钮时,您需要一个 PendingIntent 来附加到播放操作。方法MediaButtonReceiver.buildMediaButtonPendingIntent()
可以将 PlaybackState 操作转换为 PendingIntent。