前言

最近维护项目的时候遇到了MediaSessionCompat框架的音乐播放器,简单搜索记录一下这套实现播放器的结构吧。

MediaSession框架简介

我们先来看看如何设计一款音乐播放App的架构,传统的做法是这样的:

  • 注册一个Service,用于异步获取音乐库数据、音乐控制等,在Service中我们可能还需要自定义一些状态值和回调接口用于流程控制
  • 通过广播(其他方式如接口、Messenger都可以)实现Activity和Service之间的通信,使得用户可以通过界面上的组件控制音乐的播放、暂停、拖动进度条等操作

如果我们的音乐播放器还需要支持通知栏快捷控制音乐播放的功能,那么又得新增一套广播和相应的接口去响应通知栏按钮的事件。

如果还需要支持多端(电视、手表、耳机等)控制同一个播放器,那么整个系统架构可能会变得非常复杂,我们要花费大量的时间和精力去设计、优化代码的结构。那么有什么方法可以节省这些工作,提高我们的效率,然后还可以优雅地实现上述这些功能呢?

Google在Android 5.0中加入了MediaSession框架(在support-v4中同样提供了相应的兼容包,相关的类以Compat结尾,Api基本相同),专门用来解决媒体播放时界面和Service通讯的问题,意在规范上述这些功能的流程。使用这个框架我们可以减少一些流程复杂的开发工作,例如使用各种广播来控制播放器,而且其代码可读性、结构耦合度方面都控制得非常好,因此推荐大家尝试下这个框架。下面我们就开始介绍MediaSession框架的核心成员和使用流程。

MediaSessionCompat位于android/support/v4/media/session包下,主要是用于替代Android L 之后推出的MessionSession。我们通过一张别人的图来了解一下(这张图说的是MediaSession,而不是MediaSessionCompat,但大致原理是一样的):

常用成员类概述

MediaSession框架中有四个常用的成员类,它们是整个流程控制的核心

MediaBrowser

媒体浏览器,用来连接MediaBrowserService和订阅数据,通过它的回调接口我们可以获取和Service的连接状态以及获取在Service中异步获取的音乐库数据。媒体浏览器一般创建于客户端(可以理解为各个终端负责控制音乐播放的界面)中;

MediaBrowserService

浏览器服务,提供onGetRoot(控制客户端媒体浏览器的连接请求,通过返回值决定是否允许该客户端连接服务)和onLoadChildren(媒体浏览器向Service发送数据订阅时调用,一般在这执行异步获取数据的操作,最后将数据发送至媒体浏览器的回调接口中)这两个抽象方法;

同时MediaBrowserService还作为承载媒体播放器(如MediaPlayer、ExoPlayer等)和MediaSession的容器

MediaSession

媒体会话,即受控端,通过设置MediaSessionCompat.Callback回调来接收媒体控制器MediaController发送的指令,当收到指令时会触发Callback中各个指令对应的回调方法(回调方法中会执行播放器相应的操作,如播放、暂停等)。Session一般在Service.onCreate方法中创建,最后需调用setSessionToken方法设置用于和控制器配对的令牌并通知浏览器连接服务成功

MediaController

媒体控制器,在客户端中开发者不仅可以使用控制器向Service中的受控端发送指令,还可以通过设置MediaControllerCompat.Callback回调方法接收受控端的状态,从而根据相应的状态刷新界面UI。MediaController的创建需要受控端的配对令牌,因此需在浏览器成功连接服务的回调执行创建的操作

通过上述的简介中我们不难看出这四个成员之间有着非常明确的分工和作用范围,使得整个代码结构变得清晰易读。

除此之外,MediaSession框架中还有一些同样重要的类需要拿出来讲,例如封装了各种播放状态的PlaybackState,和Map相似通过键值对保存媒体信息的MediaMetadata,以及用于MediaBrowser和MediaBrowserService之间进行数据交互的MediaItem等等,下面我们通过实现一个简单的demo来具体分析这套框架的工作流程。

使用MediaSession框架构建简单的音乐播放器

例如我们的demo是这样的(见下图),只提供简单的播放暂停操作,音乐数据源从raw资源文件夹中获取:

按照工作流程,我们就从获取音乐库数据开始吧。首先界面上方添加一个RecyclerView来展示获取的音乐列表,我们在DemoActivity中完成一些RecyclerView的初始化操作

public class DemoActivity extends AppCompatActivity {private RecyclerView recyclerView;private List<MediaBrowserCompat.MediaItem> list;private DemoAdapter demoAdapter;private LinearLayoutManager layoutManager;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_demo);list = new ArrayList<>();layoutManager = new LinearLayoutManager(this);layoutManager.setOrientation(LinearLayoutManager.VERTICAL);demoAdapter = new DemoAdapter(this,list);recyclerView = (RecyclerView) findViewById(R.id.recyclerView);recyclerView.setLayoutManager(layoutManager);recyclerView.setAdapter(demoAdapter);}
}

注意List元素的类型为MediaBrowserCompat.MediaItem,因为MediaBrowser从服务中获取的每一首音乐都会封装成MediaItem对象。接下来我们创建MediaBrowser,并执行连接服务端和订阅数据的操作

public class DemoActivity extends AppCompatActivity {...private MediaBrowserCompat mBrowser;@Overrideprotected void onCreate(Bundle savedInstanceState) {...mBrowser = new MediaBrowserCompat(this,new ComponentName(this, MusicService.class),//绑定服务端browserConnectionCallback,//设置连接回调null);}@Overrideprotected void onStart() {super.onStart();//Browser发送连接请求mBrowser.connect();}@Overrideprotected void onStop() {super.onStop();mBrowser.disconnect();}/*** 连接状态的回调接口,连接成功时会调用onConnected()方法*/private MediaBrowserCompat.ConnectionCallback browserConnectionCallback=new MediaBrowserCompat.ConnectionCallback(){@Overridepublic void onConnected() {Log.e(TAG,"onConnected------");//必须在确保连接成功的前提下执行订阅的操作if (mBrowser.isConnected()) {//mediaId即为MediaBrowserService.onGetRoot的返回值//若Service允许客户端连接,则返回结果不为null,其值为数据内容层次结构的根ID//若拒绝连接,则返回nullString mediaId = mBrowser.getRoot();//Browser通过订阅的方式向Service请求数据,发起订阅请求需要两个参数,其一为mediaId//而如果该mediaId已经被其他Browser实例订阅,则需要在订阅之前取消mediaId的订阅者//虽然订阅一个 已被订阅的mediaId 时会取代原Browser的订阅回调,但却无法触发onChildrenLoaded回调//ps:虽然基本的概念是这样的,但是Google在官方demo中有这么一段注释...// This is temporary: A bug is being fixed that will make subscribe// consistently call onChildrenLoaded initially, no matter if it is replacing an existing// subscriber or not. Currently this only happens if the mediaID has no previous// subscriber or if the media content changes on the service side, so we need to// unsubscribe first.//大概的意思就是现在这里还有BUG,即只要发送订阅请求就会触发onChildrenLoaded回调//所以无论怎样我们发起订阅请求之前都需要先取消订阅mBrowser.unsubscribe(mediaId);//之前说到订阅的方法还需要一个参数,即设置订阅回调SubscriptionCallback//当Service获取数据后会将数据发送回来,此时会触发SubscriptionCallback.onChildrenLoaded回调mBrowser.subscribe(mediaId, BrowserSubscriptionCallback);}}@Overridepublic void onConnectionFailed() {Log.e(TAG,"连接失败!");}};/*** 向媒体服务器(MediaBrowserService)发起数据订阅请求的回调接口*/private final MediaBrowserCompat.SubscriptionCallback browserSubscriptionCallback =new MediaBrowserCompat.SubscriptionCallback(){@Overridepublic void onChildrenLoaded(@NonNull String parentId,@NonNull List<MediaBrowserCompat.MediaItem> children) {Log.e(TAG,"onChildrenLoaded------");//children 即为Service发送回来的媒体数据集合for (MediaBrowserCompat.MediaItem item:children){Log.e(TAG,item.getDescription().getTitle().toString());list.add(item);}//在onChildrenLoaded可以执行刷新列表UI的操作demoAdapter.notifyDataSetChanged();}};
}

通过上述的代码和注释大家应该清楚MediaBrowser连接服务到向其订阅数据的流程了,简单总结一下就是:

connect → onConnected → subscribe → onChildrenLoaded

那么Service端那边在这段流程中又做了什么呢?首先我们得继承MediaBrowserService(这里使用了support-v4包的类)创建MusicService类。MediaBrowserService继承自Service,所以记得在AndroidManifest.xml中完成配置:

<serviceandroid:name=".demo.MusicService"><intent-filter><action android:name="android.media.browse.MediaBrowserService" /></intent-filter>
</service>

我们需要在Service初始化的时候就完成MediaSession的构建,并为它设置相应的标志、状态等,具体的代码如下:

public class MusicService extends MediaBrowserServiceCompat {private MediaSessionCompat mSession;private PlaybackStateCompat mPlaybackState;@Overridepublic void onCreate() {super.onCreate();mPlaybackState = new PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_NONE,0,1.0f).build();mSession = new MediaSessionCompat(this,"MusicService");mSession.setCallback(SessionCallback);//设置回调mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);mSession.setPlaybackState(mPlaybackState);//设置token后会触发MediaBrowserCompat.ConnectionCallback的回调方法//表示MediaBrowser与MediaBrowserService连接成功setSessionToken(mSession.getSessionToken());}
}

这里解释下其中的一些细节,首先是调用MediaSession.setFlag为Session设置标志位,以便Session接收控制器的指令。然后是播放状态的设置,需调用MediaSession.setPlaybackState,那么PlaybackState又是什么呢?之前我们简单介绍过它是封装了各种播放状态的类,我们可以通过判断当前播放状态来控制各个成员的行为,而PlaybackState类为我们定义了各种状态的规范。此外我们还需要设置SessionCallback回调,当客户端使用控制器发送指令时,就会触发这些回调方法,从而达到控制播放器的目的。

public class MusicService extends MediaBrowserServiceCompat {...private MediaPlayer mMediaPlayer;@Overridepublic void onCreate() {...mMediaPlayer = new MediaPlayer();mMediaPlayer.setOnPreparedListener(PreparedListener);mMediaPlayer.setOnCompletionListener(CompletionListener);}/*** 响应控制器指令的回调*/private android.support.v4.media.session.MediaSessionCompat.Callback SessionCallback = new MediaSessionCompat.Callback(){/*** 响应MediaController.getTransportControls().play*/@Overridepublic void onPlay() {Log.e(TAG,"onPlay");if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PAUSED){mMediaPlayer.start();mPlaybackState = new PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_PLAYING,0,1.0f).build();mSession.setPlaybackState(mPlaybackState);}}/*** 响应MediaController.getTransportControls().onPause*/@Overridepublic void onPause() {Log.e(TAG,"onPause");if(mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING){mMediaPlayer.pause();mPlaybackState = new PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_PAUSED,0,1.0f).build();mSession.setPlaybackState(mPlaybackState);}}/*** 响应MediaController.getTransportControls().playFromUri* @param uri* @param extras*/@Overridepublic void onPlayFromUri(Uri uri, Bundle extras) {Log.e(TAG,"onPlayFromUri");try {switch (mPlaybackState.getState()){case PlaybackStateCompat.STATE_PLAYING:case PlaybackStateCompat.STATE_PAUSED:case PlaybackStateCompat.STATE_NONE:mMediaPlayer.reset();mMediaPlayer.setDataSource(MusicService.this,uri);mMediaPlayer.prepare();//准备同步mPlaybackState = new PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_CONNECTING,0,1.0f).build();mSession.setPlaybackState(mPlaybackState);//我们可以保存当前播放音乐的信息,以便客户端刷新UImSession.setMetadata(new MediaMetadataCompat.Builder().putString(MediaMetadataCompat.METADATA_KEY_TITLE,extras.getString("title")).build());break;}}catch (IOException e){e.printStackTrace();}}@Overridepublic void onPlayFromSearch(String query, Bundle extras) {}};/*** 监听MediaPlayer.prepare()*/private MediaPlayer.OnPreparedListener PreparedListener = new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mediaPlayer) {mMediaPlayer.start();mPlaybackState = new PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_PLAYING,0,1.0f).build();mSession.setPlaybackState(mPlaybackState);}} ;/*** 监听播放结束的事件*/private MediaPlayer.OnCompletionListener CompletionListener = new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mediaPlayer) {mPlaybackState = new PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_NONE,0,1.0f).build();mSession.setPlaybackState(mPlaybackState);mMediaPlayer.reset();}};
}

MediaSessionCompat.Callback中还有许多回调方法,大家可以按需覆盖重写即可:

构建好MediaSession后记得调用setSessionToken保存Session的配对令牌,同时调用此方法也会回调MediaBrowser.ConnectionCallback的onConnected方法,告知客户端Browser与BrowserService连接成功了,我们也就完成了MediaSession的创建和初始化

之前我们还讲到Browser与BrowserService的订阅关系,在MediaBrowserService中我们需要重写onGetRoot和onLoadChildren方法,其作用之前已经讲过就不多赘述了。

public class MusicService extends MediaBrowserServiceCompat {@Nullable@Overridepublic BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {Log.e(TAG,"onGetRoot-----------");return new BrowserRoot(MEDIA_ID_ROOT, null);}@Overridepublic void onLoadChildren(@NonNull String parentId, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {Log.e(TAG,"onLoadChildren--------");//将信息从当前线程中移除,允许后续调用sendResult方法result.detach();//我们模拟获取数据的过程,真实情况应该是异步从网络或本地读取数据MediaMetadataCompat metadata = new MediaMetadataCompat.Builder().putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ""+R.raw.jinglebells).putString(MediaMetadataCompat.METADATA_KEY_TITLE, "圣诞歌").build();ArrayList<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();mediaItems.add(createMediaItem(metadata));//向Browser发送数据result.sendResult(mediaItems);}private MediaBrowserCompat.MediaItem createMediaItem(MediaMetadataCompat metadata){return new MediaBrowserCompat.MediaItem(metadata.getDescription(),MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);}
}

最后我们回到客户端这边,四大成员还剩下控制器MediaController没讲。MediaController的创建依赖于Session的配对令牌,当Browser和BrowserService连接成功我们就可以通过Browser拿到这个令牌了。控制器创建后,我们就可以通过MediaController.getTransportControls的方法发送播放指令,同时也可以注册MediaControllerCompat.Callback回调接收播放状态,用以刷新界面UI:

public class DemoActivity extends AppCompatActivity {...private Button btnPlay;private TextView textTitle;@Overrideprotected void onCreate(Bundle savedInstanceState) {...btnPlay = (Button) findViewById(R.id.btn_play);textTitle = (TextView) findViewById(R.id.text_title);}public void clickEvent(View view) {switch (view.getId()) {case R.id.btn_play:if(mController!=null){handlerPlayEvent();}break;}}/*** 处理播放按钮事件*/private void handlerPlayEvent(){switch (mController.getPlaybackState().getState()){case PlaybackStateCompat.STATE_PLAYING:mController.getTransportControls().pause();break;case PlaybackStateCompat.STATE_PAUSED:mController.getTransportControls().play();break;default:mController.getTransportControls().playFromSearch("", null);break;}}/*** 连接状态的回调接口,连接成功时会调用onConnected()方法*/private MediaBrowserCompat.ConnectionCallback browserConnectionCallback =new MediaBrowserCompat.ConnectionCallback(){@Overridepublic void onConnected() {Log.e(TAG,"onConnected------");if (mBrowser.isConnected()) {...try{mController = new MediaControllerCompat(DemoActivity.this,mBrowser.getSessionToken());//注册回调mController.registerCallback(ControllerCallback);}catch (RemoteException e){e.printStackTrace();}}}@Overridepublic void onConnectionFailed() {Log.e(TAG,"连接失败!");}};/*** 媒体控制器控制播放过程中的回调接口,可以用来根据播放状态更新UI*/private final MediaControllerCompat.Callback ControllerCallback =new MediaControllerCompat.Callback() {/**** 音乐播放状态改变的回调* @param state*/@Overridepublic void onPlaybackStateChanged(PlaybackStateCompat state) {switch (state.getState()){case PlaybackStateCompat.STATE_NONE://无任何状态textTitle.setText("");btnPlay.setText("开始");break;case PlaybackStateCompat.STATE_PAUSED:btnPlay.setText("开始");break;case PlaybackStateCompat.STATE_PLAYING:btnPlay.setText("暂停");break;}}/*** 播放音乐改变的回调* @param metadata*/@Overridepublic void onMetadataChanged(MediaMetadataCompat metadata) {textTitle.setText(metadata.getDescription().getTitle());}};private Uri rawToUri(int id){String uriStr = "android.resource://" + getPackageName() + "/" + id;return Uri.parse(uriStr);}
}

MediaSession框架的基本用法我们已经分析完了。对于各自的播放器可能结构稍有不同,需要结合实际情况再分析。

Android媒体播放框架MediaSessionCompat介绍实践相关推荐

  1. Android 媒体播放框架MediaSession分析与实践

    版权声明:本文为博主原创文章,未经博主允许不得转载 源码:AnliaLee/BauzMusic 大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论 前言 最近一直在忙着学习和研究音乐播放器,发现介 ...

  2. android媒体播放框架,Android 使用超简单的多媒体播放器JiaoZiVideoPlayer

    在之前的项目中用到了视频播放的功能,在网上看了看使用了大家用的比较多的一个开源项目JiaoZiVideo可以迅速的实现视频播放的相关功能. JiaoZiVideo的简单使用 集成了JiaoZiVide ...

  3. 30个最佳和免费的Android媒体播放器

    今天发布的内容有所不同,我们为您介绍了Google Play App Store提供的The Best Android Media Player . 1. MusiXmatch歌词播放器 狂热的音乐爱 ...

  4. android播放器1004,Android媒体播放器在从外部网站播放流时出错(1,-1004)

    我试图从我的 Android应用程序中的网站播放音频文件,但它遇到媒体播放器错误(1,-1004). 当我在Windows媒体播放器或vlc播放器中使用它时,流链接工作正常.为什么Android媒体播 ...

  5. android 播放器 wav 无法播放,对于Android媒体播放器mp3与wav(For android media player mp3 vs. wav)...

    对于Android媒体播放器mp3与wav(For android media player mp3 vs. wav) 我想知道在Android媒体播放器上加载和播放小wav是否比较快的小文件更快. ...

  6. Android的多媒体框架OpenCore介绍

    网上资料很少, 不过还是找到一个比较详细的说明: 特地在此整理了下: 地址:http://blog.csdn.net/djy1992/article/details/9339787 分为几个阶段: 1 ...

  7. Android媒体播放器设计,基于Android系统多媒体播放器的设计与实现

    基于Android系统多媒体播放器的设计与实现 移动互联网自从其出现就以极强的吸引力捕获了全世界的关注,近些年尤以为甚,各类相关产品也随之蓬勃发展,Android智能手机就是其中之一.另外随着科技发展 ...

  8. android媒体播放器课程设计,基于Android的多媒体播放器课程设计报告

    基于Android的多媒体播放器课程设计报告 p/*/pp源代码. 我希望这篇文章对初学者有帮助. 希望更多的人在网络上共享自己的学习成果./pp*/pp1 .开发环境/ppAndroid是基于Lin ...

  9. android模拟ipod,替代iPod Touch的四款Android媒体播放器

    [牛华网讯]  北京时间2月13日消息,据国外媒体报道,智能手机和平板赢得了太多的关注,很容易忽略苹果iPod的销量仍高达数以百万.便携式的媒体播放器比如iPod Touch最终会退出市场,但目前仍然 ...

最新文章

  1. 传统多视图立体算法:PatchMatchStereo详解
  2. 浮点类型和布尔类型(Java)
  3. JavaWeb项目异常管理之log4j的使用教程
  4. GitHub热榜第一,标星近万:这个用Python做交互式图形的项目火了
  5. python 反转布尔值
  6. Linux文本界面设置网络,fedora17下通过文本界面配置网络
  7. 拦截导弹(二分匹配)
  8. mysql+sqlplus命令找不到_bash: sqlplus: command not found 解决方法
  9. 子类访问父类和方法覆写
  10. filezilla 共享多个目录_filezilla设置中文,3步搞定filezilla中文设置
  11. 20155209 林虹宇 Exp3 免杀原理与实践
  12. Hololens2 与Unity 远程连接调试程序和调试部署
  13. 阿里矢量库(各种图标搜索功能)
  14. 虚幻4引擎垃圾回收原理
  15. MTK刷机(ubuntu下)
  16. 2021年互联网大厂中秋礼盒PK!50多家一口气看完
  17. int函数使用方法c语言,int函数(int函数的使用方法举例)
  18. 最稳定的工作,是你认可的那份
  19. 为什么很多人打游戏感觉很快乐,然而学习工作中的满足感却很低
  20. 记得十年前谷歌大量使用python_关于利用Python玩转百万答题

热门文章

  1. 许昌学院计算机科学住几人间,许昌学院宿舍条件,宿舍环境图片(10篇)
  2. java 反射 修改字段_Java反射机制-修改字段值
  3. To connect to files.phpmyadmin.net insecurely, use `--no-check-certificate‘
  4. MFC中UPD通信详细解说
  5. Spring3.x与jdk8兼容问题
  6. 用无人机打点作画,密集恐惧症患者慎入!
  7. 一个 MVC 框架以 MVVM 之「魂」复活了!
  8. iptables的conntrack表满了导致访问网站很慢
  9. PostgreSQL client's startup packet different between logical and normal stream replication
  10. 如何开发auto complete 智能提示功能