Android P Media源码笔记

Vanora ·
更新时间:2024-11-10
· 981 次阅读

以前跟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.MediaMession框架说明

(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 控制端回调,可以接受到受控端的状态
(2)客户端调用服务端
意义 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) 内置的评分系统有星级、红心、赞/踩、百分比
(3)服务端回调给客户端
意义 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)

作者:qzns木雨



media Android

需要 登录 后方可回复, 如果你还没有账号请 注册新账号