MediaSession框架全解析
MediaSession这种媒体框架由MediaBrowser(媒体浏览器)和MediaBrowserService(媒体浏览器服务)两部分组成。主要作用是规范了媒体服务和界面的通信接口,达到了完全解耦,可以自由、高效进行不同的媒体的切换。
一、基础用法
首先,我们先来看一下MediaSession主要类和对象的构成,如下图:
这个图只是用来对整个框架进行梳理和回顾,相信在看完后面的使用方法后就会觉得很简单了。
简单描述一下:
- MediaBrowser:媒体浏览器,用来连接服务,在连接成功的结果回调后,获取token(配对令牌),并以此获得MediaController媒体控制器。同时,有订阅并设置订阅信息回调的功能。
- MediaController:媒体控制器,可以用
mMediaController.getMetadata()
等方法来主动获取媒体信息,也可以使用形如mMediaController.getTransportControls().skipToNext()
来发送控制指令。其次一般需要注册MediaController.Callback回调进行客户端更新。 - MediaBrowserService:浏览器服务,实现具体的媒体逻辑。一般在oncrete()中用
setSessionToken(...)
来设置token。在重写的onGetRoot(…)中判断是否允许连接,在onLoadChildren(…)中处理订阅信息。 - MediaSeesion:设置MediaSeesion.Callback,这里就是客户端指令送达的地方。在媒体信息或状态改变后,使用形如
mediaSession.setMetadata(mediaMetadata)
来通知客户端。
下面是具体的使用:
1.连接,并建立联系
先来看客户端,我们需要做的是建立连接,并且在连接成功后设置回调。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {...//媒体浏览器private MediaBrowser mMediaBrowser;//媒体控制器private MediaController mMediaController;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//新建MediaBrowser,第一个参数是context//第二个参数是CompoentName,有多种构造方法,指向要连接的服务//第三个参数是连接结果的回调connectionCallback,第四个参数为BundlemMediaBrowser = new MediaBrowser(this,new ComponentName(this, MediaService.class), connectionCallback, null);mMediaBrowser.connect();...}//连接结果的回调private MediaBrowser.ConnectionCallback connectionCallback = new MediaBrowser.ConnectionCallback() {public void onConnected() {//如果服务端接受连接,就会调此方法表示连接成功,否则回调onConnectionFailed();Log.d(TAG, "onConnected: ");//获取配对令牌MediaSession.Token token = mMediaBrowser.getSessionToken();//通过token,获取MediaController,第一个参数是context,第二个参数为tokenmMediaController = new MediaController(getBaseContext(), token);//mediaController注册回调,callback就是媒体信息改变后,服务给客户端的回调mMediaController.registerCallback(mMediaCallBack);}public void onConnectionSuspended() {//与服务断开回调(可选)}public void onConnectionFailed() {//连接失败回调(可选)}}//服务对客户端的信息回调。private MediaController.Callback mMediaCallBack = new MediaController.Callback() {//回调函数的方法都是选择性重写的,这里不列举全,具体可查询文章末尾的表格@Overridepublic 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);...}@Overridepublic void onPlaybackStateChanged(PlaybackState state) {super.onPlaybackStateChanged(state);//播放状态信息回调,更新。PlaybackState在文章后面会提及if (state.getState() == PlaybackState.STATE_PLAYING) {//正在播放} ... //获取当前播放进度long position = state.getPosition()....}@Overridepublic void onQueueChanged(List<MediaSession.QueueItem> queue) {super.onQueueChanged(queue);//播放列表信息回调,QueueItem在文章后面会提及....} @Overridepublic void onSessionEvent(String event, Bundle extras) {super.onSessionEvent(event, extras);//自定义的事件回调,满足你各种自定义需求...}@Overridepublic 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;@Overridepublic void onCreate() {super.onCreate();Log.d(TAG, "onCreate: ");//初始化,第一个参数为context,第二个参数为String类型tag,这里就设置为类名了mediaSession = new MediaSession(this, "MediaService");//设置tokensetSessionToken(mediaSession.getSessionToken());//设置callback,这里的callback就是客户端对服务指令到达处mediaSession.setCallback(mCallback);}//mediaSession设置的callback,也是客户端控制指令所到达处private MediaSession.Callback mCallback = new MediaSession.Callback() {//重写的方法都是选择性重写的,不完全列列举,具体可以查询文章末尾表格@Overridepublic void onPlay() {super.onPlay();//客户端mMediaController.getTransportControls().play()就会调用到这里,以下类推//处理播放逻辑...//处理完成后通知客户端更新,这里就会回调给客户端的MediaController.CallbackmediaSession.setPlaybackState(playbackState);}@Overridepublic void onPause() {super.onPause();//暂停....}@Overridepublic void onSkipToNext() {super.onSkipToNext();//下一首.....//通知媒体信息改变mediaSession.setMetadata(mediaMetadata);}@Overridepublic void onCustomAction(String action, Bundle extras) {super.onCustomAction(action, extras);//自定义指令发送到的地方//对应客户端 mMediaController.getTransportControls().sendCustomAction(...)}....}//自己写的方法,用于改变播放列表private void changePlayList(){....//通知播放队列改变mediaSession.setQueue(queueItems);}@Overridepublic BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {//MediaBrowserService必须重写的方法,第一个参数为客户端的packageName,第二个参数为Uid//第三个参数是从客户端传递过来的Bundle。//通过以上参数来进行判断,若同意连接,则返回BrowserRoot对象,否则返回null;//构造BrowserRoot的第一个参数为rootId(自定义),第二个参数为Bundle;return new BrowserRoot("MyMedia", null);}@Overridepublic void onLoadChildren(String parentId, MediaBrowserService.Result<List<MediaBrowser.MediaItem>> result) {//MediaBrowserService必须重写的方法,用于处理订阅信息,文章后面会提及....}....
}
在服务端里,我们会发现跟客户端的所有操作是一一对应的。
在onCreate()中,我们创建了MediaSession,设置好了token,并设置了MediaSession.CallBack用于接收客户端的各项指令。完成媒体的逻辑后,在合适的地方,我们可以使用形如mediaSession.setMetadata(mediaMetadata)
回调给客户端进行媒体信息的更新。
而在BrowserRoot onGetRoot(…)方法中,我们可以通过其中的参数来判断是否准许客户端连接,不允许就直接返回null。
2.自定义通信接口与订阅
好了,用以上的知识我们可以做一个具有基础功能的多媒体了。不过,新的问题出现了: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<MediaSession.QueueItem> queue) | 通知客户端播放列表改变 |
客户端和服务端可以通过Bundle来进行信息传递,String类型作为自定义命令的标识,达到自定义接口的目的。
此外,我们向服务端主动异步获取或回调特定的媒体列表,可以用订阅的方式来进行。
客户端
//重复订阅会报错,所以先解除订阅mMediaBrowser.unsubscribe("PARENT_ID_1");//第一个参数是String类型的parentId(标识)//第二个参数为订阅的回调MediaBrowser.SubscriptionCallbackmMediaBrowser.subscribe("PARENT_ID_1", mCallback);...//订阅信息的回调private MediaBrowser.SubscriptionCallback mCallback = new MediaBrowser.SubscriptionCallback() {@Overridepublic void onChildrenLoaded(String parentId,List<MediaBrowser.MediaItem> children) {super.onChildrenLoaded(parentId, children);//订阅信息回调,parentID为标识,children为传回的媒体列表....}@Overridepublic void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children, Bundle options) {super.onChildrenLoaded(parentId, children, options);//订阅消息时添加了Bundle参数,会回调到此方法//即mMediaBrowser.subscribe("PARENT_ID_1", mCallback,bundle)的回调...}@Overridepublic void onError(String parentId) {super.onError(parentId);//出错..}
这里需要注意:
- 因为订阅后,也会到达服务端的
onLoadChildren(...)
,并回调数据到MediaBrowser.SubscriptionCallback,所以可以采用解除订阅,再进行订阅的方式进行主动异步获取操作(订阅后,获得回调信息)。
//这样可以进行异步数据回调mMediaBrowser.unsubscribe("PARENT_ID_1");mMediaBrowser.subscribe("PARENT_ID_1", mCallback);
- 不能重复订阅相同parentId的,会报错,所以建议订阅时都先做解除订阅的操作。
- 在 mMediaBrowser.subscribe(…)方法中,可以添加第三个Bundle参数,此时回调到同存在Bundle参数的onChildrenLoaded(…)方法中,注意别弄错了回调方法。
服务端
@Overridepublic 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");
二.涉及的媒体对象解析
(1)状态对象PlaybackState
PlaybackState对象承载的信息主要有两个:播放状态、播放进度
//PlaybackState的构建
PlaybackState mState = new PlaybackState.Builder()//三个参数分别是,状态,位置,播放速度.setState(state, position, playbackSpeed).build();//PlaybackState的解析
private MediaController.Callback mCallBack = new MediaController.Callback() {....@Overridepublic 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表示播放进度的。
(2)媒体信息对象 MediaMetadata、MediaSession.QueueItem、MediaBrowser.MediaItem
先来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() {@Overridepublic 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 和idMediaDescription description = new MediaDescription.Builder().setMediaId(song.mediaId).setTitle(song.title).setSubtitle(song.subtitle).setExtras(bundle).build();QueueItem queueItem = new QueueItem(description, song.queueId);//MediaMetadata转化为QueueItemQueueItem 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,第二个是flagsMediaBrowser.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框架全解析相关推荐
- Android 多窗口框架全解析
转载: https://blog.csdn.net/xiaosayidao/article/details/75045087 Android N的的多窗口框架中,总共包含了三种模式. Split-Sc ...
- Android图片加载框架最全解析(八),带你全面了解Glide 4的用法
本文转载自郭神的Glide分析系列:http://blog.csdn.net/guolin_blog/article/details/78582548 本文同步发表于我的微信公众号,扫一扫文章底部的二 ...
- Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53939176 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭 ...
- Android图片加载框架最全解析(五)
由此我们可以得知,在没有明确指定的情况下,ImageView默认的scaleType是FIT_CENTER. 有了这个前提条件,我们就可以继续去分析Glide的源码了.当然,本文中的源码还是建在第二篇 ...
- Android图片加载框架最全解析(七),实现带进度的Glide图片加载功能
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/78357251 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭 ...
- Android图片加载框架最全解析(三),深入探究Glide的缓存机制
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/54895665 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭 ...
- iOS音频播放第三方框架FreeStreamer解析(全)
iOS音频播放第三方框架FreeStreamer解析(全) 新项目是一款音频播放类软件,作为该项目核心功能:音频播放.第一版本是使用了系统提供的AVPlayer框架来实现音频文件的播放,再开发完成之后 ...
- hashmap value占用空间大小_【Java集合框架002】原理层面:HashMap全解析
一.前言 二.HashMap 2.1 HashMap数据结构 + HashMap线程不安全 + 哈希冲突 2.1.1 HashMap数据结构 学习的时候,先整体后细节,HashMap整体结构是 底层数 ...
- 腾讯零反射全动态Android插件框架Shadow解析
简介 最近几年,腾讯对于开源事业也是越来越支持,今天要说的就是在腾讯被广泛使用的Shadow框架,一个经过线上亿级用户量检验的反射全动态Android插件框架. 首先,让我们来看一下官方对于Shado ...
- 第四章:Spring项目文件上传两种方式(全解析)
欢迎查看Java开发之上帝之眼系列教程,如果您正在为Java后端庞大的体系所困扰,如果您正在为各种繁出不穷的技术和各种框架所迷茫,那么本系列文章将带您窥探Java庞大的体系.本系列教程希望您能站在上帝 ...
最新文章
- 编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型...
- CSS设置文字的划线
- 关于mysql内存表的一个帖子(转载)
- 【Java】猜数字小游戏
- clion远程调试linux内核,Clion + 树莓派/Ubuntu 远程调试
- 【报告分享】巨量算数:疫情期间汽车专题研究洞察.pdf(附下载链接)
- 计算机应用怎么写,计算机应用专业描述怎么写
- 免费的谷歌翻译api,Google Translate API
- 计算机打印纸如何盖章,怎样使电脑制作的印章具有手动盖章效果
- WX系列无线漫游的配置
- 内网集群 无法通信_IPSEC连接成功,内网之间却无法互相通信。
- 计算机检索系统常用的运算符,计算机信息检索过程中常用的检索表达式 计算机信息检索系统.doc...
- [翻译]Exploiting CVE-2015-0057 ——Part 1
- EBU5502 Database Coursework Specifications
- ImportError: DLL load failed:找不到指定的模块 解决方案
- c语言 __at定位编译报错,盈球新版 -官网
- ubuntu18.04安装Nvidia显卡驱动后黑屏及网络、蓝牙驱动消失的解决方案
- 育碧信条:AI 在手,天下我有
- Kali linux学习入门-Kali菜单中各工具功能
- 如何使用JCO3.0同时访问多个SAP系统