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

原文地址: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()的典型实现可能如下所示:

@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()的示例,请参阅MediaBrowserServiceUniversal 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 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设置的行为:
  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 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文档中说明了要求。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值