android mediasession 音频服务框架

https://mp.weixin.qq.com/s/AngSHW1qUBsl_acFhlWGDA?

概要

下面列出构建一个典型的音乐 App 需要注意的点,然后我们将一一展开。

  • 使用 MediaSession 进行 UI 与后台播放状态的同步

  • 音乐的后台播放

  • AudioFocus 的处理

  • ACTION_AUDIO_BECOMING_NOISY

  • MediaButton 事件处理

  • 通知栏更新

  • Android 旧版本的兼容

  • WifiLock && WakeLock

  • 播放进度更新

MediaSession 框架简介

MediaSession 是 Android 5.0 推出的媒体播放框架,负责 UI 和后台播放之间的状态同步,支持了绝大部分音频播放的可能会遇到的操作,而且支持自定义操作。主要由 MediaSession (受控端) 和 MediaController (控制端) 构成:

android mediasession 音频服务框架_第1张图片
image
  1. MediaSession
  • MediaSession.Token: 用于保持与 MediaController 的正常配对

  • MediaSession.Callback:用于监听 MediaController 的各种播放指令

  1. MediaController
  • MediaController.Callback:用于监听播放状态/信息更新

  • MediaController.TransportControls:用于向 MediaSession 发送各种播放指令

基本流程就是,UI 通过使用 MediaController.TransportControls 发送播放相关的控制指令(play, pause, stop 等等),MediaSession.Callback 在接收到相关指令后,对 Player 进行对应的操作,然后状态更新通过 MediaSession 同步给 MediaController.Callback, 最后更新 UI。

后台播放

显然,后台播放需要通过 Service 实现,而且后台 Service 需要继承自 MediaSession 框架中的 MediaBrowserService,同时需要在 AndroidManifest.xml 中加入 IntentFiliter。


  
 

典型的初始化工作如下:

public class MediaPlaybackService extends MediaBrowserService {

  @Override
  public void onCreate() {
    super.onCreate();
    // 1\. 初始化 MediaSession
    // 2\. 设置 MedisSessionCallback
    // 3\. 开启 MediaButton 和 TransportControls 的支持
    // 4\. 初始化 PlaybackState
    // 5\. 关联 SessionToken
  }

  @Override
  public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
    // 对每个访问端做一些访问权限判断等
  }

  @Override
  public void onLoadChildren(final String parentMediaId,
    final Result> result) {
    // 根据访问权限返回播放列表相关信息
  }
}

而关联 UI 和 Service 的工作主要封装在了 MediaBrowser 里。MediaBrowser 主要的工作就是使用 Bind 的启动方式启动 Service,然后将 MediaSession 的 Token 回调,用于创建 MediaController。

public class MediaPlayerActivity extends AppCompatActivity {
  private MediaBrowserCompat mMediaBrowser;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mMediaBrowser = new MediaBrowserCompat(this, 
        new ComponentName(this, MediaPlaybackService.class),
        mConnectionCallbacks, null); 
  }

  @Override
  public void onStart() {
    super.onStart();
    mMediaBrowser.connect();
  }

  @Override
  public void onStop() {
    super.onStop();
    if (MediaControllerCompat.getMediaController(MediaPlayerActivity.this) != null) {
      MediaControllerCompat.getMediaController(MediaPlayerActivity.this).unregisterCallback(controllerCallback);
    }
    mMediaBrowser.disconnect();
  }
}

需要注意的是,为了保证音频在后台能够正常的持续播放和停止,需要结合 Service 的 start 和 bind 两种启动方式。每当 UI 需要获取后台播放状态时,都需要 bind 后台 Service 以保证存活,即 MediaBrowser 就是用这种方法启动 Service。而为了让 UI 都 unbind 了之后,后台的音乐不会因此停止播放,需要在音乐播放(onPlay())时,通过 start 的方式启动 Service,而音乐停止播放(onStop())后,因为不在需要 Service 的存活了, 所以可以调用 Service.stopSelf() 来停止 Service。因为只有两个启动方式都不存在的情况下,Service 才会立即销毁。生命周期下图所示,图中 counter 表示 bind 的数量:

image.gif

另外,如果有多进程的需要的话,直接将 Service 放到单独的进程就好了,因为 MediaController 和 MediaSession 的交互底层是通过 Binder 通信的,已经很好的支持了进程间通信。

MediaButton

当音乐处于后台播放的情况下,需要支持 MediaButton 的按键事件(KEYCODE_MEDIA_XXX),比如说线控耳机上的播放/暂停按钮。

首先需要注册 MediaButtonReceiver 到 AndroidManifest.xml。


   
     
   
 

有两个原因:

  1. 如果 MediaSession 是 Active 状态。 在Android 5.0 之后, Android MediaButton 的按键事件会直接分发到 MediaSession 的 onMediaButtonEvent(...),默认情况下回调用 MediaSession.Callback 的对应回调,如果有特别需要可以重写这个方法。Android 5.0之前,则需要监听事件广播,典型的代码是在 Service 中加入如下代码。因为,一方面 MediaButtonReceiver 在接收到事件后会直接 startService,另一方面,MediaButtonReceiver.handleIntent(...) 自动映射事件到 MediaSession.Callback 中的对应回调。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
   MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
   return super.onStartCommand(intent, flags, startId);
 }
  1. 如果 Android 识别出这个 MediaSession 是上一个变为 Inactive 状态的,那么 MediaButtonReceiver 就可以接受到这个广播,然后可以重新启动 MediaSession。(主要注意的是这个行为在只有在 Android 5.0 之后可以关闭)

BecomingNoisy

在耳机插入时播放音乐,然后将耳机突然拔出,可能造成音乐外放的尴尬情况。这种情况下系统会发送 AudioManager.ACTION_AUDIO_BECOMING_NOISY 广播,我们只需要在音乐播放的时候注册一个 BroadcastReceiver,在收到广播时暂停播放就好了。

private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private BecomingNoisyReceiver myNoisyAudioStreamReceiver = new BecomingNoisyReceiver();

MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {
  @Override
  public void onPlay() {
    registerReceiver(myNoisyAudioStreamReceiver, intentFilter);
  }

  @Override
  public void onStop() {
    unregisterReceiver(myNoisyAudioStreamReceiver);
  }
}

WifiLock && WakeLock

通常情况下,当用户没有使用设备一段时间,Android 系统出于省电的考虑,可能会关闭Wifi 网络和 CPU。

为了让后台播放的音乐不会因为网络关闭而获取不到音频数据等,需要获取 WifiLock,使音乐播放过程中,Wifi 保持唤醒。

WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {
  @Override
  public void onPlay() {
    wifiLock.acquire();
  }

  @Override
  public void onPause() {
    wifiLock.release(); // 或者在 onStop 中, 根据是否对网络有具体需求而定, 权衡需求和省电
  }

  @Override
  public void onStop() {
    // wifiLock.release(); 
  }
}

可以通过设置 MediaPlayer 的 WakeMode 来保持 CPU 的状态。

MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

其内部实现是通过 PowerManager 来实现的。WakeLock 会在播放结束之后(播放完成,播放错误,和重置/释放播放器)释放。所以如果播放器不是 MediaPlayer 则需要自行处理。

PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(mode|PowerManager.ON_AFTER_RELEASE, MediaPlayer.class.getName());
mWakeLock.setReferenceCounted(false);
mWakeLock.acquire();

Notification

为了减少 Service 在后台运行的时候,被系统回收的情况,通常需要将 Service 设置为 foreground。当 Service 被设置为 foreground 的时候,系统会显示一个不可移除的 Notification(引导用户强制停止 App) 提醒用户有个正在运行的高优先级后台 Service。这种体验显然是不好的,不过好在这个 Notification 是可以自定义的, 有两种方式可以实现。

  1. 使用 Android 5.0 上引入的 MediaStyle,支持 Expanded Notification, 收缩是最多显示3个按钮,展开是最多显示5个按钮。
android mediasession 音频服务框架_第2张图片
image
android mediasession 音频服务框架_第3张图片
image

典型的初始化工作如下

MediaControllerCompat controller = mediaSession.getController();
MediaMetadataCompat mediaMetadata = controller.getMetadata();
MediaDescriptionCompat description = mediaMetadata.getDescription();

NotificationCompat.Builder builder = new NotificationCompat.Builder(context);

builder.setContentTitle(description.getTitle())
    .setContentText(description.getSubtitle())
    .setSubText(description.getDescription())
    .setLargeIcon(description.getIconBitmap())
    // 点击通知栏后直接跳转至播放页面
    .setContentIntent(controller.getSessionActivity()) 
    // 左滑掉通知栏后自动停止
    .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_STOP)) 
    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 在锁屏状态下显示控制按钮
    .addAction(new NotificationCompat.Action(R.drawable.pause, getString(R.string.pause),
          MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)))
    .setStyle(new NotificationCompat.MediaStyle() // 使用系统提供的样式
      .setMediaSession(mediaSession.getSessionToken())
      .setShowActionsInCompactView(0) // 根据索引配置在简介的模式下应该显示哪些操作按钮
      .setShowCancelButton(true) // 显示关闭按钮,需要特别注意的是,5.0 之前显示在右上角,5.0 之后将不再显示
      .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this,
          PlaybackStateCompat.ACTION_STOP));

startForeground(id, builder.build());
  1. 使用自己自定义的布局, MediaStyle 能做到的都能做到,只是需要自己处理的事情比较多。由于国内存在众多 ROM,且都定制了自己的 UI, MediaStyle 在不同的 ROM 上可能表现不一致,为了统一的体验和 避免不必要的 Bug 产生,我们英语流利说中采用的就是自定义自己的 Notification。
image.gif

AudioFocus

Android 的 AudioFocus 用于处理多个音频同时播放时,如何协调它们之间的竞争关系的机制。当音乐开始播放的时候,需要和其他的音频播放竞争 AudioFocus, 当获取到 AudioFocus 时,才开始播放。 1. streamType 基本上就是用 AudioManager.STREAM_MUSIC, 代表播放音乐。

  1. durationHint 申请 AudioFocus 时,告诉被竞争的播放器竞争者需要播放的时间。

  2. AUDIOFOCUS_GAIN / AUDIOFOCUS_LOSS: 没有具体时长,长期持有,比如音乐播放。当音乐要开始播放的时候就需要申请 AUDIOFOCUS_GAIN。而当其他的音乐需要播放的时候,我们会收到 AUDIOFOCUS_LOSS 的状态改变通知,处理方法可以是直接停止,或者暂停,然后过一段时间停止。

  3. AUDIOFOCUS_GAIN_TRANSIENT / AUDIOFOCUS_LOSS_TRANSIENT:一段很短时长,比如 流利说中单词读音的播放。当我们收到 AUDIOFOCUS_LOSS_TRANSIENT 时,通常的做法是暂停音乐的播放。

  4. AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK / AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:非常短的时长,比如音效。当我们收到 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 的状态改变通知是,通常做法是降低音量,所以音乐和其他音效都会在同一个 streamType 中播放出来。

注意:这个机制不是强制的,是一个规范,只有大家都遵守的时候,机制才能运作的很好,所以为了平台统一的体验,还是需要处理的。

播放进度更新

由于 MediaSession 很好的解耦了播放器,如果使用 MediaPlayer,UI 是拿不到 MediaPlayer 对象的,所以无法直接通过 MediaPlayer 的 getCurrentPosition() 拿到进度的。而 MediaSession 框架都是通过 PlaybackState 来同步状态的, PlaybackState.Builder 有个如下的方法:

setState(int state, long position, float playbackSpeed, long updateTime)

当 UI 拿到上述参数后,可以通过如下代码计算得出当前的播放进度:

//(当前开机时间(无法更改的) – 上次更新状态的时间)* 播放速度 + 上次更新状态时的播放进度
long currentPosition = ((SystemClock.elapsedRealtime() – playbackState.getLastPositionUpdateTime() ) * playbackState. getPlaybackSpeed() ) + playbackState.getPosition();

兼容

  1. 关于两者之间的兼容性,他们是交叉兼容的,比如 MediaBrowser 和 MediaBrowserServiceCompat 搭配,或者 MediaBrowserCompat 和 MediaBrowserService 搭配,都是可以正常工作的。不会遇到类似 PreferenceFragmentCompat 没有加到 support 库前, PreferenceFragment 与 SupportFragmentManager 不兼容的尴尬情况。

  2. 虽然 MediaSession 框架在 Api 21 的时候引入,但是 support library 在 23.2.0 的时候添加 MediaSession 的兼容。支持到 v4 版本。而且 support library 在 24.2.0 的时候拆分了 media 模块 为单独的包。

com.android.support:support-media-compat:24.2.0

总结

android mediasession 音频服务框架_第4张图片
image

参考

  1. https://github.com/googlesamples/android-UniversalMusicPlayer

  2. https://developer.android.com/guide/topics/media-apps/index.html

你可能感兴趣的:(android mediasession 音频服务框架)