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

相关文章
相关标签/搜索