最近在和某音乐App合作项目中使用到,特此进行整理分析。
需求是需要获取某音乐播放的状态以及歌曲封面等内容。
整理后基本上分成三部分来写吧。
前言
本文系列主要分成三部分。
1. MediaSession框架使用.
2. MediaSession框架源码分析.
3. MediaSession框架实战App.
很多同学在学习安卓的时候,可能会完成一个音乐播放器的项目,因为音乐播放器项目会贯穿安卓的四大组件。在项目中,大家一定会在服务中使用MediaPlayer去播放音乐,但是界面如何控制服务进行音乐的更换、改变播放进度,大家往往会使用发送广播或者启动service的方式去通知服务,同时,服务可以发送广播通知界面播放进度的变化,或者使用EventBug之类的去通知。
这时候问题来了:
1、有更高效的方式嘛?
在Android 5.0中,谷歌推出了MediaSession框架专门解决媒体播放时界面和服务通讯问题。
要理解MediaSession框架,分别看看Media和Session:首先Media是媒体的意思,也就是说这个框架用于音视频媒体;而Session呢,翻译成中文就是会话的意思。一个会话,肯定是涉及两方或以上;为了保证受控端和控制端不串号(想象一个遥控器可以遥控同一型号的多台电视),就有了SessionToken的概念,相当于我们在连接蓝牙设备时的配对码,这样就保证了不串号。
下面我们参考ANdroid开发者文档来更好的理解MediaSession框架。
本部分内容参考:媒体应用架构概览.
本部分内容参考:使用媒体会话.
本部分内容参考:音频应用概览.
本部分内容参考:构建媒体浏览器服务.
本部分内容参考:构建媒体浏览器客户端.
本部分内容参考:媒体会话回调.
首先,我们先来看一下MediaSession主要类和对象的构成,如下图:
这个图只是用来对整个框架进行梳理和回顾,相信在看完后面的使用方法后就会觉得很简单了。
简单描述一下:
下面是具体的使用:
先来看客户端,我们需要做的是建立连接,并且在连接成功后设置回调。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...
//媒体浏览器
private MediaBrowser mMediaBrowser;
//媒体控制器
private MediaController mMediaController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//新建MediaBrowser,第一个参数是context
//第二个参数是ComponetName,有多种构造方法,指向要连接的服务
//第三个参数是连接结果的回调connectionCallback,第四个参数为Bundle
mMediaBrowser = new MediaBrowser(this,
new ComponentName(this, MediaService.class), connectionCallback, null);
mMediaBrowser.connect();
...
}
//连接结果的回调
private MediaBrowser.ConnectionCallback connectionCallback
= new MediaBrowser.ConnectionCallback() {
//如果服务端接受连接,就会调此方法表示连接成功,否则回调onConnectionFailed();
public void onConnected() {
//获取配对令牌
MediaSession.Token token = mMediaBrowser.getSessionToken();
//通过token,获取MediaController,第一个参数是context,第二个参数为token
mMediaController = new MediaController(getBaseContext(), token);
//mediaController注册回调,callback就是媒体信息改变后,服务给客户端的回调
mMediaController.registerCallback(mMediaCallBack);
}
public void onConnectionSuspended() {
//与服务断开回调(可选)
}
public void onConnectionFailed() {
//连接失败回调(可选)
}
}
//服务对客户端的信息回调。
private MediaController.Callback mMediaCallBack = new MediaController.Callback() {
//回调函数的方法都是选择性重写的,这里不列举全,具体可查询文章末尾的表格
@Override
public void onMetadataChanged(MediaMetadata metadata) {
super.onMetadataChanged(metadata);
//服务端运行mediaSession.setMetadata(mediaMetadata)就会到达此处,以下类推.
//歌曲信息回调,更新。MediaMetadata在文章后面会提及
MediaDescription description = metadata.getDescription();
//获取标题
String title = description.getTitle().toString();
//获取作者
String author = description.getSubtitle().toString();
//获取专辑名
String album = description.getDescription().toString();
//获取总时长
long duratime = mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
...
}
@Override
public void onPlaybackStateChanged(PlaybackState state) {
super.onPlaybackStateChanged(state);
//播放状态信息回调,更新。PlaybackState在文章后面会提及
if (state.getState() == PlaybackState.STATE_PLAYING) {
//正在播放
}
...
//获取当前播放进度
long position = state.getPosition()
....
}
@Override
public void onQueueChanged(List<MediaSession.QueueItem> queue) {
super.onQueueChanged(queue);
//播放列表信息回调,QueueItem在文章后面会提及
....
}
@Override
public void onSessionEvent(String event, Bundle extras) {
super.onSessionEvent(event, extras);
//自定义的事件回调,满足你各种自定义需求
...
}
@Override
public void onExtrasChanged(Bundle extras) {
super.onExtrasChanged(extras);
//额外信息回调,可以承载播放模式等信息
}
.....
}
}
以上代码,做了一个什么事情呢?我们在onCreate()中去连接了一个继承MediaBrowserService的服务。并在连接成功的信息后,我们取得了mMediaController,并且注册了一个回调,用于知晓服务端通知的媒体信息变更。很简单的的开始,在后面的代码中,就可以用mMediaController为所欲为了。
//在需要的地方使用以下代码
//控制媒体服务的一些方法,播放、暂停、上下首、跳转某个时间点...可查看文章末尾表格
mMediaController.getTransportControls().play();
mMediaController.getTransportControls().pause();
mMediaController.getTransportControls().skipToPrevious();
mMediaController.getTransportControls().skipToNext();
mMediaController.getTransportControls().seekTo(...);
....
//主动获取媒体信息的一些操作,获取媒体信息,播放状态...可查看文章末尾表格
MediaMetadata metadata = mMediaController.getMetadata();
PlaybackState playbackState = mMediaController.getPlaybackState();
....
需要留意的坑:非主线程创建MediaBrowser并connect的时候会报错。这是因为连接时底层代码会使用Handler,并且采用Handler handler = new Handler()的创建方式,如此使用必然会报错。解决办法:
Looper.prepare();
mBtMusicBrowser = new MediaBrowser(BaseApplication.getInstance(),
//绑定服务,这里绑定的是系统蓝牙音乐的服务
new ComponentName("com.android.bluetooth",
"com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService"),
mConnectionCallback,//关联连接回调
null);
mBtMusicBrowser.connect();
Looper.loop();
在之前和之后加上Looper.prepare()和Looper.loop()就搞定了,这个可以参考Handler的机制进行理解。
接着看服务端,我们要做的是同意客户端连接,响应客户端的控制命令,并且在信息改变时通知回调给客户端。
public class MediaService extends MediaBrowserService{
...
//媒体会话,受控端
private MediaSession mediaSession;
@Override
public void onCreate() {
super.onCreate();
//初始化,第一个参数为context,第二个参数为String类型tag,这里就设置为类名了
mediaSession = new MediaSession(this, "MediaService");
//设置token
setSessionToken(mediaSession.getSessionToken());
//设置callback,这里的callback就是客户端对服务指令到达处
mediaSession.setCallback(mCallback);
}
//mediaSession设置的callback,也是客户端控制指令所到达处
private MediaSession.Callback mCallback = new MediaSession.Callback() {
//重写的方法都是选择性重写的,不完全列列举,具体可以查询文章末尾表格
@Override
public void onPlay() {
super.onPlay();
//客户端mMediaController.getTransportControls().play()就会调用到这里,以下类推
//处理播放逻辑
...
//处理完成后通知客户端更新,这里就会回调给客户端的MediaController.Callback
mediaSession.setPlaybackState(playbackState);
}
@Override
public void onPause() {
super.onPause();
...
}
@Override
public void onSkipToNext() {
super.onSkipToNext();
//下一首
...
//通知媒体信息改变
mediaSession.setMetadata(mediaMetadata);
}
@Override
public void onCustomAction(String action, Bundle extras) {
super.onCustomAction(action, extras);
//自定义指令发送到的地方
//对应客户端 mMediaController.getTransportControls().sendCustomAction(...)
}
...
}
//自己写的方法,用于改变播放列表
private void changePlayList(){
...
//通知播放队列改变
mediaSession.setQueue(queueItems);
}
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
//MediaBrowserService必须重写的方法,第一个参数为客户端的packageName,第二个参数为Uid
//第三个参数是从客户端传递过来的Bundle。
//通过以上参数来进行判断,若同意连接,则返回BrowserRoot对象,否则返回null;
//构造BrowserRoot的第一个参数为rootId(自定义),第二个参数为Bundle;
return new BrowserRoot("MyMedia", null);
}
@Override
public void onLoadChildren(String parentId,
MediaBrowserService.Result<List<MediaBrowser.MediaItem>> result){
//MediaBrowserService必须重写的方法,用于处理订阅信息,文章后面会提及
...
}
...
}
在服务端里,我们会发现跟客户端的所有操作是一一对应的。
在onCreate()中,我们创建了MediaSession,设置好了token,并设置了MediaSession.CallBack用于接收客户端的各项指令。完成媒体的逻辑后,在合适的地方,我们可以使用形如mediaSession.setMetadata(mediaMetadata)回调给客户端进行媒体信息的更新。
而在BrowserRoot onGetRoot(…)方法中,我们可以通过其中的参数来判断是否准许客户端连接,不允许就直接返回null。
用以上的知识我们可以做一个具有基础功能的多媒体了。不过,新的问题出现了:MediaSession框架中的通信接口是有限的,如果我们的需求不止步于简单的控制怎么办,比如要满足收藏功能,改变歌曲播放的循环模式,或者获取某一个音乐列表,甚至某些独特的需求…
MediaSession框架提供了一些接口,对应关系如下表
MediaController(客户端) | MediaSession.Callback(服务端) | 作用 |
---|---|---|
sendCustomAction(String action, Bundle args) | onCustomAction(String action, Bundle extras) | 发送/接收自定义指令 |
MediaSession(服务端) | MediaController.Callback(客户端) | 作用 |
---|---|---|
sendSessionEvent(String event, Bundle extras) | onSessionEvent(String event, Bundle extras) | 发送/接收自定义指令 |
setExtras(Bundle extras) | onExtrasChanged(Bundle extras) | 通知客户端更新额外信息,播放模式等… |
setQueue(List< QueueItem> queue) | onQueueChanged(List |
通知客户端播放列表改变 |
客户端和服务端可以通过Bundle来进行信息传递,String类型作为自定义命令的标识,达到自定义接口的目的。
此外,我们向服务端主动异步获取或回调特定的媒体列表,可以用订阅的方式来进行。
客户端
//重复订阅会报错,所以先解除订阅
mMediaBrowser.unsubscribe("PARENT_ID_1");
//第一个参数是String类型的parentId(标识)
//第二个参数为订阅的回调MediaBrowser.SubscriptionCallback
mMediaBrowser.subscribe("PARENT_ID_1", mCallback);
...
//订阅信息的回调
private MediaBrowser.SubscriptionCallback mCallback
= new MediaBrowser.SubscriptionCallback() {
@Override
public void onChildrenLoaded(String parentId,
List<MediaBrowser.MediaItem> children) {
super.onChildrenLoaded(parentId, children);
//订阅信息回调,parentID为标识,children为传回的媒体列表
...
}
@Override
public void onChildrenLoaded(String parentId,
List<MediaBrowser.MediaItem> children, Bundle options) {
super.onChildrenLoaded(parentId, children, options);
//订阅消息时添加了Bundle参数,会回调到此方法
//即mMediaBrowser.subscribe("PARENT_ID_1", mCallback,bundle)的回调
...
}
@Override
public void onError(String parentId) {
super.onError(parentId);
//出错..
}
这里需要注意:
因为订阅后,也会到达服务端的onLoadChildren(…),并回调数据到MediaBrowser.SubscriptionCallback,所以可以采用解除订阅,再进行订阅的方式进行主动异步获取操作(订阅后,获得回调信息)。
//这样可以进行异步数据回调
mMediaBrowser.unsubscribe("PARENT_ID_1");
mMediaBrowser.subscribe("PARENT_ID_1", mCallback);
服务端
@Override
public void onLoadChildren(String parentId,
MediaBrowserService.Result<List<MediaBrowser.MediaItem>> result) {
//使用result之前,一定需要detach();
result.detach();
//新建MediaItem数组
ArrayList<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
//根据parentId,获取不同的媒体列表
switch(parentId){
case MEDIA_ID_ROOT:
....
break;
case PARENT_ID_1:
//模拟数据
MediaMetadata metadata = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "101")
.putString(MediaMetadata.METADATA_KEY_TITLE, "一首歌")
.build();
mediaItems.add(new MediaBrowser.MediaItem(metadata.getDescription(),
MediaBrowser.MediaItem.FLAG_PLAYABLE));
break;
...
}
//发送数据
result.sendResult(mediaItems);
}
服务端重写的onLoadChildren(…)用作订阅不同parentId返回不同的媒体数据。此外进行订阅后,服务端可以通过notifyChildrenChanged(String parentId)发送消息来进行回调。
//服务端可以直接使用notifyChildren(..),会到达onLoadChildren(..)中,并回调数据
//如果客户端订阅了对应parentId,那么在MediaBrowser.SubscriptionCallback中就能收到媒体数据
notifyChildrenChanged("parentID_1");
PlaybackState对象承载的信息主要有两个:播放状态、播放进度
//PlaybackState的构建
PlaybackState mState = new PlaybackState.Builder()
//三个参数分别是,状态,位置,播放速度
.setState(state, position, playbackSpeed)
.build();
//PlaybackState的解析
private MediaController.Callback mCallBack = new MediaController.Callback() {
....
@Override
public void onPlaybackStateChanged(PlaybackState playbackState) {
super.onPlaybackStateChanged(state);
//获得进度时长
long position = playbackState.getPosition();
//获得当前状态
switch(playbackState.getState()){
case PlaybackState.STATE_PLAYING:
//正在播放
...
break;
case PlaybackState.STATE_PAUSED:
//暂停
...
break;
case PlaybackState.ACTION_SKIP_TO_NEXT:
//跳到下一首
...
break;
...//还有很多状态标志,按需求添加
}
}
}
构建时,setState(…)有两个方法:
setState(int state, long position, float playbackSpeed)
setState(int state, long position, float playbackSpeed, long updateTime)
上面一个方法其实是调用的下面一个方法,updateTime自动设置为开机时间。
注意:播放进度的获取需要具体逻辑进行计算,客户端和服务端逻辑统一就可以了。 笔者是直接通过position表示播放进度的。
先来MediaMetadata的使用:
//构建,把代码中的字符串替换成歌曲的对应字符串
MediaMetadata metadata = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "id") //id
.putString(MediaMetadata.METADATA_KEY_TITLE, "title")//标题
.putString(MediaMetadata.METADATA_KEY_ARTIST,"artist")//作者
.putString(MediaMetadata.METADATA_KEY_ALBUM,"album")//唱片
.putLong(MediaMetadata.METADATA_KEY_DURATION,"duration")//媒体时长
.build();
//解析,通过MediaDescription获取信息
private MediaController.Callback mCallBack = new MediaController.Callback() {
@Override
public void onMetadataChanged(MediaMetadata metadata) {
super.onMetadataChanged(metadata);
MediaDescription description = mediaMetadata.getDescription();
//获取标题
String title = description.getTitle().toString();
//获取作者
String author = description.getSubtitle().toString();
//获取专辑名
String album = description.getDescription().toString();
//获取总时长
long duratime = mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
}
....
}
MediaSession.QueueItem比MediaMetadata多了一个唯一的id
//构建,传入MediaDescription 和id
MediaDescription description = new MediaDescription.Builder()
.setMediaId(song.mediaId)
.setTitle(song.title)
.setSubtitle(song.subtitle)
.setExtras(bundle)
.build();
QueueItem queueItem = new QueueItem(description, song.queueId);
//MediaMetadata转化为QueueItem
QueueItem queueItem = new QueueItem(mediaMetadata.getDescription(), id);
//解析跟MediaMetadata一样,获取MediaDescription
MediaDescription description = queueItem.getDescription();
//获取标题
String title = description.getTitle().toString();
.....
MediaBrowser.MediaItem跟MediaSession.QueueItem很相似,不同的是唯一的id,变成了flags
//MediaMetadata转化为MediaItem,构造方法第一个都是MediaDescription,第二个是flags
MediaBrowser.MediaItem mediaItem = new MediaBrowser.MediaItem(metadata.getDescription(),
MediaBrowser.MediaItem.FLAG_PLAYABLE);
//解析一样用MediaDescription
MediaDescription description = queueItem.getDescription();
//获取标题
String title = description.getTitle().toString();
...
主要的类与概念
类别 | 类 | 概念 |
---|---|---|
服务端 | android.media.session.MediaSession | 受控端 |
android.media.session.MediaSession.Token | 配对密钥 | |
android.media.session.MediaSession.Callback | 受控端回调,可以接受到控制端的指令 | |
客户端 | android.media.session.MediaController | 控制端 |
android.media.session.MediaController.TransportControls | 控制端的控制器,用于发送指令 | |
android.media.session.MediaController.Callback | 控制端回调,可以接受到受控端的状态 | |
android.media.browse.MediaBrowser.SubscriptionCallback | 订阅信息回调 |
客户端调用服务端
意义 | TransportControls | MediaSession.Callback | 说明 |
---|---|---|---|
播放 | play() | onPlay() | |
停止 | stop() | onStop() | |
暂停 | pause() | onPause() | |
指定播放位置 | seekTo(long pos) | onSeekTo(long) | |
快进 | fastForward() | onFastForward() | |
回倒 | rewind() | onRewind() | |
下一首 | skipToNext() | onSkipToNext() | |
上一首 | skipToPrevious() | onSkipToPrevious() | |
指定id播放 | skipToQueueItem(long) | onSkipToQueueItem(long) | 指定的是Queue的id |
指定id播放 | playFromMediaId(String,Bundle) | onPlayFromMediaId(String,Bundle) | 指定的是MediaMetadata的id |
搜索播放 | playFromSearch(String,Bundle) | onPlayFromSearch(String,Bundle) | 需求不常见 |
指定uri播放 | playFromUri(Uri,Bundle) | onPlayFromUri(Uri,Bundle) | 需求不常见 |
发送自定义动作 | sendCustomAction(String,Bundle) | onCustomAction(String,Bundle) | 可用来更换播放模式、重新加载音乐列表等 |
打分 | setRating(Rating rating) | onSetRating(Rating) | 内置的评分系统有星级、红心、赞/踩、百分比 |
服务端回调给客户端
意义 | MediaSession | MediaController.Callback | 说明 |
---|---|---|---|
当前播放音乐 | setMetadata(MediaMetadata) | onMetadataChanged(MediaMetadata) | |
播放状态 | setPlaybackState(PlaybackState) | onPlaybackStateChanged(PlaybackState) | |
播放队列 | setQueue(List MediaSession.QueueItem>) | onQueueChanged(List MediaSession.QueueItem>) | |
播放队列标题 | setQueueTitle(CharSequence) | onQueueTitleChanged(CharSequence) | 不常用 |
额外信息 | setExtras(Bundle) | onExtrasChanged(Bundle) | 可以记录播放模式等信息 |
自定义事件 | sendSessionEvent(String,Bundle) | onSessionEvent(String, Bundle) |
MediaSession框架中个人感觉最妙的部分就是播放进度的获取了
如果在原来,可通过不断地调用MediaPlayer的getPosition获取播放进度,但如果项目的整体架构比较好的话,界面是拿不到MediaPlayer对象的。在MediaSession框架中,完全不需要去获取播放进度,当然前提是播放状态是准确的。
我们来看看PlaybackState.Builder的setState方法:
setState(int state, long position, float playbackSpeed)
setState(int state, long position, float playbackSpeed, long updateTime)
第二个的方法比第一个的多了一个参数叫更新时间,其实第一个方法会调用第二个方法,并指定更新时间为开机至今的时间(因为开机时间无法更改,系统时间可以改)。
在界面上上如何获得当前播放进度呢:
计算公式如下
((获取当前开机时间 – 上次更新状态的时间)* 播放速度 + 上次更新状态时的播放进度)
代码如下,这部分代码在MediaSessionRecord之中,后面会在源码里面说到。
long currentPosition = ((SystemClock.elapsedRealtime() – playbackState.getLastPositionUpdateTime() ) * playbackState. getPlaybackSpeed() ) + playbackState.getPosition();
Android5.0 提出了全新的MediaSession概念用于播放器与控制器之间进行交互,它取代之前的RemoteControlClient,并提供了更为灵活的客户端受控端模型,下面是它的架构图:
受控端(播放器):
播放器需要创建MediaSession,创建的时候就类似于在系统注册了它,并告诉系统它可以被其他控制端所控制。
framework(中介):
受控端创建MediaSession以后都会登记在framework中,framework同时会记录所有的MediaSession,并向控制端提供查询及状态更新服务。
控制端(播放状态显示及控制):
控制端实现方式可以多种多样,可以在另外一个app里实现,也可以在系统ui里实现,它主要通过framework的MediaSessionManager来查询系统中的session并根据自己的需要来选择要控制的session,并能要求系统在session发生变化的时候通知自己。
在同一个应用内部实现MediaSession框架只需要根据MediaSession框架实现即可,但是如果需要第三方应用(系统应用)来作为控制端那么就需要使用MediaSessionManager框架实现。
控制端需要先通过系统服务取得MediaSessionManager,然后查询系统中的Session,并根据自己的需求确定要控制的session实例,比如根据标识符或者包名来确认。
取得session以后需要完成两件事。
1、取得该session的MediaControl,该control用于控制播放器执行具体的操作,比如调用MediaControl的play方法将最终由系统执行播放器的callback里的onPlay;
2、需要向该session注册自己的callback,该callback在播放器状态发生变化时会被调用以通知控制端播放器的状态及数据(媒体名、进度等等)。
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
this.mediaSessionManager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
this.componentName = new ComponentName(context, context.getClass());
mediaSessionManager.addOnActiveSessionsChangedListener(this, componentName);
List<MediaController> controllers = mediaSessionManager.getActiveSessions(componentName);
for (MediaController mc : controllers) {
if (isSupported(mc)) {
mControl = mc;
}
}
mControl.registerCallback(new MediaController.Callback(){
@Override
public void onSessionDestroyed() {
}
@Override
public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
}
@Override
public void onPlaybackStateChanged(@Nullable PlaybackState state) {
}
@Override
public void onMetadataChanged(@Nullable MediaMetadata metadata) {
}
@Override
public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
}
@Override
public void onQueueTitleChanged(@Nullable CharSequence title) {
}
@Override
public void onExtrasChanged(@Nullable Bundle extras) {
}
@Override
public void onAudioInfoChanged(PlaybackInfo info) {
});
注意:需要添加权限android.permission.MEDIA_CONTENT_CONTROL,只有系统应用才可以调用
如果第三方应用也调用的话会提示:
Not for use by third-party applications due to privacy of media consumption
Constant Value: “android.permission.MEDIA_CONTENT_CONTROL”
参考: