MediaSession框架全解析

MediaSession这种媒体框架由MediaBrowser(媒体浏览器)和MediaBrowserService(媒体浏览器服务)两部分组成。主要作用是规范了媒体服务和界面的通信接口,达到了完全解耦,可以自由、高效进行不同的媒体的切换。

一、基础用法

首先,我们先来看一下MediaSession主要类和对象的构成,如下图:
在这里插入图片描述
这个图只是用来对整个框架进行梳理和回顾,相信在看完后面的使用方法后就会觉得很简单了。

简单描述一下:

  • MediaBrowser:媒体浏览器,用来连接服务,在连接成功的结果回调后,获取token(配对令牌),并以此获得MediaController媒体控制器。同时,有订阅并设置订阅信息回调的功能。
  • MediaController:媒体控制器,可以用mMediaController.getMetadata()等方法来主动获取媒体信息,也可以使用形如mMediaController.getTransportControls().skipToNext()来发送控制指令。其次一般需要注册MediaController.Callback回调进行客户端更新。
  • MediaBrowserService:浏览器服务,实现具体的媒体逻辑。一般在oncrete()中用setSessionToken(...)来设置token。在重写的onGetRoot(…)中判断是否允许连接,在onLoadChildren(…)中处理订阅信息。
  • MediaSeesion:设置MediaSeesion.Callback,这里就是客户端指令送达的地方。在媒体信息或状态改变后,使用形如mediaSession.setMetadata(mediaMetadata)来通知客户端。

下面是具体的使用:

1.连接,并建立联系

先来看客户端,我们需要做的是建立连接,并且在连接成功后设置回调。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
	...
	//媒体浏览器
    private MediaBrowser mMediaBrowser;
    //媒体控制器
    private MediaController mMediaController;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //新建MediaBrowser,第一个参数是context
        //第二个参数是CompoentName,有多种构造方法,指向要连接的服务
        //第三个参数是连接结果的回调connectionCallback,第四个参数为Bundle
        mMediaBrowser = new MediaBrowser(this,
                new ComponentName(this, MediaService.class), connectionCallback, null);
        mMediaBrowser.connect();
        ...
    }

	//连接结果的回调
	 private MediaBrowser.ConnectionCallback connectionCallback 
	 							= new MediaBrowser.ConnectionCallback() {
	 
		 public void onConnected() {
		     	//如果服务端接受连接,就会调此方法表示连接成功,否则回调onConnectionFailed();
	           Log.d(TAG, "onConnected: ");
	           //获取配对令牌
	           MediaSession.Token token = mMediaBrowser.getSessionToken();
	           //通过token,获取MediaController,第一个参数是context,第二个参数为token
	           mMediaController = new MediaController(getBaseContext(), token);
			
			   //mediaController注册回调,callback就是媒体信息改变后,服务给客户端的回调
	           mMediaController.registerCallback(mMediaCallBack);
		     }
		       
		    public void onConnectionSuspended() {
		         //与服务断开回调(可选)
		     }
		     
		    public void onConnectionFailed() {
		         //连接失败回调(可选)
		     }
	 }

	//服务对客户端的信息回调。
 	private MediaController.Callback mMediaCallBack = new MediaController.Callback() {
		//回调函数的方法都是选择性重写的,这里不列举全,具体可查询文章末尾的表格
		  @Override
        public void onMetadataChanged(MediaMetadata metadata) {
            super.onMetadataChanged(metadata);
			//服务端运行mediaSession.setMetadata(mediaMetadata)就会到达此处,以下类推.

         	//歌曲信息回调,更新。MediaMetadata在文章后面会提及
			MediaDescription description = metadata.getDescription();
			//获取标题
			String title = description.getTitle().toString();
			//获取作者
			String author = description.getSubtitle().toString();
			//获取专辑名
            String album = description.getDescription().toString();
			//获取总时长
			long duratime = mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
			...
        }
        
		 @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            super.onPlaybackStateChanged(state);
			//播放状态信息回调,更新。PlaybackState在文章后面会提及
			 if (state.getState() == PlaybackState.STATE_PLAYING) {
          		//正在播放
          	 } 
          	 ...	
			//获取当前播放进度
			 long position = state.getPosition()
		     ....
        }

		@Override
        public void onQueueChanged(List<MediaSession.QueueItem> queue) {
            super.onQueueChanged(queue);
			//播放列表信息回调,QueueItem在文章后面会提及
			....
        }	

		@Override
        public void onSessionEvent(String event, Bundle extras) {
            super.onSessionEvent(event, extras);
            //自定义的事件回调,满足你各种自定义需求
            ...
        }

        @Override
        public void onExtrasChanged(Bundle extras) {
            super.onExtrasChanged(extras);
            //额外信息回调,可以承载播放模式等信息
        }
        .....
	}
}

以上代码,做了一个什么事情呢?我们在onCreate()中去连接了一个继承MediaBrowserService的服务。并在连接成功的信息后,我们取得了mMediaController,并且注册了一个回调,用于知晓服务端通知的媒体信息变更。很简单的的开始,在后面的代码中,就可以用mMediaController为所欲为了。

	 //在需要的地方使用以下代码
	 //控制媒体服务的一些方法,播放、暂停、上下首、跳转某个时间点...可查看文章末尾表格
	 mMediaController.getTransportControls().play();
	 mMediaController.getTransportControls().pause();
	 mMediaController.getTransportControls().skipToPrevious();
	 mMediaController.getTransportControls().skipToNext();
	 mMediaController.getTransportControls().seekTo(...);
	 ....
	 //主动获取媒体信息的一些操作,获取媒体信息,播放状态...可查看文章末尾表格
     MediaMetadata metadata = mMediaController.getMetadata();
     PlaybackState playbackState = mMediaController.getPlaybackState();
     ....

需要留意的坑非主线程创建MediaBrowser并connect的时候会报错。这是因为连接时底层代码会使用Handler,并且采用Handler handler = new Handler()的创建方式,如此使用必然会报错。解决办法:

	 Looper.prepare();
	 
     mBtMusicBrowser = new MediaBrowser(BaseApplication.getInstance(),
     			 //绑定服务,这里绑定的是系统蓝牙音乐的服务
                 new ComponentName("com.android.bluetooth", 
                 	"com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService"), 
                 mConnectionCallback,//关联连接回调
                 null);
     mBtMusicBrowser.connect();
     
     Looper.loop();

在之前和之后加上Looper.prepare()和Looper.loop()就搞定了,这个可以参考Handler的机制进行理解。


接着看服务端,我们要做的是同意客户端连接,响应客户端的控制命令,并且在信息改变时通知回调给客户端。

public class MediaService extends MediaBrowserService{

	...
	//媒体会话,受控端
	private MediaSession mediaSession;

	@Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: ");
        //初始化,第一个参数为context,第二个参数为String类型tag,这里就设置为类名了
        mediaSession = new MediaSession(this, "MediaService");
        //设置token
        setSessionToken(mediaSession.getSessionToken());
        //设置callback,这里的callback就是客户端对服务指令到达处
        mediaSession.setCallback(mCallback);
    }

	//mediaSession设置的callback,也是客户端控制指令所到达处
	private MediaSession.Callback mCallback = new MediaSession.Callback() {
	//重写的方法都是选择性重写的,不完全列列举,具体可以查询文章末尾表格
		@Override
        public void onPlay() {
            super.onPlay();
			//客户端mMediaController.getTransportControls().play()就会调用到这里,以下类推
			//处理播放逻辑
			...
			//处理完成后通知客户端更新,这里就会回调给客户端的MediaController.Callback
			
			mediaSession.setPlaybackState(playbackState);
        }
		
        @Override
        public void onPause() {
            super.onPause();
            //暂停
            ....
        }

        @Override
        public void onSkipToNext() {
            super.onSkipToNext();
            //下一首
            .....
            //通知媒体信息改变
            mediaSession.setMetadata(mediaMetadata);
        }

        @Override
        public void onCustomAction(String action, Bundle extras) {
            super.onCustomAction(action, extras);
			//自定义指令发送到的地方
			//对应客户端 mMediaController.getTransportControls().sendCustomAction(...)

        }
		....
	}
	
	//自己写的方法,用于改变播放列表
	private void changePlayList(){
		....
		
		//通知播放队列改变
		mediaSession.setQueue(queueItems);
	}

	 @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
		//MediaBrowserService必须重写的方法,第一个参数为客户端的packageName,第二个参数为Uid
		//第三个参数是从客户端传递过来的Bundle。
		//通过以上参数来进行判断,若同意连接,则返回BrowserRoot对象,否则返回null;

		//构造BrowserRoot的第一个参数为rootId(自定义),第二个参数为Bundle;
        return new BrowserRoot("MyMedia", null);
    }

	 @Override
    public void onLoadChildren(String parentId, 
    				MediaBrowserService.Result<List<MediaBrowser.MediaItem>> result) {
	//MediaBrowserService必须重写的方法,用于处理订阅信息,文章后面会提及
	....
	}

	....
}

在服务端里,我们会发现跟客户端的所有操作是一一对应的。

在onCreate()中,我们创建了MediaSession,设置好了token,并设置了MediaSession.CallBack用于接收客户端的各项指令。完成媒体的逻辑后,在合适的地方,我们可以使用形如mediaSession.setMetadata(mediaMetadata)回调给客户端进行媒体信息的更新。

而在BrowserRoot onGetRoot(…)方法中,我们可以通过其中的参数来判断是否准许客户端连接,不允许就直接返回null。

2.自定义通信接口与订阅

好了,用以上的知识我们可以做一个具有基础功能的多媒体了。不过,新的问题出现了:MediaSession框架中的通信接口是有限的,如果我们的需求不止步于简单的控制怎么办,比如要满足收藏功能,改变歌曲播放的循环模式,或者获取某一个音乐列表,甚至某些独特的需求…

MediaSession框架提供了一些接口,对应关系如下表

MediaController(客户端)MediaSession.Callback(服务端)作用
sendCustomAction(String action, Bundle args)onCustomAction(String action, Bundle extras)发送/接收自定义指令
MediaSession(服务端)MediaController.Callback(客户端)作用
sendSessionEvent(String event, Bundle extras)onSessionEvent(String event, Bundle extras)发送/接收自定义指令
setExtras(Bundle extras)onExtrasChanged(Bundle extras)通知客户端更新额外信息,播放模式等…
setQueue(List< QueueItem> queue)onQueueChanged(List<MediaSession.QueueItem> queue)通知客户端播放列表改变

客户端和服务端可以通过Bundle来进行信息传递,String类型作为自定义命令的标识,达到自定义接口的目的。

此外,我们向服务端主动异步获取回调特定的媒体列表,可以用订阅的方式来进行。
客户端

	//重复订阅会报错,所以先解除订阅
    mMediaBrowser.unsubscribe("PARENT_ID_1");
    //第一个参数是String类型的parentId(标识)
    //第二个参数为订阅的回调MediaBrowser.SubscriptionCallback
    mMediaBrowser.subscribe("PARENT_ID_1", mCallback);
    ...

	//订阅信息的回调
	private MediaBrowser.SubscriptionCallback mCallback 
							= new MediaBrowser.SubscriptionCallback() 	{
							
	       @Override
	       public void onChildrenLoaded(String parentId,
	       								 	List<MediaBrowser.MediaItem> children) {
	           super.onChildrenLoaded(parentId, children);
			   //订阅信息回调,parentID为标识,children为传回的媒体列表
			   ....
	       }
	       
	        @Override
	        public void onChildrenLoaded(String parentId, 
	        				List<MediaBrowser.MediaItem> children, Bundle options) {
	            super.onChildrenLoaded(parentId, children, options);
				//订阅消息时添加了Bundle参数,会回调到此方法
				//即mMediaBrowser.subscribe("PARENT_ID_1", mCallback,bundle)的回调
				...
	        }

	       @Override
	       public void onError(String parentId) {
	           super.onError(parentId);
	       	   //出错..
	       }

这里需要注意:

  • 因为订阅后,也会到达服务端的onLoadChildren(...),并回调数据到MediaBrowser.SubscriptionCallback,所以可以采用解除订阅,再进行订阅的方式进行主动异步获取操作(订阅后,获得回调信息)。
   	//这样可以进行异步数据回调
   	mMediaBrowser.unsubscribe("PARENT_ID_1");
   	mMediaBrowser.subscribe("PARENT_ID_1", mCallback);
  • 不能重复订阅相同parentId的,会报错,所以建议订阅时都先做解除订阅的操作。
  • 在 mMediaBrowser.subscribe(…)方法中,可以添加第三个Bundle参数,此时回调到同存在Bundle参数的onChildrenLoaded(…)方法中,注意别弄错了回调方法。

服务端

    @Override
    public void onLoadChildren(String parentId, 
    				MediaBrowserService.Result<List<MediaBrowser.MediaItem>> result) {
        //使用result之前,一定需要detach();
        result.detach();
		//新建MediaItem数组
        ArrayList<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
		
		//根据parentId,获取不同的媒体列表
		switch(parentId){
			case MEDIA_ID_ROOT:
				....
				break;
			case PARENT_ID_1:
			    //模拟数据
		        MediaMetadata metadata = new MediaMetadata.Builder()
		               .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "101")
		               .putString(MediaMetadata.METADATA_KEY_TITLE, "一首歌")
		               .build();
		        mediaItems.add(new MediaBrowser.MediaItem(metadata.getDescription(), 
		        							MediaBrowser.MediaItem.FLAG_PLAYABLE));
                break;
                ...
		}
		//发送数据
        result.sendResult(mediaItems);
    }

服务端重写的onLoadChildren(…)用作订阅不同parentId返回不同的媒体数据。此外进行订阅后,服务端可以通过notifyChildrenChanged(String parentId)发送消息来进行回调

	//服务端可以直接使用notifyChildren(..),会到达onLoadChildren(..)中,并回调数据
	//如果客户端订阅了对应parentId,那么在MediaBrowser.SubscriptionCallback中就能收到媒体数据
	notifyChildrenChanged("parentID_1");

二.涉及的媒体对象解析

(1)状态对象PlaybackState

PlaybackState对象承载的信息主要有两个:播放状态、播放进度

//PlaybackState的构建
PlaybackState mState = new PlaybackState.Builder()
							//三个参数分别是,状态,位置,播放速度
							.setState(state, position, playbackSpeed)
							.build();

//PlaybackState的解析
private MediaController.Callback mCallBack = new MediaController.Callback() {
	 ....
	 @Override
     public void onPlaybackStateChanged(PlaybackState playbackState) {
        super.onPlaybackStateChanged(state);
		//获得进度时长
		long position = playbackState.getPosition();
		
		//获得当前状态
		switch(playbackState.getState()){
			case PlaybackState.STATE_PLAYING:
				//正在播放
				...
				break;
			case PlaybackState.STATE_PAUSED:
				//暂停
				...
				break;
			case PlaybackState.ACTION_SKIP_TO_NEXT:
				//跳到下一首
				...
				break;
			...//还有很多状态标志,按需求添加
		}
	}
}

构建时,setState(…)有两个方法:
setState(int state, long position, float playbackSpeed)
setState(int state, long position, float playbackSpeed, long updateTime)
上面一个方法其实是调用的下面一个方法,updateTime自动设置为开机时间。

注意:播放进度的获取需要具体逻辑进行计算,客户端和服务端逻辑统一就可以了。 笔者是直接通过position表示播放进度的。

(2)媒体信息对象 MediaMetadata、MediaSession.QueueItem、MediaBrowser.MediaItem

先来MediaMetadata的使用:

//构建,把代码中的字符串替换成歌曲的对应字符串
MediaMetadata metadata = new MediaMetadata.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "id") //id
        .putString(MediaMetadata.METADATA_KEY_TITLE, "title")//标题
        .putString(MediaMetadata.METADATA_KEY_ARTIST,"artist")//作者
        .putString(MediaMetadata.METADATA_KEY_ALBUM,"album")//唱片
        .putLong(MediaMetadata.METADATA_KEY_DURATION,"duration")//媒体时长
        .build();

//解析,通过MediaDescription获取信息
private MediaController.Callback mCallBack = new MediaController.Callback() {
 	@Override
    public void onMetadataChanged(MediaMetadata metadata) {
        super.onMetadataChanged(metadata);
		MediaDescription description = mediaMetadata.getDescription();
		//获取标题
		String title = description.getTitle().toString();
		//获取作者
		String author = description.getSubtitle().toString();
		//获取专辑名
        String album = description.getDescription().toString();
		//获取总时长
		long duratime = mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
	}
	....
}

MediaSession.QueueItem比MediaMetadata多了一个唯一的id

	//构建,传入MediaDescription 和id
	MediaDescription description = new MediaDescription.Builder()
                    .setMediaId(song.mediaId)
                    .setTitle(song.title)
                    .setSubtitle(song.subtitle)
                    .setExtras(bundle)
                    .build();
    QueueItem queueItem = new QueueItem(description, song.queueId);
	
	//MediaMetadata转化为QueueItem
	QueueItem queueItem = new QueueItem(mediaMetadata.getDescription(), id);

	//解析跟MediaMetadata一样,获取MediaDescription 
	MediaDescription description = queueItem.getDescription();
	//获取标题
	String title = description.getTitle().toString();
	.....

MediaBrowser.MediaItem跟MediaSession.QueueItem很相似,不同的是唯一的id,变成了flags

	//MediaMetadata转化为MediaItem,构造方法第一个都是MediaDescription,第二个是flags
	MediaBrowser.MediaItem mediaItem = new MediaBrowser.MediaItem(metadata.getDescription(), 
														MediaBrowser.MediaItem.FLAG_PLAYABLE);

	//解析一样用MediaDescription 
	MediaDescription description = queueItem.getDescription();
	//获取标题
	String title = description.getTitle().toString();
	...

三、附录:类与方法一览

主要的类与概念

类别概念
服务端android.media.session.MediaSession受控端
android.media.session.MediaSession.Token配对密钥
android.media.session.MediaSession.Callback受控端回调,可以接受到控制端的指令
客户端android.media.session.MediaController控制端
android.media.session.MediaController.TransportControls控制端的控制器,用于发送指令
android.media.session.MediaController.Callback控制端回调,可以接受到受控端的状态
android.media.browse.MediaBrowser.SubscriptionCallback订阅信息回调

客户端调用服务端

意义TransportControlsMediaSession.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)内置的评分系统有星级、红心、赞/踩、百分比

服务端回调给客户端

意义MediaSessionMediaController.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)
  • 24
    点赞
  • 129
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值