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 订阅信息回调

客户端调用服务端

意义 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)
发布了12 篇原创文章 · 获赞 36 · 访问量 4780

猜你喜欢

转载自blog.csdn.net/weixin_42229694/article/details/89315026