转眼间,在XX音乐(国内著名音乐APP公司)工作了1年多了,作为Android多媒体开发的主力,必须奉上一点知识了。
今天,先说一下android播放音乐时如何在蓝牙设备上显示歌曲名、歌手、专辑等信息的。
在那个风和日丽、鸟语花香的日子,突然客服Miss Hu发来一个消息,问我说,有用户反馈说在车载蓝牙上播放歌曲看不到歌曲名、歌手、专辑等信息。
我当时虽然不是一脸懵逼,但对这个问题而言确实是只知其一不知其二。
其一,代码中并没有任何直接与蓝牙相关的任何操作;
其二,真不清楚如何控制蓝牙显示的。于是乎,开始深入这个问题......
一、首先,讲一下Android上面蓝牙的部分规范。
截止到现在,世界上已经发布了约40个蓝牙应用规范。先介绍一下最常用的2个。分别是:
1.Advanced Audio Distribution Profile 简称为A2DP(高质量音频分发规范)定义了如何将立体声质量的音频通过流媒体的方式从媒体源传输到接收器上,A2DP有两种应用场景分别是播放和录音。
2.Audio Video Remote Control Profile 简称为AVRCP,定义了蓝牙设备和audio/video控制功能通信的特点和过程。该Profile定义了AV/C数字命令控制集。命令和信息通过AVCTP协议进行传输。
也就是说,连接蓝牙耳机的时候一般使用A2DP协议,而控制和显示通过AVCTP协议实现。
上图来自Google I/O 2013 - Best Practices for Bluetooth Development
那么谷歌是怎么推荐通过Avrcp在蓝牙设备上显示歌曲信息的呢?请看下图
顺便附上视频链接,分秒都给你seek到了,看不了youtube的自己想办法
https://www.youtube.com/watch?v=EC5-cEbr520&feature=youtu.be&t=25m18s
二、那我们去深入一下RemoteControlClient和Avrcp (此时已是身不由己)
RemoteControlClient enables exposing information meant to be consumed by remote controls capable of displaying metadata, artwork and media transport control buttons.
RemoteControlClient暴露信息给具有遥控功能的显示媒体、艺术品和按钮控制设备。(请忽略本人的翻译不准确性)
根据谷歌的说法,先往AudioManager里面注册一个RemoteControlClient实例,然后获取MetaDataEditor,往里面填充信息,然后执行MetaDataEditor.apply(),就是这么easy;
MetaDataEditor是什么? 这个不要问了,随便瞟两眼就知道了。
那么apply里面做了什么呢?
先看一下Android 4.3的源码,这里为什么先说这个版本,因为5.0系统与这个不一样,后面再详细解释。
apply里面根据参数不同,执行了不同的代码,我们只看sendMetadata_syncCacheLock好了。
先从mRcDisplays里面取出DisplayInfoForClient,发送IRemoteControlDisplay.setMetadata接口。
实现IRemoteControlDisplay.setMetadata接口共有下面几个:
我想大家已经看到了,Avrcp实现了这个,但是还需要确认一下。
上面说了,这一切都是从mRcDisplays中来的,mRcDisplays又是什么?
它是一个ArrayList
那么这么里面的DisplayInfoForClient又是哪里来的?
还是在RemoteControlClient这个类里面有一个方法onPlugDisplay里面有mRcDisplays.add(),从此处一一添加进去。
接着往下,onPlugDisplay是在一个MSG_PLUG_DISPLAY消息里面处理的。这个消息是从plugRemoteControlDisplay()这个方法里面执行的。
关键点来了,是谁搞了plugRemoteControlDisplay()这个?
讲到这里,开始跳入framework层代码,不卖关子了,这个是在AudioService里面执行了。
这个plugRemoteControlDisplaysIntoClient_syncRcStack方法是在
AudioService里面注册registerRemoteControlClient的时候调用了。
哈哈,看到这里,是不是想起了google官方介绍如何使用RemoteControlClient的,就是注册到AudioService。具体怎么玩,这里不讲了,因为不是这里的重点。
刚刚上面已经提到IRemoteControlDisplay.setMetadata去更新数据,那么这个IRemoteControlDisplay到底是哪里来的?
于是,上图已经给了答案了,是AudioService中的mRcDisPlays中的。
这个mRcDisPlays里面的内容是通过registerRemoteControlDisplay方法add进去的。
而registerRemoteControlDisplay是在AudioManager中调用的。查找引用,发现
也就是说Avrcp里面注册了这个registerRemoteControlDisplay。
这里就不说其他三个,重点还是蓝牙上面。
registerRemoteControlDisplay是在start里面执行的,start是在make里面执行的,make是在com.android.bluetooth.a2dp.A2dpServic.start里面的,而这个是在ProfileService启动的时候执行的,再往上就不深究了,这里已经有答案了。
这里的结论是IRemoteControlDisplay.setMetadata确实是发给Avrcp里面继承这个接口的元素了。
接下来看com.android.bluetooth.avrcp.Avrcp这个东西。
Avrcp中IRemoteControlDisplayWeak类继承扩展了这个接口,实现了setMetadata这个方法。
setMetadata执行了updateMetadata方法,将歌曲信息更新到内部的mMetadata里面。
至于如何发送,接收端如何显示,这里也不作详细解释了。
也就是说如果Android手机连接蓝牙播放,最好把歌曲信息、歌手、专辑等信息通过RemoteControlClient发送给蓝牙就行了,蓝牙设备就能对应的显示出来歌曲内容和播放状态。
还有一件事,不要忘了,上面讲的是Android4.3的系统。对于5.0以上系统和5.0以下系统是不一样的。
5.0系统RemoteControlClient中的MetaDataEditor.apply()是将MetaData给MediaSession传递过去的,在MediaSession中通过setMetadata(metadata)方法将metadata设置进去。
先调用AudioManager中的registerRemoteControlClient方法注册RemoteControlClient为rcClient。
而AudioManager中的registerRemoteControlClient有三个调用地方:
然后给rcClient注册MediaSessionLegacyHelper单例为helper。
rcClient.registerWithSession(MediaSessionLegacyHelper.getHelper(mContext));
helper是MediaSessionLegacyHelper.getHelper(mContext)获取到的。
registerWithSession中helper添加了监听helper.addRccListener,然后获取MediaSession。
MediaSession是从ArrayMap
mSessions是在getHolder()方法中put进去的。
也就是在MediaSessionLegacyHelper.addRccListener(),MediaSessionLegacyHelper.removeRccListener(),
MediaSessionLegacyHelper.addMediaButtonListener(),MediaSessionLegacyHelper.removeMediaButtonListener()
四个方法中实现的。
这也就是说为什么google官网要求RemoteCntrolClinet要跟mediabutton的注册要一起了。
提到这里就伤心不已,后来因为优化了mediabutton的注册策略,引起了蓝牙显示问题,唉,都是演技....
哎呀,又刹不住车了,扯远了,回归正题。
现在说说MediaSession.setMetadata(metadata);
mBinder是什么?
mBinder是通过MediaSessionManager.createSession(mCbStub, tag, userId)得到的。
也就是MediaSessionService.createSession()得到的。
进而得知返回的是类型为MediaSessionRecord的实例。
也就是说MediaSession.setMetadata实际是执行了MediaSessionRecord.setMetadata();
MediaSessionRecord.setMetadata()里面发送MSG_UPDATE_METADATA这个消息由MessageHandler处理,调用pushMetadataUpdate()方法,交给cb.onMetadataChanged处理,cb就是ISessionControllerCallback。
再来看com.android.bluetooth.avrcp.Avrcp文件,
在Avrcp.start()时,将一个构建了mRemoteController类注册到AudioService中,
mRemoteController中的RemoteControllerWeak用来监听MediaSession的变化。
当AudioManager.registerRemoteController()时,调用rctlr.startListeningToSessions()。
然后构建出一个SessionsListenerRecord添加到ArrayList类型的mSessionsListeners中。
重点来了,通过MediaSessionManager.getActiveSessions()方法将其设置到MediaController里面。
MediaController已经实现了ISessionControllerCallback接口,当接收到onMetadataChanged()时,发送MSG_UPDATA_METADATA消息执行mCallback.onMetadataChanged();
接着调用MediaControllerCallback.onMetadataChanged()执行onNewMediaMetadata(metadata)。
onNewMediaMetadata内部调用Avrcp中的RemoteControllerWeak.onClientMetadataUpdate()从而将歌曲信息更新到Avrcp中。
这样就将RemoteControlClient和Avrcp连接起来了,使用的是MediaSession。
而Android 4.3使用的则是RemoteControlDisplay,这就是二者的区别,却在app层接口基本是一致的。
好了,到这里基本算是搞定了RemoteControlClient和Avrcp的关系了,也算完成了蓝牙播放显示歌曲信息的功能了。
唉,这会感觉快吐血了。其实这一段反反复复斟酌了好久文字,担心读者搞不清楚,结果差点把自己也绕进去,还好我是这篇文章的原创,这口血已经咽回去了。
三、如果感觉上面太深(各位大神请见谅我的自夸,),同为媒体组的兄弟们还是不要笑话我了,
直接Show Code吧(亮剑)
这里因为业务需要我已经把MediaMetadata信息装进了HashMap,大家还是按照官方要求就行了。
同志们,到这里,你觉得完成了这个需求了吗?
别怕打击,这个时候其实只完成了55%(数据不确定性,完全凭个人感觉捏造),还有很多手机不能显示。为什么呢?
因为RemoteControlClient是从Android 4.0才出现的,那之前的系统呢?
所以,蓝牙肯定还有一种取信息的方式,至少一种。
这种方法是广播com.android.music.metachanged。
直接Show Code吧
Intent mediaIntent =newIntent("com.android.music.metachanged");
mediaIntent.putExtra("artist","歌手名称");
mediaIntent.putExtra("track","歌曲名称");
mediaIntent.putExtra("album","专辑名称");
mediaIntent.putExtra("duration", (Long) duration));
mediaIntent.putExtra("playing", (boolean) playing); //播放状态
getContext().sendBroadcast(mediaIntent); //豆沙绿的背景看起来是不是眼睛舒服多了.......
做完这一步,98%的蓝牙设备都能正常显示了,但是请记住,发送完这个广播之后,如果不小心执行了metadata.clear(), metadata.apply(),你的信息可能就会被清除了。
但是别忘了,还有2%的解决不了,为什么呢?
部分三星手机搞不掂(在官网论坛看到一个说法,跟自身的适配有关);
部分车载蓝牙显示异常,最起码我的车是这样的
(前面增加数字相关的字符串,其实我想说,车载蓝牙可能是一个很混乱的行业,不过腾讯、苹果等土豪已经涉足合作了,也许未来能统一起来)
比如,客户反馈一台宝马三系用我们产品蓝牙显示异常,于是我们真的去租了一辆BMW用来调试。
不要质疑我们的行为,这不是炒作,我很负责的告诉你,我们媒体组真的很敬业。
到这里,特此感谢媒体组各位的支持和帮助,感谢某人的警示良言,让我坚持2天不松懈。
好了,到此为止了,真心累了,写文章真的很耗脑力体力,周末整个没休息,不过还是要多写写。
期待下一篇好文!