您的应用必须在清单中通过 intent-filter 声明 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 限制了用户与音频应用的交互方式。有关更多信息,请参阅为汽车播放音频。您可以在连接时查看 clientPackageName
以确定客户端类型,并根据客户端(或 rootHints
,如果有)返回不同的 BrowserRoot
。
使用 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
对象不应包含图标位图。您应在构建每个项的 MediaDescription
时,通过调用 setIconUri()
来改用 Uri
。
有关如何实现 onLoadChildren()
的示例,请参阅 Universal Android Music Player 示例应用。
媒体浏览器服务生命周期
Android 服务的行为取决于它是已启动还是绑定到一个或多个客户端。服务创建后,可以启动、绑定或两者兼而有之。在所有这些状态下,它都功能齐全,可以执行其设计的工作。区别在于服务将存在多长时间。绑定服务在其所有绑定客户端解绑之前不会被销毁。已启动的服务可以明确停止和销毁(假设它不再绑定到任何客户端)。
当在另一个 activity 中运行的 MediaBrowser
连接到 MediaBrowserService
时,它将 activity 绑定到服务,使服务处于绑定状态(但未启动)。此默认行为内置于 MediaBrowserServiceCompat
类中。
仅绑定(未启动)的服务会在其所有客户端解绑时被销毁。如果您的 UI activity 在此时断开连接,服务就会被销毁。如果您尚未播放任何音乐,这不是问题。但是,当播放开始时,用户可能会期望即使切换应用后也能继续收听。您不希望在解绑 UI 以与其他应用协作时销毁播放器。
因此,您需要确保服务在开始播放时通过调用 startService()
而启动。已启动的服务必须明确停止,无论它是否绑定。这可确保您的播放器即使在控制 UI activity 解绑后也能继续运行。
要停止已启动的服务,请调用 Context.stopService()
或 stopSelf()
。系统会尽快停止并销毁服务。但是,如果一个或多个客户端仍绑定到服务,则停止服务的调用将延迟到所有客户端解绑为止。
MediaBrowserService
的生命周期受其创建方式、绑定到它的客户端数量以及它从媒体会话回调接收的调用控制。总结如下
- 当服务响应媒体按钮而启动时,或者当 activity 绑定到它时(通过其
MediaBrowser
连接后),服务就会被创建。 - 媒体会话
onPlay()
回调应包含调用startService()
的代码。这可确保服务启动并持续运行,即使所有绑定到它的 UIMediaBrowser
activity 解绑后也是如此。 onStop()
回调应调用stopSelf()
。如果服务已启动,此操作会将其停止。此外,如果没有 activity 绑定到服务,服务就会被销毁。否则,服务会一直绑定,直到其所有 activity 解绑。(如果在服务销毁之前收到后续的startService()
调用,则待停止操作将被取消。)
以下流程图演示了服务的生命周期管理方式。变量计数器跟踪绑定客户端的数量
将 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 个操作以显示在通知的标准大小 contentView 中。(此处指定了暂停按钮。) - 在 Android 5.0(API 级别 21)及更高版本中,一旦服务不再在前台运行,您可以通过滑动通知来停止播放器。在早期版本中无法执行此操作。为了让用户在 Android 5.0(API 级别 21)之前移除通知并停止播放,您可以通过调用
setShowCancelButton(true)
和setCancelButtonIntent()
在通知的右上角添加取消按钮。
当您添加暂停和取消按钮时,您需要一个 PendingIntent 以附加到播放操作。方法 MediaButtonReceiver.buildMediaButtonPendingIntent()
负责将 PlaybackState 操作转换为 PendingIntent。