以前跟Android Meida部分源码,做了细致的笔记,贴出来说不定会有帮助呢。
MediaSession使用参考这篇文章:MediaSession框架全解析
跟踪的工程路径:\android_9\aosp\packages\apps\Car\Media
需要加载的目录:
\android_9\aosp\packages\apps
\android_9\aosp\frameworks\base
说明多媒体跟踪代码的部分思路,直至找到具体的服务实现
1. 在MediaManager的setMediaClientComponent(…)中,新建了一个MediaBrowser,猜测是与服务通讯的桥梁
\android_9\aosp\packages\apps\Car\Media\src\com\android\car\media\MediaManager.java
mBrowser = new MediaBrowser(mContext, component, mMediaBrowserConnectionCallback, null);
mBrowser.connect();
->2. 跟踪MediaBrowser的connect()方法,确实是在启动服务并建立连接
X:\android_9\aosp\frameworks\base\media\java\android\media\browse\MediaBrowser.java
final Intent intent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
intent.setComponent(mServiceComponent);
mServiceConnection = new MediaServiceConnection();
在MediaServiceConnection()中,新建了一个ServiceCallbacks(存有弱引用MediaBrowser实例)调用AIDL方法的Connect()将其传入
mServiceBinder.connect(mContext.getPackageName(), mRootHints,
mServiceCallbacks);
->3. 跟踪mServiceComponent对象,弄清楚连接到哪里的服务
->1). mServiceComponent是一个ComponentName实例,ComponentName存有包名和类名,可以用作Intent的界面跳转
->2). mServiceComponent是在MediaBrowser构造方法中传入;
->3). 回到 1.MediaManager的setMediaClientComponent(…) 方法中,MediaBrowser的mServiceComponent对象是此方法参数传入的
->4. 在MediaActivity中,在changeMediaSource(…)方法中进行了setMediaClientComponent(…)的调用
X:\android_9\aosp\packages\apps\Car\Media\src\com\android\car\media\MediaActivity.java
ComponentName component = mMediaSource.getBrowseServiceComponentName();
MediaManager.getInstance(this).setMediaClientComponent(component);
->5. 跟踪MediaSource的getBrowseServiceComponentName()方法
X:\android_9\aosp\packages\apps\Car\libs\car-media-common\src\com\android\car\media\common\MediaSource.java
public ComponentName getBrowseServiceComponentName() {
if (mBrowseServiceClassName != null) {
return new ComponentName(mPackageName, mBrowseServiceClassName);
} else {
return null;
}
}
...
//mBrowseServiceClassName来自于此方法
private String getBrowseServiceClassName(String packageName) {
PackageManager packageManager = mContext.getPackageManager();
Intent intent = new Intent();
intent.setAction(MediaBrowserService.SERVICE_INTERFACE);
intent.setPackage(packageName);
List resolveInfos = packageManager.queryIntentServices(intent,
PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos == null || resolveInfos.isEmpty()) {
return null;
}
return resolveInfos.get(0).serviceInfo.name;
}
->6. 找到MediaBrowserService.SERVICE_INTERFACE字段
X:\android_9\aosp\frameworks\base\media\java\android\service\media\MediaBrowserService.java
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
->7. 全局搜索android.media.browse.MediaBrowserService
字段,在manifest找到对应的Action,找到具体实现服务
也就是以下路径:
蓝牙:
X:\android_9\aosp\packages\apps\Bluetooth\src\com\android\bluetooth\a2dpsink\mbs\A2dpMediaBrowserService.java
音乐:
X:\android_9\aosp\packages\apps\Music\src\com\android\music\MediaPlaybackService.java
X:\android_9\aosp\packages\apps\Car\LocalMediaPlayer\src\com\android\car\media\localmediaplayer\LocalMediaBrowserService.java
收音机:
X:\android_9\aosp\packages\apps\Car\Radio\src\com\android\car\radio\RadioService.java
二、结构与编写思路
Media的结构与编写思路
框图中的文件路径:
媒体界面
X:\android_9\aosp\packages\apps\Car\Media\src\com\android\car\media\MediaActivity.java
MediaSource
X:\android_9\aosp\packages\apps\Car\libs\car-media-common\src\com\android\car\media\common\MediaSource.java
MediaManager
X:\android_9\aosp\packages\apps\Car\Media\src\com\android\car\media\MediaManager.java
PlaybackModel
X:\android_9\aosp\packages\apps\Car\libs\car-media-common\src\com\android\car\media\common\PlaybackModel.java
三、MediaMession框架
对MediaMession框架的刷新与补充
总结
Mession底层仍是使用AIDL,它的优点和缺点同样突出
优点:完全统一了调用接口和回调接口,界面与不同类型音乐服务可以完全解耦、切换。
缺点:回调接口固定,限制较多,外部应用调用麻烦(需要完全理解框架并编写客户端);
(1)建立连接
客户端:
第一步,创建mediaBrowser,绑定服务,并关联绑定回调
MediaBrowserCompat mediaBrowser = new MediaBrowserCompat(this,
new ComponentName(this, MusicService.class), //绑定浏览器服务
mConnectionCallback,//关联连接回调
null);
第二步,获取mediaController,一般是在连接成功的回调中。当然源码在MediaSource中选择暴露获取controller的方法
//一般在连接回调获得
MediaBrowserCompat.ConnectionCallback mConnectionCallback =
new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
MediaSessionCompat.Token token = mMediaBrowser.getSessionToken();
MediaControllerCompat mediaController = new MediaControllerCompat(this, token);
//注册服务的回调
mediaController.registerCallback(mMediaControllerCallback);
}
};
//MediaSource中
public MediaController getMediaController() {
if (mBrowser == null || !mBrowser.isConnected()) {
return null;
}
MediaSession.Token token = mBrowser.getSessionToken();
return new MediaController(mContext, token);
}
本质是一样的,其中token相当于钥匙。然后就可以用mediaController来进行对服务的控制了,而MediaController.CallBack
就是服务的回调
服务端:
第一步,继承MediaBrowserService,重写onGetRoot(..)
和onLoadChildren(..)
方法,前者是判断是否允许客户端连接,后者是客户端异步请求信息的调用。
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
Bundle rootHints) {
/**
* 在返回数据之前,可以进行黑白名单控制,以控制不同客户端浏览不同的媒体资源
* */
if(!PackageUtil.isCallerAllowed(this, clientPackageName, clientUid)) {
return new BrowserRoot(null, null);
}
//此方法只在服务连接的时候调用
//返回一个rootId不为空的BrowserRoot则表示客户端可以连接服务,也可以浏览其媒体资源
//如果返回null则表示客户端不能流量媒体资源
return new BrowserRoot(BrowserRootId.MEDIA_ID_ROOT, null);
}
//需重写,异步请求数据,需要返回的结果
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List> result) {
....
}
第二步,初始化服务端对象Session
//初始化
MediaSessionCompat mSession = new MediaSessionCompat(this, "MusicService");
//表示MediaBrowser与MediaBrowserService连接成功
setSessionToken(mSession.getSessionToken());
//设置控制监听
mSession.setCallback(SessionCallback);
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
这里seesionCallBack就是MediaSessionCompat.Callback
对象,客户端controller的控制操作,就会调用到这个对象的重写方法里。而session就是传给客户端信息的主要对象了。
(2)控制与回调
客户端控制:可以使用mediaController.getTransportControls().skipToNext()
此类的方法,给服务通讯,而服务就会调到创建的MediaSessionCompat.Callback里
private android.support.v4.media.session.MediaSessionCompat.Callback SessionCallback = new MediaSessionCompat.Callback(){
/**
* 响应MediaController.getTransportControls().play
*/
@Override
public void onPlay() {
....
}
/**
* 响应MediaController.getTransportControls().onPause
*/
@Override
public void onPause() {
....
}
.....
}
此外,源码中显示mediaController可以获取当前播放媒体信息,播放状态等等,大致方法:
public MediaMetadata getMetadata() {...}
public PlaybackState getPlaybackState() {...}
public List getQueue(){...}
public CharSequence getQueueTitle(){....}
服务端回调:可以调用session.setMetadata(MediaMetadata)
此类方法给客户端通讯,而客户端就会调到创建的MediaController.CallBack
里
//媒体控制器控制播放过程中的回调接口
MediaControllerCompat.Callback mMediaControllerCallback =
new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) {
//响应session.setPlaybackState(PlaybackState)
//播放状态发生改变时的回调
onMediaPlayStateChanged(state);
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
//响应session.setMetadata(MediaMetadata)
//播放的媒体数据发生变化时的回调
if(metadata == null) {
return;
}
onPlayMetadataChanged(metadata);
}
};
(3)异步主动获取信息
客户端:mediaBrowser发起信息请求,注意发起前需要先unsubscribe,好像是官方bug。这里传入的ID,在服务端进行对请求的区分。
//----------异步获取数据------
//订阅/发起信息请求
mediaBrowser.unsubscribe(BrowserRootId.MEDIA_ID_MUSIC_LIST_REFRESH);
mediaBrowser.subscribe(BrowserRootId.MEDIA_ID_MUSIC_LIST_REFRESH, mSubscriptionCallback);
//异步回调接口
MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =
new MediaBrowserCompat.SubscriptionCallback() {
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List children) {
//数据获取成功后的回调
}
@Override
public void onError(@NonNull String id) {
//数据获取失败的回调
}
};
这部分源码很复杂,主要是在MediaSource.java中,简单说明对外调用的方法是subscribeChildren(...)
,而subscription对象继承于MediaBrowser.SubscriptionCallback
,回调也在其中重写方法里。
public void subscribeChildren(@Nullable String parentId, ItemsSubscription callback) {
if (mBrowser == null) {
throw new IllegalStateException("Browsing is not available for this source: "
+ getName());
}
if (mRootNode == null && !mBrowser.isConnected()) {
throw new IllegalStateException("Subscribing to the root node can only be done while "
+ "connected: " + getName());
}
mRootNode = mBrowser.getRoot();
String itemId = parentId != null ? parentId : mRootNode;
ChildrenSubscription subscription = mChildrenSubscriptions.get(itemId);
if (subscription != null) {
subscription.add(callback);
} else {
subscription = new ChildrenSubscription(mBrowser, itemId);
subscription.add(callback);
mChildrenSubscriptions.put(itemId, subscription);
subscription.start(CHILDREN_SUBSCRIPTION_RETRIES,
CHILDREN_SUBSCRIPTION_RETRY_TIME_MS);
}
}
服务端:继承MediaBrowserService时就必须重写onLoadChildren(..)
方法
parentId
可用于区分请求,result.sendResult(mediaItems)
用作向客户端返回一个list
,不管如何操作前需要result.detach();
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List> result) {
if(BrowserRootId.MEDIA_ID_MUSIC_LIST_REFRESH.equals(parentId)) {
//一定要先detach()
result.detach();
//模拟获取数据的过程
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ""+R.raw.jinglebells)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "圣诞歌")
.build();
ArrayList mediaItems = new ArrayList();
mediaItems.add(createMediaItem(metadata));
//向Browser发送数据,返回的是一个List
result.sendResult(mediaItems);
} else {
result.detach();
}
}
这部分服务代码就很多了,根据parentId进行区分再读取数据返回
(4)QueueItem和MediaMetadata
session.setQueue(List)
用于回调播放列表。
List
用于异步请求返回。
QueueItem和MediaMetadata是什么关系呢?QueueItem在构造的时候,需要MediaDescription,而MediaDescription可以通过MediaMetadata获得。在构造QueueItem时,注意id不重复
在DataModel.java中找到栗子:
MediaDescription.Builder builder = new MediaDescription.Builder()
.setMediaId(cursor.getString(keyColumn))
.setTitle(cursor.getString(titleColumn))
.setExtras(path);
if (subtitleColumn != -1) {
builder.setSubtitle(cursor.getString(subtitleColumn));
}
MediaDescription description = builder.build();
results.add(new MediaItem(description, mFlags));
// We rebuild the queue here so if the user selects the item then we
// can immediately use this queue.
if (mQueue != null) {
mQueue.add(new QueueItem(description, idx));
}
idx++;
以上是读取数据时,创建QueueItem和MediaItem区别,首先都是有description作为参数,区别在于MediaItem有一个mFlags标志位,而mQueue的参数idx是唯一不重复的id。
2.类与方法的说明 (1)主要类与概念类 | 概念 |
---|---|
android.media.session.MediaSession | 受控端 |
android.media.session.MediaSession.Token | 配对密钥 |
android.media.session.MediaController | 控制端 |
android.media.session.MediaSession.Callback | 受控端回调,可以接受到控制端的指令 |
android.media.session.MediaController.TransportControls | 控制端的遥控器,用于发送指令 |
android.media.session.MediaController.Callback | 控制端回调,可以接受到受控端的状态 |
意义 | 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) |