版权声明:本文为博主原创文章,未经博主允许不得转载
源码: AnliaLee/BauzMusic
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论
前言
最近一直在忙着学习和研究音乐播放器,发现介绍 MediaSession 框架的资料非常少,更多的是一些源码和开源库,这对于初学者来说不是很友好,可能看着看着就绕晕了,遂博主决定动手写点这方面的博客分享给大家
参考资料
googlesamples/android-UniversalMusicPlayer
Media Apps Overview (有前辈翻译后的版本 Android媒体应用(一) )
MediaSession框架简介
我们先来看看如何设计一款音乐播放App的架构,传统的做法是这样的:
- 注册一个 Service ,用于异步获取音乐库数据、音乐控制等,在 Service 中我们可能还需要自定义一些 状态值 和 回调接口 用于流程控制
- 通过广播(其他方式如 接口 、 Messenger 都可以)实现 Activity 和 Service 之间的通信,使得用户可以通过界面上的组件控制音乐的播放、暂停、拖动进度条等操作
如果我们的音乐播放器还需要支持 通知栏 快捷控制音乐播放的功能,那么又得新增一套广播和相应的接口去响应 通知栏按钮 的事件
如果还需要支持多端(电视、手表、耳机等)控制同一个播放器,那么整个系统架构可能会变得非常复杂,我们要花费大量的时间和精力去设计、优化代码的结构。那么有什么方法可以节省这些工作,提高我们的效率,然后还可以优雅地实现上述这些功能呢?
Google 在 Android 5.0 中加入了 MediaSession 框架(在 support-v4 中同样提供了相应的兼容包,相关的类以 Compat 结尾, Api 基本相同),专门用来解决媒体播放时界面和 Service 通讯的问题,意在规范上述这些功能的流程。使用这个框架我们可以减少一些流程复杂的开发工作,例如使用各种广播来控制播放器,而且其代码可读性、结构耦合度方面都控制得非常好,因此推荐大家尝试下这个框架。下面我们就开始介绍 MediaSession 框架的核心成员和使用流程
MediaSession框架的使用
常用成员类概述
MediaSession 框架中有四个常用的成员类,它们是整个流程控制的核心
-
MediaBrowser
媒体浏览器 ,用来 连接MediaBrowserService 和 订阅数据 ,通过它的回调接口我们可以获取和 Service 的连接状态以及获取在 Service 中异步获取的音乐库数据。媒体浏览器一般创建于 客户端 (可以理解为各个终端 负责控制音乐播放的界面 )中 -
MediaBrowserService
浏览器服务 ,提供 onGetRoot (控制客户端媒体浏览器的连接请求,通过返回值决定是否允许该客户端连接服务)和 onLoadChildren (媒体浏览器向 Service 发送数据订阅时调用,一般在这执行 异步获取数据 的操作,最后将数据发送至媒体浏览器的回调接口中)这两个抽象方法
同时 MediaBrowserService 还作为承载 媒体播放器 (如MediaPlayer、ExoPlayer等)和 MediaSession 的容器 -
MediaSession
媒体会话 ,即 受控端 ,通过设置 MediaSessionCompat.Callback 回调来接收媒体控制器 MediaController 发送的指令,当收到指令时会触发 Callback 中各个指令对应的 回调方法 (回调方法中会执行 播放器 相应的操作,如播放、暂停等)。 Session 一般在 Service.onCreate 方法中创建,最后需调用 setSessionToken 方法设置用于 和控制器配对的令牌 并通知 浏览器 连接 服务 成功 -
MediaController
媒体控制器 ,在客户端中开发者不仅可以使用控制器向 Service 中的 受控端 发送指令,还可以通过设置 MediaControllerCompat.Callback 回调方法接收 受控端的状态 ,从而根据相应的状态 刷新界面UI 。 MediaController 的创建需要 受控端的配对令牌 ,因此需在 浏览器成功连接服务 的回调执行创建的操作
通过上述的简介中我们不难看出这四个成员之间有着非常明确的分工和作用范围,使得整个代码结构变得清晰易读。可以通过下面这张图来简单归纳它们之间的关系
除此之外, MediaSession 框架中还有一些同样重要的类需要拿出来讲,例如封装了各种 播放状态 的 PlaybackState ,和Map相似通过 键值对 保存 媒体信息 的 MediaMetadata ,以及用于 MediaBrowser 和 MediaBrowserService 之间进行数据交互的 MediaItem 等等,下面我们通过实现一个简单的demo来具体分析这套框架的工作流程
使用MediaSession框架构建简单的音乐播放器
例如我们的demo是这样的(见下图),只提供简单的播放暂停操作,音乐数据源从raw资源文件夹中获取
按照工作流程,我们就从获取音乐库数据开始吧。首先界面上方添加一个 RecyclerView 来展示获取的音乐列表,我们在 DemoActivity 中完成一些 RecyclerView 的初始化操作
public class DemoActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private List<MediaBrowserCompat.MediaItem> list;
private DemoAdapter demoAdapter;
private LinearLayoutManager layoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
list = new ArrayList<>();
layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
demoAdapter = new DemoAdapter(this,list);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(demoAdapter);
}
}
注意 List 元素的类型为 MediaBrowserCompat.MediaItem ,因为 MediaBrowser 从服务中获取的每一首音乐都会封装成 MediaItem 对象。接下来我们创建 MediaBrowser ,并执行连接服务端和订阅数据的操作
public class DemoActivity extends AppCompatActivity {
...
private MediaBrowserCompat mBrowser;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
mBrowser = new MediaBrowserCompat(
this,
new ComponentName(this, MusicService.class),//绑定浏览器服务
BrowserConnectionCallback,//设置连接回调
null
);
}
@Override
protected void onStart() {
super.onStart();
//Browser发送连接请求
mBrowser.connect();
}
@Override
protected void onStop() {
super.onStop();
mBrowser.disconnect();
}
/**
* 连接状态的回调接口,连接成功时会调用onConnected()方法
*/
private MediaBrowserCompat.ConnectionCallback BrowserConnectionCallback =
new MediaBrowserCompat.ConnectionCallback(){
@Override
public void onConnected() {
Log.e(TAG,"onConnected------");
//必须在确保连接成功的前提下执行订阅的操作
if (mBrowser.isConnected()) {
//mediaId即为MediaBrowserService.onGetRoot的返回值
//若Service允许客户端连接,则返回结果不为null,其值为数据内容层次结构的根ID
//若拒绝连接,则返回null
String mediaId = mBrowser.getRoot();
//Browser通过订阅的方式向Service请求数据,发起订阅请求需要两个参数,其一为mediaId
//而如果该mediaId已经被其他Browser实例订阅,则需要在订阅之前取消mediaId的订阅者
//虽然订阅一个 已被订阅的mediaId 时会取代原Browser的订阅回调,但却无法触发onChildrenLoaded回调
//ps:虽然基本的概念是这样的,但是Google在官方demo中有这么一段注释...
// This is temporary: A bug is being fixed that will make subscribe
// consistently call onChildrenLoaded initially, no matter if it is replacing an existing
// subscriber or not. Currently this only happens if the mediaID has no previous
// subscriber or if the media content changes on the service side, so we need to
// unsubscribe first.
//大概的意思就是现在这里还有BUG,即只要发送订阅请求就会触发onChildrenLoaded回调
//所以无论怎样我们发起订阅请求之前都需要先取消订阅
mBrowser.unsubscribe(mediaId);
//之前说到订阅的方法还需要一个参数,即设置订阅回调SubscriptionCallback
//当Service获取数据后会将数据发送回来,此时会触发SubscriptionCallback.onChildrenLoaded回调
mBrowser.subscribe(mediaId, BrowserSubscriptionCallback);
}
}
@Override
public void onConnectionFailed() {
Log.e(TAG,"连接失败!");
}
};
/**
* 向媒体浏览器服务(MediaBrowserService)发起数据订阅请求的回调接口
*/
private final MediaBrowserCompat.SubscriptionCallback BrowserSubscriptionCallback =
new MediaBrowserCompat.SubscriptionCallback(){
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List<MediaBrowserCompat.MediaItem> children) {
Log.e(TAG,"onChildrenLoaded------");
//children 即为Service发送回来的媒体数据集合
for (MediaBrowserCompat.MediaItem item:children){
Log.e(TAG,item.getDescription().getTitle().toString());
list.add(item);
}
//在onChildrenLoaded可以执行刷新列表UI的操作
demoAdapter.notifyDataSetChanged();
}
};
}
通过上述的代码和注释大家应该清楚 MediaBrowser 从 连接服务 到向其 订阅数据 的流程了,简单总结一下就是
connect → onConnected → subscribe → onChildrenLoaded
那么 Service 端那边在这段流程中又做了什么呢?首先我们得继承 MediaBrowserService (这里使用了 support-v4 包的类)创建 MusicService 类。 MediaBrowserService 继承自 Service ,所以记得在 AndroidManifest.xml 中完成配置
<service
android:name=".demo.MusicService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"