MediaSession框架

MediaSession框架

一、MediaSession框架

二、BlueTooth蓝牙播放信息显示

三、MediaButton响应优先级

四、锁屏显示

五、播放优化


MediaSession

1. added in API level 21 (Android 5.0)
2. 允许与媒体控制器、音量键、媒体按钮和传输控制交互。
3. 一般来说,一个应用程序只需要一个会话来进行所有回放,尽管可以创建多个会话来提供更好的媒体控制。
4. 线程安全。
Android 5.0更新了新的媒体播放API和媒体类型通知,使用最新的API可以让系统界面能够读取你的媒体播放并提取和显示专辑封面。

比如在Lolippop上,播放音乐时锁屏界面背景会变成专辑封面,并且还有播放控制按钮。

媒体回放控制
使用新增的通知和媒体 API 可确保系统 UI 了解您的媒体回放情况,并可提取和显示专辑封面。现在,可以利用新增的 MediaSession 类和 MediaController 类更轻松地在整个 UI 和服务范围内控制媒体回放。

新增的 MediaSession 类替代了弃用的 RemoteControlClient 类,仅提供一套回调方法来处理传输控制和媒体按钮。如果您的应用提供媒体回放,并运行在 Android TV 或 Wear 平台上,请使用 MediaSession 类,通过同样的回调方法来处理您的传输控制。

现在,您可以使用新增的 MediaController 类开发自己的媒体控制器应用。该类可通过您的应用的 UI 进程,以线程安全方式监控和控制媒体回放。请在创建控制器时指定一个 MediaSession.Token 对象,以便您的应用可与给定 MediaSession 交互。您可以利用 MediaController.TransportControls 方法,通过发送 play()、stop()、skipToNext() 和 setRating() 等命令来控制该会话上的媒体回放。对于控制器,您还可以注册一个 MediaController.Callback 对象来侦听该会话上的元数据和状态变化。

此外,您还可以利用新增的 Notification.MediaStyle 类创建允许将回放控制与媒体会话绑定的丰富通知。

媒体控件和 RemoteControlClient
RemoteControlClient 类现已弃用。请尽快切换到新的 MediaSession API。
Android 5.0 中的锁定屏幕不会为 MediaSession 或 RemoteControlClient 显示传输控件。不过,您的应用可以通过一个通知从锁定屏幕提供媒体播放控件。这让您的应用可以对媒体按钮的显示进行更多控制,同时为使用锁定设备和未锁定设备的用户提供一致的体验。

为实现此目的,Android 5.0 引入了一个新的 Notification.MediaStyle 模板。Notification.MediaStyle 将您使用 Notification.Builder.addAction() 添加的通知操作转换为精简按钮,嵌入到应用的媒体播放通知中。将您的会话令牌传递到 setSession() 方法以告知系统该通知控制进行中的媒体会话。

请务必将通知的可见性设为 VISIBILITY_PUBLIC,以将通知标记为安全,从而显示在任何锁定屏幕上(以安全方式或其他方式)。如需了解详细信息,请参阅锁定屏幕通知。

要让应用在 Android TV 或 Wear 平台上运行时显示媒体播放控件,则实现 MediaSession 类。如果您的应用需要在 Android 设备上接收媒体按钮事件,您还应实现 MediaSession。

MediaSession、MediaController详细使用参看谷歌官方例子
https://github.com/googlesamples/android-UniversalMusicPlayer
一共用到四个重要的东西
MediaSession
SessionToken
MediaSessionService    AndroidSDK\sources\android-23\com\android\server\media\MediaSessionService.java

MediaController

MediaSession 媒体会话
官翻:
一个会话,一般是涉及两方或以上;

在使用MediaSession的情况下,一般有受控端(一个)和控制端(可以有多个)。


三个重要参数
Token:     表示一个正在进行的会话。允许创建一个MediaController与之交流 (用于做匹配识别的参数)。
             new MediaSession()之后,对象便有了token,getSessionToken()获取。
Callback: 接收媒体按键、来自MediaController传输控制、系统的命令等,外部使用setCallback()设置到mediasession里面去。

QueueItem: 它是mediasession里面播放队列Queue的其中一个元素。有描述字段、Id、可序列化。

MediaSession框架_第1张图片

MediaSession 简单使用
MediaSessionCompat mSession;
mSession = new MediaSessionCompat(this, "MusicService");
mSession.setCallback(new MediaSessionCompat.Callback());
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
                MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
//FLAG_HANDLES_MEDIA_BUTTONS 控制媒体按钮
//FLAG_HANDLES_TRANSPORT_CONTROLS 控制传输命令
mSession.setActive(true); //激活
mSession.release(); //退出时需要销毁
更新播放状态和歌曲信息
mSession.setPlaybackState(state); //最重要
mSession.setMetadata(MediaMetadataCompat  metadata);
//media_session的服务,可通过命令查看系统当前的服务信息 adb shell dumpsys media_session

MediaMetadataCompat 
包含有关项的媒体元数据,如标题、艺术家等。
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)                
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)   
                .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs)              
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
                .putString(MusicProviderSource.CUSTOM_METADATA_TRACK_SOURCE, source)
                .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl)                
                .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber)
                .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount)

                .build();

MediaController 
官翻:是一个允许APP与MediaSession相互配合使用,可以把媒体按钮事件、一些命令发给MediaSession。
构造函数:MediaController(Context context, MediaSession.Token token)
三个重要参数
TransportControls:  用来控制mediasession播放的接口,允许APP发送media transport commands给mediasession。
Callback: 接收mediasession的回调,外部使用registerCallback ()设置。
PlaybackInfo:   存放当前播放的信息,很少使用。
TransportControls里面有很多方法 :
play()、pause()、stop()、seekTo()、skipToNext()、skipToPrevious()等等

创建MediaController:
MediaControllerCompat mediaController = new MediaControllerCompat(context, token);

mediaController.registerCallback(mMediaControllerCallback);

MediaController 与 MediaSession 多对一关系
开始播放某一首歌:
mediaController.getTransportControls().playFromMediaId(item.getMediaId(), null);
调用MediaController中的mSessionBinder.playFromMediaId(mediaId, extras)方法。
TransportControls中的play、pause等方法全都是调用mSessionBinder对应的方法。
那么mSessionBinder ?
mSessionBinder = token.getBinder();
所以不管MediaController有几个对象,
只要token相同,

就保证是操作的同一个MediaSession 。

MediaSession框架_第2张图片

MediaSession构造函数
mCbStub = new CallbackStub(this);  
MediaSessionManager manager = (MediaSessionManager) context
        .getSystemService(Context.MEDIA_SESSION_SERVICE);
try {
        mBinder = manager.createSession(mCbStub, tag, userId);
        mSessionToken = new Token(mBinder.getController());
        mController = new MediaController(context, mSessionToken);
} catch (RemoteException e) {
        throw new RuntimeException("Remote error creating session.", e);
}
createSession最终调用MediaSessionService的createSessionLocked方法创建了一个MediaSessionRecord叫做session。
然后mAllSessions.add(session);    //mAllSessions = new ArrayList()
然后mPriorityStack.addSession(session);    //mPriorityStack = new MediaSessionStack()   栈
manager.createSession()最终返回的是MediaSessionRecord中的ISession回调类。

那么MediaController里面的mSessionBinder其实就是MediaSessionRecord中的ISessionController回调类。

MediaController与MediaSession相互关系
一、MediaController操作,会调至MediaSession.Callback
mediaController.getTransportControls().playFromMediaId(item.getMediaId(), null);
mSessionBinder.playFromMediaId(mediaId, extras);
对应调用MediaSessionRecord中ControllerStub类的playFromMediaId()方法。
然后调用mCb.onPlayFromMediaId(mediaId, extras);
其实就是MediaSession中的mCbStub.onPlayFromMediaId(mediaId, extras);
进而调用MediaSession中Callback的onPlayFromMediaId方法。

二、MediaSession操作,会调至MediaController.Callback
mediaController.registerCallback(mMediaControllerCallback);
通过调用MediaSessionRecord中ControllerStub类的registerCallbackListener()方法

注册到MediaSessionRecord中mControllerCallbacks里面。 

//ArrayList mControllerCallbacks

当MediaSession调用setPlaybackState方法时,其实调用了MediaSessionRecord中SessionStub类的setPlaybackState方法。
里面作了2件事情:
1.mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState);  //调用MediaSessionService更新栈

2.mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);  //发消息回调给所有的MediaController.Callback

MediaSession框架_第3张图片

MediaSessionService更新栈(5.0代码)
首先看一下MediaSessionRecord中的setPlaybackState()方法:
mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState);
1.  updateSessions = mPriorityStack.onPlaystateChange(record, oldState, newState);
首先判断shouldUpdatePriority(oldState, newState) 是否应该更新优先级
如果需要,就把它放到ArrayList的第0个位置,也就是置于栈顶。
2.  如果更新成功,调用pushSessionsChanged()
(1)  rememberMediaButtonReceiverLocked() 
    //取活动栈的顶端的栈,第0个, 保持MediaButtonReceiver
    // Remember media button receiver and keep it in the persistent storage.
    // This should be called whenever there's no media session to receive media button event
(2)  pushRemoteVolumeUpdateLocked(userId);

(3)  取出mSessionsListeners里面的元素record.mListener.onActiveSessionsChanged(tokens);

UniversalMusicPlayer播放流程
mediaController.getTransportControls().playFromMediaId(item.getMediaId(), null);
mSessionBinder.playFromMediaId(mediaId, extras);
mSession.setQueue(newQueue);
//requestAudioFocus 等等、注册噪音变化广播等
//使用Exoplayer.play()    https://github.com/srMarlins/SimpleExoPlayer
mExoPlayer.prepare(mediaSource);

已经开始播放之后
mSession.setActive(true);
mSession.setPlaybackState(newState);

mSession.setMetadata(metadata); //更新歌曲信息

二、BlueTooth蓝牙播放显示
MediaSession框架_第4张图片

AVRCP: Audio Video Remote Control Profile    音频/视频远程控制配置文件 
链接    https://en.wikipedia.org/wiki/List_of_Bluetooth_profiles

主要功能:
  通过蓝牙耳机(比如索尼SBH50)或车载控制台控制手机上音乐播放
  在蓝牙耳机或车载控制台上显示手机上音乐播放的状态,歌名,歌手等信息
  在蓝牙耳机或车载控制台上浏览手机上的音乐文件,显示播放列表
版本:1.0、1.3、1.4、1.5、1.6

https://developer.android.com/about/versions/jelly-bean.html#43-profiles
Android官网说了,Android 4.3 添加了对 Bluetooth AVRCP 1.3的支持,
可以通过4.0新增的remote control client APIs 使用。

应用程序可以传输track name, composer, and other types of media metadata.

KUGOU蓝牙播放显示
RemoteControlClient
added in API level 14,  Deprecated since API level 21, Use MediaSession instead.
官译:能够给那些有能显示metadata,artwork和media transport control buttons提供信息的。
目前使用的RemoteControlClient。
RemoteControlClient是和媒体按钮事件receiver紧密相连的,
所以一定要注册在RemoteControlClient被注册到AudioManager之前,先注册mediabuttonReceiver到AudioManager中。
主要使用就两个方法:
1.  mRemoteControlClient.setPlaybackState(state);
2.  MetadataEditor metaData = mRemoteControlClient.editMetadata(true);
     metaData.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,  “老司机之歌”);
     ......

     metaData.apply();

Android 5.0以下蓝牙播放显示(以4.3为例)  一
AudioManager.registerRemoteControlClient(mRemoteControlClient);
实则 AudioService.registerRemoteControlClient();

registerRemoteControlClient:
AudioService里面有个mRcDisplays栈,registerRemoteControlDisplay的时候
会把mRcDisplays栈里面每个对象的IRemoteControlClient添加到需要注册的mRemoteControlClient里面,
保存在mRemoteControlClient内部的mRcDisplays栈。

注:AudioService中mRcDisplays栈存储的是所有注册进来的IRemoteControlDisplay对象,
         通过AudioManager.registerRemoteControlDisplay()设置进来的。

目前知道:
1. com.android.bluetooth.a2dp.Avrcp.java的start()方法中有调用
    mAudioManager.registerRemoteControlDisplay(mRemoteControlDisplay);
2. 原生Android中,锁屏界面相关的UI由KeyguardHostView提供
    com.android.internal.policy.impl.keyguard.KeyguardUpdateMonitor.handleBootCompleted() 
    mAudioManager.registerRemoteControlDisplay(mRemoteControlDisplay);
    //after the system has finished booting

    在KeyguardUpdateMonitor收到Intent.ACTION_BOOT_COMPLETED广播以后,注册进去。

Android 5.0以下蓝牙播放显示(以4.3为例)  二
调用RemoteControlClient.setPlaybackState(state);
1. 接着调用sendPlaybackState_syncCacheLock()通知元对象Avrcp.java里面的mRemoteControlDisplay.setPlaybackState()
完成蓝牙的播放状态显示。
2. KeyguardUpdateMonitor回调KeyguardHostView中的onMusicPlaybackStateChanged方法。

启动一个线程执行showAppropriateWidgetPage(),........


还有就是一些手机无论如何都不显示,通过日志查看以及反编译QQ音乐,得到结果:
Intent mediaIntent =newIntent("com.android.music.metachanged");
mediaIntent.putExtra("artist","歌手名称");
mediaIntent.putExtra("track","歌曲名称");
mediaIntent.putExtra("album","专辑名称");
mediaIntent.putExtra("duration", (Long) duration));
mediaIntent.putExtra("playing", (boolean) playing); //播放状态

getContext().sendBroadcast(mediaIntent);

Android 5.0以上蓝牙播放显示(以5.0为例)  一

前面不是说了RemoteControlClient废弃了吗?
怎么用起来没问题啊。


AudioManager.registerRemoteControlClient(mRemoteControlClient);
rcClient.registerWithSession(MediaSessionLegacyHelper.getHelper(mContext));
往mRemoteControlClient里面注册了一个MediaSessionLegacyHelper单例helper。

mRemoteControlClient将helper.addRccListener(mRcMediaIntent, mTransportListener);
addRccListener方法中创建对应mRcMediaIntent的MediaSession对象。

mRemoteControlClient得到了helper中对应PendingIntent的MediaSession对象,保存在mSession。

结果还是用的MediaSession。
调用RemoteControlClient.setPlaybackState(state);

恰是调用了mSession.setPlaybackState


Android 5.0以上蓝牙播放显示(以5.0为例)  二
MediaSession与AVRCP
Avrcp.start()过程:          //com.android.bluetooth.avrcp.Avrcp.java
先将一个构建了mRemoteController类,定义了一个监听RemoteControlClient的mRemoteControllerCb,
然后AudioManager.registerRemoteController(),实际执行了mRemoteController.startListeningToSessions()
startListeningToSessions:
1. 将Avrcp里面的mRemoteControllerCb监听和mRemoteController里面的mSessionListener监听,
     一同通过MediaSessionManager.addOnActiveSessionsChangedListener(),设置进入MediaSessionManager,然后构成2个新的监听,
     将2个新监听设置到MediaSessionService,创建一个SessionsListenerRecord的record对象,
     并将record对象添加到MediaSessionService中mSessionsListeners列表里面。
2.  mSessionListener.onActiveSessionsChanged(mSessionManager.getActiveSessions(listenerComponent));
      立刻去获取本进程的mediasession列表,构建List
      直接回调自己内部的onActiveSessionsChanged(List controllers),

      然后updateController(controller)。 //这里其实并不会显示歌曲内容,因为avrcp里面的metadata是空的

MediaSession框架_第5张图片

Android 5.0以上蓝牙播放显示(以5.0为例)  三
MediaSession与AVRCP
调用mSession.setPlaybackState,看看发生了什么?
直接进入MediaSessionRecord,执行setPlaybackState()方法,
1.  mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState)
     详见15页
     15页中,2.(3)有这些操作:
     (1) 取出mSessionsListeners里面的元素record, 回调MediaSessionManager中onActiveSessionsChanged(),           
           然后回调RemoteController里面的onActiveSessionsChanged()
     (2) RemoteController里面的updateController(controller); //controller即是MediaController
           执行mCurrentSession.registerCallback(),给controller注册回调,加入MediaController的mCallbacks列表中,
           然后将MediaController自己本地的回调mCbStub,注册到MediaSessionRecord中, 添加到mControllerCallbacks
           然后发消息MSG_NEW_PLAYBACK_STATE、MSG_NEW_MEDIA_METADATA,去更新onNewPlaybackState(),onNewMediaMetadata() 
2.  mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
     (1) 发消息执行pushPlaybackStateUpdate();
     (2) 取出mControllerCallbacks,调用onPlaybackStateChanged(), 传回至MediaController
     (3) 发消息调用mCallback.onPlaybackStateChanged((PlaybackState) msg.obj) 传回至RemoteController,并执行onNewPlaybackState()

     (4) 然后更新Avrcp里面的onClientPlaybackStateUpdate()

Android 5.0以上蓝牙播放显示(以5.0为例)  四
MediaSession与AVRCP
同理:
MetadataEditor metaData = mRemoteControlClient.editMetadata(true);//创建一个新的MetadataEditor
metaData.putString(MediaMetadataRetriever.METADATA_KEY_TITLE,  “老司机之歌”);
......
metaData.apply();
调用metaData.apply()即是调用了mSession.setMetadata(mMediaMetadata);,
1.  跳至MediaSessionRecord.pushMetadataUpdate()中
2.  取出mControllerCallbacks中的元素,回调mediacontroller里面的onMetadataChanged(mMetadata),
3.  发消息调用mCallback.onPlaybackStateChanged((PlaybackState) msg.obj) 传回至RemoteController,
     并执行onNewMediaMetadata(MediaMetadata metadata)
4.  然后更新Avrcp中onClientMetadataUpdate()去更新Metadata.

但是Avrcp并没有直接将metaData设置,而是通过registerNotificationRspTrackChangeNative()方法

使JNI间接调用Avrcp的getElementAttr()方法得到trackTitle、artist、albumTitle、duration.

Android 5.0以上蓝牙播放显示(以5.0为例)  五
问题反馈:车载蓝牙没有更新歌曲信息?

没有进行setPlaybackState(),明明已经更新了Metadata,

为什么不显示?原因:
1. 因为第一次MediaSessionRecord中mControllerCallbacks是没有元素的,
     没有回调mediacontroller里面的onMetadataChanged(mMetadata)。

2. metaData.clear();

三、MediaButton响应优先级

MediaSession框架_第6张图片
酷狗一个一直存在的问题:
不管我最后一次播的是QQ音乐、还是酷狗,此时点击线控耳机的按钮,总是响应QQ音乐。

现有注册mediabutton方式:AudioManager.registerMediaButtonEventReceiver();
谷歌官方说了,要用MediaSession.setMediaButtonReceiver(PendingIntent mbr)做替换。

原因:酷狗的MediaSession没有处于栈顶。
解决方案:MediaSession.setPlaybackState(state)进行更新到栈顶。

但是经过观察日志分析:QQ正在播放的时候,点击酷狗播放以后,此时点击mediabutton,竟然响应的还是QQ音乐。
QQ音乐在播放时,被动变为暂停的时候(例如收到音频焦点丢失的时候),会将mediasession.release(), 然后重新创建。

重新创建的时候更新NONE -> PLAYING -> PAUSE, 又把它更新到栈顶了。

四、锁屏显示

MediaSession框架_第7张图片

D:\AndroidSDK\sources\android-21\com\android\keyguard\KeyguardTransportControlView.java

onAttachedToWindow()方法中mAudioManager.registerRemoteController(mRemoteController);

与Avrcp的方式一模一样的,这里不再重复解释。

MediaSession框架_第8张图片

五、播放优化
MediaSession框架_第9张图片

(1) 获取播放进度
不断地调用getPosition(),  这是最普通也是最准确的方法,但是牺牲了跨进程通信性能。
在MediaSession框架中:
不需要去实时获取播放进度,当然前提是播放状态是准确的。

我们来看看PlaybackState.Builder的setState方法:
setState(@State int state, long position, float playbackSpeed)
setState(@State int state, long position, float playbackSpeed, long updateTime)

position:当前播放歌曲的实际进度
playbackSpeed:一般默认是1.0,播放流的速度
updateTime:SystemClock.elapsedRealtime() 返回的自启动以来的经历的毫秒数

前台的更新:
如果state是playing:   currentPosition = (SystemClock.elapsedRealtime() - updateTime) * playbackSpeed + position;

其他:currentPosition  = position;

(2) 获取播放时长
酷狗:不断地调用getduration(),  这是最普通也是最准确的方法,但是牺牲了跨进程通信性能。
在MediaSession框架中:
不需要去实时跨进程获取播放进度,还是通过MediaSession.setMetadata(@Nullable MediaMetadata metadata)。

将信息更新到前台。

(3) 前台获取歌手名、歌曲名、专辑名、队列数目

(4) 前台用MediaController.getTransportControls().play、pause、stop、seekTo方式替换现有的跨进程调用

六、通知栏置顶问题(只有我的手机有问题)

酷狗一直存在的通知栏问题:
不管我最后一次播的是QQ音乐、还是酷狗,酷狗通知栏永远都是在下边。

现有注册mediabutton方式:
AudioManager.registerMediaButtonEventReceiver(componentMB);
谷歌官方说了,要用MediaSession.setMediaButtonReceiver(PendingIntent mbr)做替换。

原因:
酷狗的MediaSession没有处于栈顶。

解决方案:

MediaSession.setPlaybackState(state)进行更新到栈顶。

MediaSession框架_第10张图片


你可能感兴趣的:(android,java)