文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android P Media源码笔记

2022-06-06 13:43

关注

以前跟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(){
    
    @Override
    public void onPlay() {
            ....
        }
    
    @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木雨


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯