构建媒体浏览器服务您的应用必须 MediaBrowserService 在其清单中声明带有 intent-filter。您可以选择自己的服务名称; 在以下示例中,它是“MediaPlaybackService”。
<service android:name=”.MediaPlaybackService”>
<intent-filter>
<action android:name=”android.media.browse.MediaBrowserService” />
</intent-filter>
</service>
注意:推荐的实现 MediaBrowserService 是 MediaBrowserServiceCompat。这是在 media-compat 支持库中定义的。在整个页面中,术语“MediaBrowserService”指的是 of 的一个实例 MediaBrowserServiceCompat。
初始化媒体会话当服务收到 onCreate() 生命周期回调方法时,它应该执行以下步骤:
创建并初始化媒体会话设置媒体会话回调设置媒体会话令牌 onCreate() 下面的代码演示了以下步骤:
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 mMediaSession;
private PlaybackStateCompat.Builder mStateBuilder;
@Override
public void onCreate() {
super.onCreate();
// Create a MediaSessionCompat
mMediaSession = new MediaSessionCompat(context, LOG_TAG);
// Enable callbacks from MediaButtons and TransportControls
mMediaSession.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
mStateBuilder = new PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PLAY_PAUSE);
mMediaSession.setPlaybackState(mStateBuilder.build());
// MySessionCallback() has methods that handle callbacks from a media controller
mMediaSession.setCallback(new MySessionCallback());
// Set the session’s token so that client activities can communicate with it.
setSessionToken(mMediaSession.getSessionToken());
}
}
管理客户连接 MediaBrowserService 有两种处理客户端连接的方法:onGetRoot() 控制对服务的访问,并 onLoadChildren() 为客户端提供构建和显示 MediaBrowserService 内容层次结构菜单的能力。
使用控制客户端连接 onGetRoot()
该 onGetRoot() 方法返回内容层次结构的根节点。如果方法返回 null,则拒绝连接。
要允许客户端连接到您的服务并浏览其媒体内容,onGetRoot()必须返回一个非空的 BrowserRoot,它是一个表示您的内容层次结构的根 ID。
要允许客户端在不浏览的情况下连接到 MediaSession,onGetRoot()仍必须返回非 null 的 BrowserRoot,但根 ID 应表示空的内容层次结构。
典型的实现 onGetRoot() 可能如下所示:
@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 hierachy
// so onLoadChildren returns nothing. This disables the ability to browse for content.
return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null);
}
}
在某些情况下,您可能希望实施白 / 黑名单方案来控制连接。有关白名单的示例,请参阅通用 Android 音乐播放器示例应用程序中的 PackageValidator 类。
注意:您应该考虑提供不同的内容层次结构,具体取决于进行查询的客户端类型。特别是,Android Auto 会限制用户与音频应用的互动方式。有关更多信息,请参阅为自动播放音频。您可以查看 clientPackageName 连接时间以确定客户端类型,并 BrowserRoot 根据客户端(或者 rootHints 如果有)返回不同的客户端类型。
与内容沟通内容 onLoadChildren() 在客户端连接之后,它可以通过重复调用 MediaBrowserCompat.subscribe() 来构建 UI 的本地表示来遍历内容层次结构。该 subscribe() 方法将回调发送 onLoadChildren() 到服务,该服务返回 MediaBrowser.MediaItem 对象列表。
每个 MediaItem 都有一个唯一的 ID 字符串,它是一个不透明的标记。当客户想要打开子菜单或播放项目时,它会传递 ID。您的服务负责将 ID 与相应的菜单节点或内容项相关联。
一个简单的实现 onLoadChildren() 可能如下所示:
@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);
}
注意:MediaItem MediaBrowserService 传递的对象不应包含图标位图。使用 Uri 的,而不是调用 setIconUri() 在生成 MediaDescription 的每个项目
有关如何实施的示例 onLoadChildren(),请参阅 MediaBrowserService 和 Universal Android Music Player 示例应用程序。
媒体浏览器服务生命周期 Android 服务的行为取决于它是启动还是绑定到一个或多个客户端。创建服务后,可以启动,绑定或同时启用它。在所有这些状态中,它功能齐全,可以执行其设计的工作。不同之处在于服务存在多长时间。绑定的服务在其所有绑定的客户端解除绑定之前不会被销毁。可以显式停止和销毁已启动的服务(假设它不再绑定到任何客户端)。
当 MediaBrowser 另一个活动中的运行连接到 a 时 MediaBrowserService,它会将活动绑定到服务,从而使服务绑定(但不启动)。此默认行为内置于 MediaBrowserServiceCompat 类中。
只有绑定(并且未启动)的服务在其所有客户端解除绑定时销毁。如果此时 UI 活动断开连接,则服务将被销毁。如果您还没有播放任何音乐,这不是问题。但是,当播放开始时,用户可能希望即使在切换应用后也能继续收听。当您取消绑定 UI 以使用其他应用程序时,您不希望销毁播放器。
因此,您需要确保在通过调用开始播放服务时启动该服务 startService()。无论是否绑定,必须明确停止已启动的服务。这可确保即使控制 UI 活动解除绑定,您的播放器也会继续执行。
要停止已启动的服务,请致电 Context.stopService() 或 stopSelf()。系统会尽快停止并销毁服务。但是,如果一个或多个客户端仍然绑定到该服务,则停止该服务的调用将延迟,直到其所有客户端解除绑定。
它的生命周期 MediaBrowserService 由创建方式,绑定到它的客户端数量以及从媒体会话回调接收的调用控制。总结一下:
该服务在响应媒体按钮或活动绑定到它(通过其连接后 MediaBrowser)启动时创建。媒体会话 onPlay() 回调应包括调用的代码 startService()。这可确保服务启动并继续运行,即使 MediaBrowser 绑定到它的所有 UI 活动都解除绑定。该 onStop() 回调应该调用 stopSelf()。如果服务已启动,则会停止该服务。此外,如果没有绑定的活动,服务将被销毁。否则,服务将保持绑定,直到其所有活动解除绑定。(如果 startService() 在销毁服务之前收到后续呼叫,则取消挂起停止。)以下流程图演示了如何管理服务的生命周期。变量计数器跟踪绑定客户端的数量:
将 MediaStyle 通知与前台服务一起使用当服务正在播放时,它应该在前台运行。这使系统知道服务正在执行有用的功能,如果系统内存不足,则不应该被杀死。前台服务必须显示通知,以便用户知道它并可以选择控制它。该 onPlay() 回调应该把服务的前景。(请注意,这是“前景”的特殊含义。虽然 Android 在前台考虑服务以进行流程管理,但是对于用户,播放器正在后台播放,而其他应用程序在“前景”中可见屏幕。)
当服务在前台运行时,它必须显示通知,理想情况下是一个或多个传输控件。通知还应包括会话元数据中的有用信息。
在播放器开始播放时构建并显示通知。这样做的最佳位置是 MediaSessionCompat.Callback.onPlay() 方法内部。
以下示例使用 NotificationCompat.MediaStyle 专为媒体应用设计的。它显示了如何构建显示元数据和传输控件的通知。便捷方法 getController() 允许您直接从媒体会话创建媒体控制器。
// 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 通知,颜色用于整个通知背景。测试你的背景颜色。温柔的眼睛,避免极其明亮或荧光的颜色。
用于 setMediaSession() 将通知与您的会话相关联。这允许第三方应用和配套设备访问和控制会话。
用于 setMediaSession() 将通知与您的会话相关联。这允许第三方应用和配套设备访问和控制会话。
用于 setShowActionsInCompactView() 在通知的标准大小的 contentView 中添加最多 3 个操作。(此处指定了暂停按钮。)
在 Android 5.0(API 级别 21)及更高版本中,一旦服务不再在前台运行,您可以滑动通知以停止播放器。您不能在早期版本中执行此操作。要允许用户在 Android 5.0(API 级别 21)之前删除通知并停止播放,您可以通过调用 setShowCancelButton(true) 和在通知的右上角添加取消按钮 setCancelButtonIntent()。
添加暂停和取消按钮时,您需要 PendingIntent 附加到播放操作。该方法 MediaButtonReceiver.buildMediaButtonPendingIntent() 执行将 PlaybackState 操作转换为 PendingIntent 的工作。
总结写的一般,欢迎留言、私信指出问题与不足之处!如回复不及时可加入 Android 技术交流群:150923287 一起学习探讨 Android 开发技术!