原文地址:https://developer.android.google.cn/guide/topics/media-apps/audio-app/building-an-audio-app.html
音频应用程序的首选架构是客户端/服务器设计。播放器及其media session 在MediaBrowserService中实现,UI、media controller 与MediaBrowser都在Android Activity中。
MediaBrowserService提供两个主要功能:
- 当您使用MediaBrowserService时,MediaBrowser的其他组件和应用程序可以发现您的服务,创建自己的media controller,连接到media session并控制播放器。这正是Android Wear和Android Auto Applications获取到您的媒体应用程序的权限的原因。
- 它还提供可选的浏览API。应用程序不必使用此功能。浏览API允许客户端查询服务并构建其内容层次结构的表示,其可以表示播放列表,媒体库或其他类型的集合。
注意:像media session和media controller一样, media browser services and media browsers 的推荐实现是MediaBrowserServiceCompat和MediaBrowserCompat,它们在media-compat支持库中定义。它们替换了在API 21中引入的MediaBrowserService和MediaBrowser类的早期版本。为简洁起见,术语“MediaBrowserService”和“MediaBrowser”分别指MediaBrowserServiceCompat和MediaBrowserCompat的实例。
1.构建Media Browser Service
如何创建包含media session的media browser service,管理客户端连接,并在播放音频时成为前台服务。
您的应用程序必须在其清单中声明具有意图过滤器的MediaBrowserService。 您可以选择自己的服务名称; 在下面的例子中,它是“MediaPlaybackService”。
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
注意:MediaBrowserService的推荐实现是MediaBrowserServiceCompat。 这在media-compat支持库中定义。 在此页面中,“MediaBrowserService”是指MediaBrowserServiceCompat的一个实例.
初始化media session
当服务收到onCreate()生命周期回调方法时,它应该执行以下步骤:
- 创建并初始化media session
- 设置media session回调
- 设置media session token
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()仍然必须返回一个非空的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);
}
}
在某些情况下,您可能需要实施一个白名单/黑名单方案来控制连接。有关白名单的示例,请参阅通用 Universal Android Music Player 的 PackageValidator类。
注意:您应该考虑根据客户端进行查询的类型提供不同的内容层次结构。特别是,Android Auto限制用户如何与音频应用程序交互。有关详细信息,请参阅为 自动播放音频。您可以在连接时查看clientPackageName以确定客户端类型,并根据客户端(或rootHints(如果有))返回不同的BrowserRoot。
与onLoadChildren()通信内容
客户端连接后,可以通过重复调用MediaBrowserCompat.subscribe()来构建UI的本地表示,从而遍历内容层次。 subscribe()方法将回调的onLoadChildren()发送到service,该service返回MediaBrowser.MediaItem对象的列表。每个MediaItem都有唯一的ID字符串,它是不透明的令牌。当客户端打开子菜单或播放项目时,会传递ID。service负责将ID与相应的菜单节点或内容项相关联。
onLoadChildren()的简单实现可能如下所示:
@Override
public void onLoadChildren(final String parentMediaId,
final Result<List<MediaItem>> result) {
// Browsing not allowed
if (TextUtils.equals(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时,使用Uri代替调用setIconUri()。
有关如何实现onLoadChildren()的示例,请参阅MediaBrowserService和Universal Android Music Player示例应用程序。
media browser service 生命周期
Android service的行为取决于是启动还是绑定到一个或多个客户端。创建service后,可以启动,绑定或同时用两种。在所有这些状态中,它都是功能齐全的,可以执行其设计的工作。不同的是service的将存在多长时间。绑定的service在所有绑定的客户端解除绑定之前不会被销毁。启动的service可以被显式地停止和销毁(假设它不再绑定到任何客户端)。当在另一个Activity中运行的MediaBrowser连接到MediaBrowserService时,它将活动绑定到服务,使服务受限(但未启动)。此默认行为内置于MediaBrowserServiceCompat类中。
当service所有客户端解除绑定时,只有绑定(而不是启动)的servcie会被销毁。如果您的UI Activity在此时断开连接,则该service将被销毁。如果您还没有播放任何音乐,这不是问题。但是,播放开始时,即使切换应用程序,用户也可能希望继续聆听。当使用其他应用时,并不能销毁播放器。
因此,您需要确保通过调用startService()启动service。必须明确停止启动的服务,无论是否绑定。这样可以确保即使控制UI活动解除绑定,您的播放器也会继续执行。
要停止启动的服务,请调用Context.stopService()或stopSelf()。系统尽快停止并破坏服务。但是,如果一个或多个客户端仍然绑定到服务,则停止服务的呼叫将被延迟,直到其所有客户端解除绑定。
MediaBrowserService的生命周期由创建的方式控制,客户端的数量以及从媒体会话回调接收的呼叫。总结:
- service是在响应媒体按钮启动时创建的,或者当活动绑定到媒体按钮(通过其MediaBrowser连接后)时创建。
- media session 的onPlay()回调应该包括调用startService()的代码。即使在绑定到它的所有UI MediaBrowser活动解除绑定的情况下,也可以确保服务启动并继续运行。
- onStop()回调应该调用stopSelf()。如果服务已启动,则会停止该服务。此外,如果没有Activity绑定service,service将被销毁。否则,service将保持绑定,直到其所有Activity解除绑定。 (如果在service销毁之前接收到后续的startService()调用,则取消挂起的停止。)
使用具有前台service的MediaStyle通知
当一个服务正在播放时,它应该在前台运行。这使系统知道服务正在执行有用的功能,如果系统内存不足,则不应该被杀死。前台服务必须显示通知,以便用户知道该通知,并且可以选择性地控制它。 onPlay()回调应该把服务放在前台。 (请注意,这里“前台”的含义有些特殊。虽然Android将Service置为前台视为进程管理的目的,但是对于用户而言,播放器正在后台播放,而其他应用程序在前台。)
当service在前台运行时,它必须显示一个通知,理想情况下是使用一个或多个传输控件。该通知还应包括会话元数据中的有用信息。
播放器开始播放时构建并显示通知。最好的方法是在MediaSessionCompat.Callback.onPlay()方法中。
以下示例使用NotificationCompat.MediaStyle,它专为媒体应用程序而设计。它显示了如何构建显示元数据和传输控件的通知。 getController()方便方法可以直接从media session创建 media controller 。
// 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);
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(this,
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(this, R.color.primaryDark))
// Add a pause button
.addAction(new NotificationCompat.Action(
R.drawable.pause, getString(R.string.pause),
MediaButtonReceiver.buildMediaButtonPendingIntent(this,
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(this,
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()将通知与您的会话相关联。这允许第三方应用和随播设备访问和控制会话。
- 使用setShowActionsInCompactView()添加最多3个操作以显示在通知的标准大小的contentView中。 (这里指定了暂停按钮。)
- 在Android 5.0(API级别21)及更高版本中,一旦服务不再在前台运行,您可以滑动通知来停止播放器。您不能在早期版本中执行此操作。要允许用户在Android 5.0(API级别21)之前删除通知并停止播放,您可以通过调用setShowCancelButton(true)和setCancelButtonIntent()在通知的右上角添加取消按钮。
2.构建Media Browser客户端
如何创建包含UI和media controller的media browser客户端活动,并与media browser service进行连接和通信。
要完成客户端/服务器设计,您必须构建一个包含您的UI代码的活动组件,一个关联的MediaController和一个MediaBrowser。
MediaBrowser执行两个重要功能:它连接到MediaBrowserService,连接后,将为您的UI创建MediaController。
注意:MediaBrowser的推荐实现是MediaBrowserCompat,它是在Media-Compat支持库中定义的。在这个页面中,术语“MediaBrowser”是指MediaBrowserCompat的一个实例。
连接到MediaBrowserService
创建客户端活动后,它将连接到MediaBrowserService。有一个握手和舞蹈涉及。修改活动的lifecyle回调如下:
onCreate()构造一个MediaBrowserCompat。以您定义的MediaBrowserService和MediaBrowserCompat.ConnectionCallback的名称传递。
onStart()连接到MediaBrowserService。 MediaBrowserCompat.ConnectionCallback的魔法来源于此。如果连接成功,onConnect()回调将创建媒体控制器,将其链接到媒体会话,将UI控件链接到MediaController,并注册控制器以接收来自媒体会议。
当您的活动停止时,onStop()将断开MediaBrowser并取消注册MediaController.Callback。
public class MediaPlayerActivity extends AppCompatActivity {
private MediaBrowserCompat mMediaBrowser;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
// Create MediaBrowserServiceCompat
mMediaBrowser = new MediaBrowserCompat(this,
new ComponentName(this, MediaPlaybackService.class),
mConnectionCallbacks,
null); // optional Bundle
}
@Override
public void onStart() {
super.onStart();
mMediaBrowser.connect();
}
@Override
public void onStop() {
super.onStop();
// (see "stay in sync with the MediaSession")
if (MediaControllerCompat.getMediaController(MediaPlayerActivity.this) != null) {
MediaControllerCompat.getMediaController(MediaPlayerActivity.this).unregisterCallback(controllerCallback);
}
mMediaBrowser.disconnect();
}
}
自定义MediaBrowserCompat.ConnectionCallback
当您的活动构建MediaBrowserCompat时,必须创建一个ConnectionCallback的实例。 修改其onConnected()方法以从MediaBrowserService检索媒体会话令牌,并使用令牌创建MediaControllerCompat。
使用MediaControllerCompat.setMediaController()方便的方法来保存控制器的链接。 这样可以处理媒体按钮。 它还允许您在构建传输控件时调用MediaControllerCompat.getMediaController()来检索控制器。
以下代码示例显示如何修改onConnected()方法。
private final MediaBrowserCompat.ConnectionCallback mConnectionCallbacks =
new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
// Get the token for the MediaSession
MediaSessionCompat.Token token = mMediaBrowser.getSessionToken();
// Create a MediaControllerCompat
MediaControllerCompat mediaController =
new MediaControllerCompat(MediaPlayerActivity.this, // Context
token);
// Save the controller
MediaControllerCompat.setMediaController(MediaPlayerActivity.this, mediaController);
// Finish building the UI
buildTransportControls();
}
@Override
public void onConnectionSuspended() {
// The Service has crashed. Disable transport controls until it automatically reconnects
}
@Override
public void onConnectionFailed() {
// The Service has refused our connection
}
};
将您的UI连接到媒体控制器
在上面的ConnectionCallback示例代码中,包含一个调用buildTransportControls()来显示你的UI。 您需要为控制播放器的UI元素设置onClickListeners。 为每个选择适当的MediaControllerCompat.TransportControls方法。
你的代码看起来像这样,每个按钮都有一个onClickListener:
void buildTransportControls()
{
// Grab the view for the play/pause button
mPlayPause = (ImageView) findViewById(R.id.play_pause);
// Attach a listener to the button
mPlayPause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Since this is a play/pause button, you'll need to test the current state
// and choose the action accordingly
int pbState = MediaControllerCompat.getMediaController(MediaPlayerActivity.this).getPlaybackState().getState();
if (pbState == PlaybackStateCompat.STATE_PLAYING) {
MediaControllerCompat.getMediaController(MediaPlayerActivity.this).getTransportControls().pause();
} else {
MediaControllerCompat.getMediaController(MediaPlayerActivity.this).getTransportControls().play();
}
});
MediaControllerCompat mediaController = MediaControllerCompat.getMediaController(MediaPlayerActivity.this);
// Display the initial state
MediaMetadataCompat metadata = mediaController.getMetadata();
PlaybackStateCompat pbState = mediaController.getPlaybackState();
// Register a Callback to stay in sync
mediaController.registerCallback(controllerCallback);
}
TransportControls方法将回调发送到您的服务的媒体会话。 确保您已经为每个控件定义了相应的MediaSessionCompat.Callback方法。
与媒体会话保持同步
UI应显示媒体会话的当前状态,如其PlaybackState和Metadata所描述的。 创建传输控件时,您可以获取会话的当前状态,将其显示在UI中,并根据状态及其可用操作启用和禁用传输控件。
要在每次其状态或元数据更改时从媒体会话接收回调,请使用以下两种方法定义MediaControllerCompat.Callback:
MediaControllerCompat.Callback controllerCallback =
new MediaControllerCompat.Callback() {
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {}
};
在构建传输控件时注册回调(请参阅buildTransportControls()方法),并在活动停止时取消注册(在活动的onStop()生命周期方法中)。
3.Media Session回调
介绍 media session回调方法如何管理 media session, media browser service以及其他应用程序组件,如通知和广播接收器。
您的媒体会话回调在多个API中调用方法来控制播放器,管理音频焦点,以及与媒体会话和媒体浏览器服务进行通信。
onPlay() | onPause() | onStop() | |
Audio Focus | requestFocus() passing in your OnAudioFocusChangeListener .Always call requestFocus() first, proceed only if focus is granted. | abandonAudioFocus() | |
Service | startService() | stopSelf() | |
Media Session | setActive(true) - Update metadata and state | - Update metadata and state | setActive(false) - Update metadata and state |
Player Implementation | Start the player | Pause the player | Stop the player |
Becoming Noisy | Register your BroadcastReceiver | Unregister your BroadcastReceiver | |
Notifications | startForeground(notification) | stopForeground(false) | stopForeground(false) |
onPlay() | onPause() | onStop() | |
Audio Focus | requestFocus() passing in your OnAudioFocusChangeListener .Always call requestFocus() first, proceed only if focus is granted. | abandonAudioFocus() | |
Service | startService() | stopSelf() | |
Media Session | setActive(true) - Update metadata and state | - Update metadata and state | setActive(false) - Update metadata and state |
Player Implementation | Start the player | Pause the player | Stop the player |
Becoming Noisy | Register your BroadcastReceiver | Unregister your BroadcastReceiver | |
Notifications | startForeground(notification) | stopForeground(false) | stopForeground(false) |
以下是回调的示例框架:
private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
// Defined elsewhere...
private AudioManager.OnAudioFocusChangeListener afChangeListener;
private BecomingNoisyReceiver myNoisyAudioStreamReceiver = new BecomingNoisyReceiver();
private MediaStyleNotification myPlayerNotification;
private MediaSessionCompat mediaSession;
private MediaBrowserService service;
private SomeKindOfPlayer player;
MediaSessionCompat.Callback callback = new
MediaSessionCompat.Callback() {
@Override
public void onPlay() {
AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
// Request audio focus for playback, this registers the afChangeListener
int result = am.requestAudioFocus(afChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// Start the service
service.start();
// Set the session active (and update metadata and state)
mediaSession.setActive(true);
// start the player (custom call)
player.start();
// Register BECOME_NOISY BroadcastReceiver
registerReceiver(myNoisyAudioStreamReceiver, intentFilter);
// Put the service in the foreground, post notification
service.startForeground(myPlayerNotification);
}
}
@Override
public void onStop() {
AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
// Abandon audio focus
am.abandonAudioFocus(afChangeListener);
unregisterReceiver(myNoisyAudioStreamReceiver);
// Start the service
service.stop(self);
// Set the session inactive (and update metadata and state)
mediaSession.setActive(false);
// stop the player (custom call)
player.stop();
// Take the service out of the foreground
service.stopForeground(false);
}
@Override
public void onPause() {
AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
// Update metadata and state
// pause the player (custom call)
player.pause();
// unregister BECOME_NOISY BroadcastReceiver
unregisterReceiver(myNoisyAudioStreamReceiver, intentFilter);
// Take the service out of the foreground, retain the notification
service.stopForeground(false);
}
}
注意:如果您使用必要的回调创建您的MediaSession,使用Google Assistant的人可以使用语音命令控制您的应用。 Google Assistant文档中说明了要求。