构建媒体浏览器服务
您的应用必须MediaBrowserService在其清单中声明带有intent-filter。您可以选择自己的服务名称; 在以下示例中,它是“MediaPlaybackService”。
注意:推荐的实现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> 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 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开发技术!