Android媒体应用(四)--构建音频应用程序

来源:互联网 发布:win7 软件 乱码 编辑:程序博客网 时间:2024/06/06 03:13

原文地址: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提供两个主要功能:

  1. 当您使用MediaBrowserService时,MediaBrowser的其他组件和应用程序可以发现您的服务,创建自己的media controller,连接到media session并控制播放器。这正是Android Wear和Android Auto Applications获取到您的媒体应用程序的权限的原因。
  2. 它还提供可选的浏览API。应用程序不必使用此功能。浏览API允许客户端查询服务并构建其内容层次结构的表示,其可以表示播放列表,媒体库或其他类型的集合。

注意:像media sessionmedia controller一样, media browser services and media browsers 的推荐实现是MediaBrowserServiceCompat和MediaBrowserCompat,它们在media-compat支持库中定义。它们替换了在API 21中引入的MediaBrowserService和MediaBrowser类的早期版本。为简洁起见,术语“MediaBrowserService”和“MediaBrowser”分别指MediaBrowserServiceCompat和MediaBrowserCompat的实例。

1.构建Media Browser Service

如何创建包含media sessionmedia 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()生命周期回调方法时,它应该执行以下步骤:

  1. 创建并初始化media session
  2. 设置media session回调
  3. 设置media session token
下面的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()仍然必须返回一个非空的BrowserRoot,但是根ID应该表示一个空的内容层次结构。


onGetRoot()的典型实现可能如下所示:

@Overridepublic 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()的简单实现可能如下所示:

@Overridepublic 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的生命周期由创建的方式控制,客户端的数量以及从媒体会话回调接收的呼叫。总结:

  1. service是在响应媒体按钮启动时创建的,或者当活动绑定到媒体按钮(通过其MediaBrowser连接后)时创建。
  2. media session 的onPlay()回调应该包括调用startService()的代码。即使在绑定到它的所有UI MediaBrowser活动解除绑定的情况下,也可以确保服务启动并继续运行。
  3. 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 metadataMediaControllerCompat 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 foregroundstartForeground(id, builder.build());

使用MediaStyle通知时,请注意这些NotificationCompat设置的行为:
  1. 当您使用setContentIntent()时,您的服务将在点击通知时自动启动,方便的功能。
  2. 在类似锁屏的“不受信任”情况下,通知内容的默认可见性为VISIBILITY_PRIVATE。你可能想看到锁屏上的运输控制,所以要使用VISIBILITY_PUBLIC。
  3. 设置背景颜色时要小心。在Android 5.0或更高版本的普通通知中,颜色仅适用于小应用程序图标的背景。但对于Android 7.0之前的MediaStyle通知,该颜色用于整个通知背景。测试你的背景颜色。在眼睛上温柔,避免非常明亮或荧光的颜色。
这些设置仅在您使用NotificationCompat.MediaStyle时可用:
  1. 使用setMediaSession()将通知与您的会话相关联。这允许第三方应用和随播设备访问和控制会话。
  2. 使用setShowActionsInCompactView()添加最多3个操作以显示在通知的标准大小的contentView中。 (这里指定了暂停按钮。)
  3. 在Android 5.0(API级别21)及更高版本中,一旦服务不再在前台运行,您可以滑动通知来停止播放器。您不能在早期版本中执行此操作。要允许用户在Android 5.0(API级别21)之前删除通知并停止播放,您可以通过调用setShowCancelButton(true)和setCancelButtonIntent()在通知的右上角添加取消按钮。
当您添加暂停和取消按钮时,您需要一个PendingIntent才能附加到播放动作。 MediaButtonReceiver.buildMediaButtonPendingIntent()方法将PlaybackState操作转换为PendingIntent。

2.构建Media Browser客户端

如何创建包含UI和media controllermedia 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 FocusrequestFocus() passing in your OnAudioFocusChangeListener.
Always call requestFocus() first, proceed only if focus is granted. abandonAudioFocus()ServicestartService() stopSelf()Media SessionsetActive(true) 
- Update metadata and state- Update metadata and statesetActive(false) 
- Update metadata and statePlayer ImplementationStart the playerPause the playerStop the playerBecoming NoisyRegister your BroadcastReceiverUnregister your BroadcastReceiver NotificationsstartForeground(notification)stopForeground(false)stopForeground(false)onPlay()onPause()onStop()Audio FocusrequestFocus() passing in your OnAudioFocusChangeListener.
Always call requestFocus() first, proceed only if focus is granted. abandonAudioFocus()ServicestartService() stopSelf()Media SessionsetActive(true) 
- Update metadata and state- Update metadata and statesetActive(false) 
- Update metadata and statePlayer ImplementationStart the playerPause the playerStop the playerBecoming NoisyRegister your BroadcastReceiverUnregister your BroadcastReceiver NotificationsstartForeground(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 = newMediaSessionCompat.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文档中说明了要求。

阅读全文
1 0