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

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

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

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

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

UI 控制音樂播放,更新進度

關於音樂播放器的開發,官方在 5.0 以上提供的 MediaSession 框架來更方便完成音樂相關功能的開發。github

大體流程是:瀏覽器

分爲 UI 端和 Service 端。UI 端負責控制播放,暫停等操做,經過 MediaController 進行信息傳遞到 Service 端。服務器

Service 進行相關指令的處理,並將播放狀態(歌曲信息, 播放進度)經過MediaSession 回傳給 UI 端,UI 端更新顯示。網絡

如上圖顯示:(圖片不能查看請移步:github.com/yunshuipiao…session

UI 界面上半部分是播放狀態,中間部分是歌曲列表,下半部分是控制器。其中 加載歌曲 模擬從不一樣渠道獲取播放列表。app

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 端實現。ide

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

相關文章
相關標籤/搜索