Android 音樂播放器開發實錄(MediaSession)

swensun發表於2019-11-10

最近完成了專案中關於音樂播放器開發相關的內容,之後又花了兩天進行總結,特此記錄。

另一方面,音樂播放器也同時用到了 Android 四大元件,對於剛接觸 Android 開發的人來說也是值得去學習開發的一個功能。部分內容可能不會說的太詳細。

需求:音樂播放器具有的功能

  1. 音樂後臺播放(Service),UI 顯示進度,歌曲資訊
  2. 音樂播放通知和鎖屏通知,可操作(播放,暫停,上下一曲)
  3. 音訊焦點的處理(其他音樂播放器播放時相關狀態更新)
  4. 耳機線控模式的處理

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 端 的初始化過程

  1. UI 端 通過 MediaBroswer 發出對 Service 的連線指令。
  2. Service 建立初始化,設定 token,進行 Service 的初始化工作。
  3. UI 端收到連線成功的回撥,對 MediaController 進行初始化,MediaBroswer 訂閱 Service 的播放列表資訊。Service 通過 onLoadChildren 將當前播放資訊傳回 UI 端。
  4. 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 相關物件的作用及使用,才能更容易的理解播放器的通訊機制。

原始碼:github

相關文章