您现在的位置: 主页 > 嵌入式操作系统 > Android > Android:构建一个典型的音乐App
本文所属标签:
为本文创立个标签吧:

Android:构建一个典型的音乐App

来源:网络整理 网络用户发布,如有版权联系网管删除 2018-07-26 


前言

在过去的英语流利说 5.0 版本中,更新了“每日听力”功能。其实从技术的角度来看,和实现一个音乐 App 没有多大差异。为了保证用户有个良好的收听体验,有一些注意事项是我们需要处理的。

概要

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

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

  • 音乐的后台播放

  • AudioFocus 的处理

  • ACTION_AUDIO_BECOMING_NOISY

  • MediaButton 事件处理

  • 通知栏更新

  • Android 旧版本的兼容

  • WifiLock && WakeLock

  • 播放进度更新

MediaSession 框架简介

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

  1. MediaSession

    • MediaSession.Token: 用于保持与 MediaController 的正常配对

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

  2. 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。

<intent-filter>
  <action android:name="android.media.browse.MediaBrowserService" />
 </intent-filter>

典型的初始化工作如下:

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<List<MediaItem>> 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 的数量:

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

MediaButton

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

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

<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
   <intent-filter>
     <action android:name="android.intent.action.MEDIA_BUTTON" />
   </intent-filter>
 </receiver>

有两个原因:

  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个按钮。

典型的初始化工作如下

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。

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

总结

参考

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

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





              查看评论 回复



嵌入式交流网主页 > 嵌入式操作系统 > Android > Android:构建一个典型的音乐App
 需要 音乐 播放

"Android:构建一个典型的音乐App"的相关文章

网站地图

围观()