最近完成了專案中關於音樂播放器開發相關的內容,之後又花了兩天進行總結,特此記錄。
另一方面,音樂播放器也同時用到了 Android 四大元件,對於剛接觸 Android 開發的人來說也是值得去學習開發的一個功能。部分內容可能不會說的太詳細。
需求:音樂播放器具有的功能
- 音樂後臺播放(Service),UI 顯示進度,歌曲資訊
- 音樂播放通知和鎖屏通知,可操作(播放,暫停,上下一曲)
- 音訊焦點的處理(其他音樂播放器播放時相關狀態更新)
- 耳機線控模式的處理
UI 控制音樂播放,更新進度
關於音樂播放器的開發,官方在 5.0 以上提供的 MediaSession 框架來更方便完成音樂相關功能的開發。
大致流程是:
分為 UI 端和 Service 端。UI 端負責控制播放,暫停等操作,通過 MediaController 進行資訊傳遞到 Service 端。
Service 進行相關指令的處理,並將播放狀態(歌曲資訊, 播放進度)通過MediaSession 回傳給 UI 端,UI 端更新顯示。
如上圖顯示:(圖片不能檢視請移步:github.com/yunshuipiao…
UI 介面上半部分是播放狀態,中間部分是歌曲列表,下半部分是控制器。其中 載入歌曲 模擬從不同渠道獲取播放列表。
UI 部分使用 ViewModel + livedata 實現,如下:
/**
* 上一首
*/
mf_to_previous.setOnClickListener {
viewModel.skipToPrevious()
}
/**
* 下一首
*/
mf_to_next.setOnClickListener {
viewModel.skipToNext()
}
/**
* 播放暫停
*/
mf_to_play.setOnClickListener {
viewModel.playOrPause()
}
/**
* 載入音樂
*/
mf_to_load.setOnClickListener {
viewModel.getNetworkPlayList()
}
複製程式碼
下面主要來看一下載入歌曲, 播放暫停是如何進行控制的,主要的邏輯在 ViewModel 端實現。
ViewModel 的相關物件:
class MainViewModel : ViewModel() {
private lateinit var mContext: Context
/**
* 播放控制器,對 Service 發出播放,暫停,上下一曲的指令
*/
private lateinit var mMediaControllerCompat: MediaControllerCompat
/**
* 媒體瀏覽器,負責連線 Service,得到 Service 的相關資訊
*/
private lateinit var mMediaBrowserCompat: MediaBrowserCompat
/**
* 播放狀態的資料(是否正在播放,播放進度)
*/
public var mPlayStateLiveData = MutableLiveData<PlaybackStateCompat>()
/**
* 播放歌曲的資料(歌曲,歌手等)
*/
public var mMetaDataLiveData = MutableLiveData<MediaMetadataCompat>()
/**
* 播放列表的資料
*/
public var mMusicsLiveData = MutableLiveData<MutableList<MediaDescriptionCompat>>()
/**
* 播放控制器的回撥
* (比如 UI 發出下一曲指令,Service 端切換歌曲播放之後,將播放狀態資訊傳回 UI 端, 更新 UI)
*/
private var mMediaControllerCompatCallback = object : MediaControllerCompat.Callback() {
override fun onQueueChanged(queue: MutableList<MediaSessionCompat.QueueItem>?) {
super.onQueueChanged(queue)
// 服務端的queue變化
MusicHelper.log("onQueueChanged: $queue" )
mMusicsLiveData.postValue(queue?.map { it.description } as MutableList<MediaDescriptionCompat>)
}
override fun onRepeatModeChanged(repeatMode: Int) {
super.onRepeatModeChanged(repeatMode)
}
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
super.onPlaybackStateChanged(state)
mPlayStateLiveData.postValue(state)
MusicHelper.log("music onPlaybackStateChanged, $state")
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
super.onMetadataChanged(metadata)
MusicHelper.log("onMetadataChanged, $metadata")
mMetaDataLiveData.postValue(metadata)
}
override fun onSessionReady() {
super.onSessionReady()
}
override fun onSessionDestroyed() {
super.onSessionDestroyed()
}
override fun onAudioInfoChanged(info: MediaControllerCompat.PlaybackInfo?) {
super.onAudioInfoChanged(info)
}
}
/**
* 媒體瀏覽器連線 Service 的回撥
*/
private var mMediaBrowserCompatConnectionCallback: MediaBrowserCompat.ConnectionCallback = object :
MediaBrowserCompat.ConnectionCallback() {
override fun onConnected() {
super.onConnected()
// 連線成功
MusicHelper.log("onConnected")
mMediaControllerCompat = MediaControllerCompat(mContext, mMediaBrowserCompat.sessionToken)
mMediaControllerCompat.registerCallback(mMediaControllerCompatCallback)
mMediaBrowserCompat.subscribe(mMediaBrowserCompat.root, mMediaBrowserCompatSubscriptionCallback)
}
override fun onConnectionSuspended() {
super.onConnectionSuspended()
}
override fun onConnectionFailed() {
super.onConnectionFailed()
}
}
/**
* 媒體瀏覽器訂閱 Service 資料的回撥
*/
private var mMediaBrowserCompatSubscriptionCallback = object : MediaBrowserCompat.SubscriptionCallback() {
override fun onChildrenLoaded(
parentId: String,
children: MutableList<MediaBrowserCompat.MediaItem>
) {
super.onChildrenLoaded(parentId, children)
// 伺服器 setChildLoad 的回撥方法
MusicHelper.log("onChildrenLoaded, $children")
}
}
複製程式碼
相關資訊看註釋,流程會逐步介紹。
初始化
fun init(context: Context) {
mContext = context
mMediaBrowserCompat = MediaBrowserCompat(context, ComponentName(context, MusicService::class.java),
mMediaBrowserCompatConnectionCallback, null)
mMediaBrowserCompat.connect()
}
複製程式碼
先初始化 MedaBrowserCompat, 對 Service 發出連線指令。連線成功之後 Service 進行初始化。
Service 的相關內容如下:
class MusicService : MediaBrowserServiceCompat() {
private var mRepeatMode: Int = PlaybackStateCompat.REPEAT_MODE_NONE
/**
* 播放狀態,通過 MediaSession 回傳給 UI 端。
*/
private var mState = PlaybackStateCompat.Builder().build()
/**
* UI 可能被銷燬,Service 需要儲存播放列表,並處理迴圈模式
*/
private var mPlayList = arrayListOf<MediaSessionCompat.QueueItem>()
/**
* 當前播放音樂的相關資訊
*/
private var mMusicIndex = -1
private var mCurrentMedia: MediaSessionCompat.QueueItem? = null
/**
* 播放會話,將播放狀態資訊回傳給 UI 端。
*/
private lateinit var mSession: MediaSessionCompat
/**
* 真正的音樂播放器
*/
private var mMediaPlayer: MediaPlayer = MediaPlayer()
/**
* 播放控制器的事件回撥,UI 端通過播放控制器發出的指令會在這裡接收到,交給真正的音樂播放器處理。
*/
private var mSessionCallback = object : MediaSessionCompat.Callback() {
....
}
複製程式碼
上面瞭解了整個音樂播放器分別在 UI 端和 Service 端的相關物件。
繼續初始化過程,連線成功之後,Service 會進行初始化工作。
override fun onCreate() {
super.onCreate()
mSession = MediaSessionCompat(applicationContext, "MusicService")
mSession.setCallback(mSessionCallback)
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS)
sessionToken = mSession.sessionToken
mMediaPlayer.setOnCompletionListener(mCompletionListener)
mMediaPlayer.setOnPreparedListener(mPreparedListener)
mMediaPlayer.setOnErrorListener { mp, what, extra -> true }
}
複製程式碼
這是 UI 端 MediaBrowser 的工作。UI 端會收到連線成功的回撥。
程式碼如上,連線成功之後會初始化 MediaController, 設定監聽回撥。MediaBrowser 並訂閱 Service 端的播放列表。
mMediaBrowserCompat.subscribe(mMediaBrowserCompat.root,mMediaBrowserCompatSubscriptionCallback)
複製程式碼
上面有兩個引數,其中 root 是:當 Service 初始化成功時, Service端 會實現兩個方法:
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
MusicHelper.log("onLoadChildren, $parentId")
result.detach()
val list = mPlayList.map { MediaBrowserCompat.MediaItem(it.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) }
result.sendResult(list as MutableList<MediaBrowserCompat.MediaItem>?)
}
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
return BrowserRoot("MusicService", null)
}
複製程式碼
onGetRoot 方法提供 root。訂閱之後 onLoadChildren 會將當前播放列表傳送出去,這時 UI 端在 媒體瀏覽器就能收到當前 Service 的播放列表資料。
因為這時播放列表為空,所以 UI 端接收到的播放列表也為空。
因為 MediaSession 支援多個 UI 端接入。比如 UI 端 A 設定了播放列表,此時 UI 端 B 進行連線,則可以獲取當前的播放列表進行操作。
總結:UI 端 和 Service 端 的初始化過程
- UI 端 通過 MediaBroswer 發出對 Service 的連線指令。
- Service 建立初始化,設定 token,進行 Service 的初始化工作。
- UI 端收到連線成功的回撥,對 MediaController 進行初始化,MediaBroswer 訂閱 Service 的播放列表資訊。Service 通過 onLoadChildren 將當前播放資訊傳回 UI 端。
- UI 端收到播放列表的資訊,進行 UI 更新,顯示播放列表。
設定播放列表
在出初始化的過程中,播放列表為空。下面介紹 UI 端如何獲取播放列表並傳給 Service 播放。
UI 端通過如下函式模擬從網路獲取播放列表。
fun getNetworkPlayList() {
val playList = MusicLibrary.getMusicList()
playList.forEach {
mMediaControllerCompat.addQueueItem(it.description)
}
}
複製程式碼
並通過 播放控制器新增到 Service。
- MediaMetadataCompat:UI 端播放列表的資料型別是 MediaMetadataCompat,包含了歌曲內容的全部資訊(歌名,歌手,播放uri,圖示等等)
- MediaDescriptionCompat: UI 端傳到 Service 的資料,是 MediaMetadataCompat 的部分內容,主要用於簡單資訊的展示。
Service 端收到播放列表新增的回撥:
override fun onAddQueueItem(description: MediaDescriptionCompat) {
super.onAddQueueItem(description)
// 客戶端新增歌曲
if (mPlayList.find { it.description.mediaId == description.mediaId } == null) {
mPlayList.add(
MediaSessionCompat.QueueItem(description, description.hashCode().toLong())
)
}
mMusicIndex = if (mMusicIndex == -1) 0 else mMusicIndex
mSession.setQueue(mPlayList)
}
複製程式碼
上面根據 mediaId 對播放列表進行去重,播放歌曲下標設定。
- QueueItem:播放列表的內容,裡面存有 MediaDescriptionCompat。
通過 Session.setQueue() 設定播放列表, UI 端獲取回撥,更新播放列表。
override fun onQueueChanged(queue: MutableList<MediaSessionCompat.QueueItem>?) {
super.onQueueChanged(queue)
// 服務端的queue變化
MusicHelper.log("onQueueChanged: $queue" )
mMusicsLiveData.postValue(queue?.map { it.description } as MutableList<MediaDescriptionCompat>)
}
複製程式碼
後面就是 livedata 將資料通知到 UI 端,進行列表更新。
viewModel.mMusicsLiveData.observe(this, Observer {
mMusicAdapter.setList(it)
})
public fun setList(datas: List<MediaDescriptionCompat>) {
mList.clear()
mList.addAll(datas)
notifyDataSetChanged()
}
複製程式碼
這裡解釋一下,為什麼在 UI 端獲取到播放列表之後,不直接更新UI: 因為獲取播放列表,傳到Service 之後可能會失敗,造成歌曲不可播放。
這也符合響應式的操作:UI 發出 Action -> 處理Action -> UI 收到 Action 造成的狀態改變,更新 UI。
UI 端不應該在操作之後主動更新。後面的播放暫停也是這個做法。
播放暫停
有了設定播放列表的前提,下面接著進行播放暫停的相關流程介紹。
UI端通過 mediaController 發出播放歌曲的指令 -> Service 端收到指令,切換歌曲播放 -> 通過 MediaSession 將播放狀態資訊傳回 UI 端 -> UI 端進行更新。
fun playOrPause() {
if (mPlayStateLiveData.value?.state == PlaybackStateCompat.STATE_PLAYING) {
mMediaControllerCompat.transportControls.pause()
} else {
mMediaControllerCompat.transportControls.play()
}
}
複製程式碼
UI 端: 如果當前播放狀態是正在播放,則傳送暫停播放的指令;反之,則傳送播放的指令。
override fun onPlay() {
super.onPlay()
if (mCurrentMedia == null) {
onPrepare()
}
if (mCurrentMedia == null) {
return
}
mMediaPlayer.start()
setNewState(PlaybackStateCompat.STATE_PLAYING)
}
複製程式碼
Service端:收到播放指令後,當前播放歌曲為空,進行播放前處理,準備資源。如果此時當前歌曲還是為空(比如沒有播放列表時點選播放),則返回。否則進行播放。
override fun onPrepare() {
super.onPrepare()
if (mPlayList.isEmpty()) {
MusicHelper.log("not playlist")
return
}
if (mMusicIndex < 0 || mMusicIndex >= mPlayList.size) {
MusicHelper.log("media index error")
return
}
mCurrentMedia = mPlayList[mMusicIndex]
val uri = mCurrentMedia?.description?.mediaUri
MusicHelper.log("uri, $uri")
if (uri == null) {
return
}
// 載入資源要重置
mMediaPlayer.reset()
try {
if (uri.toString().startsWith("http")) {
mMediaPlayer.setDataSource(applicationContext, uri)
} else {
// assets 資源
val assetFileDescriptor = applicationContext.assets.openFd(uri.toString())
mMediaPlayer.setDataSource(
assetFileDescriptor.fileDescriptor,
assetFileDescriptor.startOffset,
assetFileDescriptor.length
)
}
mMediaPlayer.prepare()
} catch (e: Exception) {
e.printStackTrace()
}
}
複製程式碼
這裡獲取到當前需要播放的歌曲,使用 MediaPlayer 進行載入準備。準備完成之後:
private var mPreparedListener: MediaPlayer.OnPreparedListener =
MediaPlayer.OnPreparedListener {
val mediaId = mCurrentMedia?.description?.mediaId ?: ""
val metadata = MusicLibrary.getMeteDataFromId(mediaId)
mSession.setMetadata(metadata.putDuration(mMediaPlayer.duration.toLong()))
mSessionCallback.onPlay()
}
複製程式碼
獲取到當前播放的歌曲資訊,MediaSession 通過 setMetaData() 傳送到客戶端,進行UI 更新。
準備完成之後會再次進行播放。回到上面的程式碼,此時 MediaSession 會將 播放狀態 通過 setNewState() 傳送到客戶端,進行 UI 更新。
private fun setNewState(state: Int) {
val stateBuilder = PlaybackStateCompat.Builder()
stateBuilder.setActions(getAvailableActions(state))
stateBuilder.setState(
state,
mMediaPlayer.currentPosition.toLong(),
1.0f,
SystemClock.elapsedRealtime()
)
mState = stateBuilder.build()
mSession.setPlaybackState(mState)
}
複製程式碼
這裡的播放狀態包括四個引數,是否正在播放,當前進度,播放速度,最近更新時間(用過UI播放進度更新)。
UI 端收到 MediaMession 的歌曲資訊,進行 UI 更新。
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
super.onPlaybackStateChanged(state)
mPlayStateLiveData.postValue(state)
MusicHelper.log("music onPlaybackStateChanged, $state")
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
super.onMetadataChanged(metadata)
MusicHelper.log("onMetadataChanged, $metadata")
mMetaDataLiveData.postValue(metadata)
}
viewModel.mPlayStateLiveData.observe(this, Observer {
if (it.state == PlaybackStateCompat.STATE_PLAYING) {
mf_to_play.text = "暫停"
mPlayState = it
mf_tv_seek.progress = it.position.toInt()
handler.sendEmptyMessageDelayed(1, 250)
} else {
mf_to_play.text = "播放"
handler.removeMessages(1)
}
})
viewModel.mMetaDataLiveData.observe(this, Observer {
val title = it.getString(MediaMetadataCompat.METADATA_KEY_TITLE)
val singer = it.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)
val duration = it.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
val durationShow = "${duration / 60000}: ${duration / 1000 % 60}"
mf_tv_title.text = "標題:$title"
mf_tv_singer.text = "歌手:$singer"
mf_tv_progress.text = "時長:$durationShow"
mMusicAdapter.notifyPlayingMusic(it.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID))
mf_tv_seek.max = duration.toInt()
})
viewModel.mMusicsLiveData.observe(this, Observer {
mMusicAdapter.setList(it)
})
複製程式碼
這裡也可以看到,如果 UI 端需要顯示進度條,但是 MediaSession 並不會一直回傳進度給 UI 端。
inner class SeekHandle: Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
var position = (SystemClock.elapsedRealtime() - mPlayState.lastPositionUpdateTime ) * mPlayState.playbackSpeed + mPlayState.position
mf_tv_seek.progress = position.toInt()
sendEmptyMessageDelayed(1, 250)
}
}
複製程式碼
這是使用 handle 執行定時迴圈任務,去通過計算得到當前的進度,注意 handler 的處理,防止記憶體洩漏。
以上就是整個音樂播放器的初始化,播放暫停的過程。
前臺通知保持音樂播放
由於 Service 在退到後臺之後會被銷燬,音樂就會停止播放。後面介紹使用前臺通知的方式,在通知欄顯示播放資訊及控制按鈕,防止 Service 被銷燬;並在鎖屏介面也支援控制播放。
在切換不同播放狀態的基礎上,建立並啟動通知。
sessionToken?.let {
val description = mCurrentMedia?.description ?: MediaDescriptionCompat.Builder().build()
when(state) {
PlaybackStateCompat.STATE_PLAYING -> {
val notification = mNotificationManager.getNotification(description, mState, it)
ContextCompat.startForegroundService(
this@MusicService,
Intent(this@MusicService, MusicService::class.java)
)
startForeground(MediaNotificationManager.NOTIFICATION_ID, notification)
}
PlaybackStateCompat.STATE_PAUSED -> {
val notification = mNotificationManager.getNotification(
description, mState, it
)
mNotificationManager.notificationManager
.notify(MediaNotificationManager.NOTIFICATION_ID, notification)
}
PlaybackStateCompat.STATE_STOPPED -> {
stopSelf()
}
}
}
複製程式碼
根據當前的狀態,播放狀態則啟動前臺服務,並顯示通知在通知欄上(包括鎖屏通知)
暫停狀態則更新通知的顯示,更新相關按鈕。相關程式碼參考 MediaNotificationManager 檔案。
音訊焦點的處理
當播放器 A 在播放音樂,此時其他到播放器播放音樂,此時兩個音樂播放器都會在播放,涉及音訊焦點的處理。
當耳機拔出時,也要暫停音樂的播放。
回到 onPlay 方法,在播放一首歌之前, 需要主動去獲取音訊的焦點,有了音訊焦點才能播放(其他播放器失去音訊焦點暫停音樂播放)。
override fun onPlay() {
super.onPlay()
if (mCurrentMedia == null) {
onPrepare()
}
if (mCurrentMedia == null) {
return
}
if (mAudioFocusHelper.requestAudioFocus()) {
mMediaPlayer.start()
setNewState(PlaybackStateCompat.STATE_PLAYING)
}
}
複製程式碼
fun requestAudioFocus(): Boolean {
registerAudioNoisyReceiver()
val result = mAudioManager.requestAudioFocus(
this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}
複製程式碼
在請求音訊焦點的時候,註冊廣播接收器,可以在耳機撥出時收到廣播,暫停音樂播放。
fun registerAudioNoisyReceiver() {
if (!mAudioNoisyReceiverRegistered) {
context.registerReceiver(mAudioNoisyReceiver, AUDIO_NOISY_INTENT_FILTER)
mAudioNoisyReceiverRegistered = true
}
}
fun unregisterAudioNoisyReceiver() {
if (mAudioNoisyReceiverRegistered) {
context.unregisterReceiver(mAudioNoisyReceiver)
mAudioNoisyReceiverRegistered = false
}
}
複製程式碼
在請求音訊焦點時傳入了介面,可以在音訊焦點變化時改變播放狀態。
override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
/**
* 獲取音訊焦點
*/
AudioManager.AUDIOFOCUS_GAIN -> {
if (mPlayOnAudioFocus && !mMediaPlayer.isPlaying) {
mSessionCallback.onPlay()
} else if (mMediaPlayer.isPlaying) {
setVolume(MEDIA_VOLUME_DEFAULT)
}
mPlayOnAudioFocus = false
}
/**
* 暫時失去音訊焦點,但可降低音量播放音樂,類似導航模式
*/
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> setVolume(MEDIA_VOLUME_DUCK)
/**
* 暫時失去音訊焦點,一段時間後會重新獲取焦點,比如鬧鐘
*/
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> if (mMediaPlayer.isPlaying) {
mPlayOnAudioFocus = true
mSessionCallback.onPause()
}
/**
* 失去焦點
*/
AudioManager.AUDIOFOCUS_LOSS -> {
mAudioManager.abandonAudioFocus(this)
mPlayOnAudioFocus = false
// 這裡暫停播放
mSessionCallback.onPause()
}
}
}
複製程式碼
線控模式
當耳機連線時,通過耳機上的按鈕也要控制音樂的播放。
在耳機上的按鈕按下時,Service 端會收到回撥。
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
return super.onMediaButtonEvent(mediaButtonEvent)
}
複製程式碼
這個方法有預設實現,包括通知欄的按鈕,耳機的按鈕。預設實現是:音量加減,單擊暫停,單機播放, 雙擊下一曲。返回值為 true 表示按鈕事件被處理。因此可以通過重寫該方法滿足線控的相關要求。
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
val action = mediaButtonEvent?.action
val keyevent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
val keyCode= keyevent?.keyCode
MusicHelper.log("action: $action, keyEvent: $keyevent")
return if (keyevent?.keyCode == KeyEvent.KEYCODE_HEADSETHOOK && keyevent.action == KeyEvent.ACTION_UP) {
//耳機單機操作
mHeadSetClickCount += 1
if (mHeadSetClickCount == 1) {
handler.sendEmptyMessageDelayed(1, 800)
}
true
} else {
super.onMediaButtonEvent(mediaButtonEvent)
}
}
複製程式碼
這裡判斷如果是耳機按鈕的操作,則統計800毫秒內按鈕按了幾次,來實現自己的線控模式。
inner class HeadSetHandler: Handler() {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
// 根據耳機按下的次數決定執行什麼操作
when(mHeadSetClickCount) {
1 -> {
if (mMediaPlayer.isPlaying) {
mSessionCallback.onPause()
} else {
mSessionCallback.onPlay()
}
}
2 -> {
mSessionCallback.onSkipToNext()
}
3 -> {
mSessionCallback.onSkipToPrevious()
}
4 -> {
mSessionCallback.onSkipToPrevious()
mSessionCallback.onSkipToPrevious()
}
}
}
}
複製程式碼
總結
到目前為止,已經實現了文章開頭說的幾個音樂播放器具有的功能,使用到了 MediaSession 來作為 UI端 和 Service 端通訊的基礎(底層Binder)。
重點在於理解 MediaSession 相關物件的作用及使用,才能更容易的理解播放器的通訊機制。
)