We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[TOC]
最近完成了项目中关于音乐播放器开发相关的内容,之后又花了两天进行总结,特此记录。
另一方面,音乐播放器也同时用到了 Android 四大组件,对于刚接触 Android 开发的人来说也是值得去学习开发的一个功能。部分内容可能不会说的太详细。
关于音乐播放器的开发,官方在 5.0 以上提供的 MediaSession 框架来更方便完成音乐相关功能的开发。
大致流程是:
分为 UI 端和 Service 端。UI 端负责控制播放,暂停等操作,通过 MediaController 进行信息传递到 Service 端。
Service 进行相关指令的处理,并将播放状态(歌曲信息, 播放进度)通过MediaSession 回传给 UI 端,UI 端更新显示。
如上图显示:
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 端如何获取播放列表并传给 Service 播放。
UI 端通过如下函数模拟从网络获取播放列表。
fun getNetworkPlayList() { val playList = MusicLibrary.getMusicList() playList.forEach { mMediaControllerCompat.addQueueItem(it.description) } }
并通过 播放控制器添加到 Service。
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 对播放列表进行去重,播放歌曲下标设置。
通过 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
The text was updated successfully, but these errors were encountered:
MusicService
Sorry, something went wrong.
赞👍
No branches or pull requests
简易音乐播放器开发实录
[TOC]
最近完成了项目中关于音乐播放器开发相关的内容,之后又花了两天进行总结,特此记录。
另一方面,音乐播放器也同时用到了 Android 四大组件,对于刚接触 Android 开发的人来说也是值得去学习开发的一个功能。部分内容可能不会说的太详细。
需求:音乐播放器具有的功能
UI 控制音乐播放,更新进度
关于音乐播放器的开发,官方在 5.0 以上提供的 MediaSession 框架来更方便完成音乐相关功能的开发。
大致流程是:
分为 UI 端和 Service 端。UI 端负责控制播放,暂停等操作,通过 MediaController 进行信息传递到 Service 端。
Service 进行相关指令的处理,并将播放状态(歌曲信息, 播放进度)通过MediaSession 回传给 UI 端,UI 端更新显示。
如上图显示:
UI 界面上半部分是播放状态,中间部分是歌曲列表,下半部分是控制器。其中 加载歌曲 模拟从不同渠道获取播放列表。
UI 部分使用 ViewModel + livedata 实现,如下:
下面主要来看一下加载歌曲, 播放暂停是如何进行控制的,主要的逻辑在 ViewModel 端实现。
ViewModel 的相关对象:
相关信息看注释,流程会逐步介绍。
初始化
先初始化 MedaBrowserCompat, 对 Service 发出连接指令。连接成功之后 Service 进行初始化。
Service 的相关内容如下:
上面了解了整个音乐播放器分别在 UI 端和 Service 端的相关对象。
继续初始化过程,连接成功之后,Service 会进行初始化工作。
这是 UI 端 MediaBrowser 的工作。UI 端会收到连接成功的回调。
代码如上,连接成功之后会初始化 MediaController, 设置监听回调。MediaBrowser 并订阅 Service 端的播放列表。
上面有两个参数,其中 root 是:当 Service 初始化成功时, Service端 会实现两个方法:
onGetRoot 方法提供 root。订阅之后 onLoadChildren 会将当前播放列表发送出去,这时 UI 端在 媒体浏览器就能收到当前 Service 的播放列表数据。
因为这时播放列表为空,所以 UI 端接收到的播放列表也为空。
因为 MediaSession 支持多个 UI 端接入。比如 UI 端 A 设置了播放列表,此时 UI 端 B 进行连接,则可以获取当前的播放列表进行操作。
总结:UI 端 和 Service 端 的初始化过程
设置播放列表
在出初始化的过程中,播放列表为空。下面介绍 UI 端如何获取播放列表并传给 Service 播放。
UI 端通过如下函数模拟从网络获取播放列表。
并通过 播放控制器添加到 Service。
Service 端收到播放列表添加的回调:
上面根据 mediaId 对播放列表进行去重,播放歌曲下标设置。
通过 Session.setQueue() 设置播放列表, UI 端获取回调,更新播放列表。
后面就是 livedata 将数据通知到 UI 端,进行列表更新。
这里解释一下,为什么在 UI 端获取到播放列表之后,不直接更新UI:
因为获取播放列表,传到Service 之后可能会失败,造成歌曲不可播放。
这也符合响应式的操作:UI 发出 Action -> 处理Action -> UI 收到 Action 造成的状态改变,更新 UI。
UI 端不应该在操作之后主动更新。后面的播放暂停也是这个做法。
播放暂停
有了设置播放列表的前提,下面接着进行播放暂停的相关流程介绍。
UI端通过 mediaController 发出播放歌曲的指令 -> Service 端收到指令,切换歌曲播放 -> 通过 MediaSession 将播放状态信息传回 UI 端 -> UI 端进行更新。
UI 端: 如果当前播放状态是正在播放,则发送暂停播放的指令;反之,则发送播放的指令。
Service端:收到播放指令后,当前播放歌曲为空,进行播放前处理,准备资源。如果此时当前歌曲还是为空(比如没有播放列表时点击播放),则返回。否则进行播放。
这里获取到当前需要播放的歌曲,使用 MediaPlayer 进行加载准备。准备完成之后:
获取到当前播放的歌曲信息,MediaSession 通过 setMetaData() 发送到客户端,进行UI 更新。
准备完成之后会再次进行播放。回到上面的代码,此时 MediaSession 会将 播放状态 通过 setNewState() 发送到客户端,进行 UI 更新。
这里的播放状态包括四个参数,是否正在播放,当前进度,播放速度,最近更新时间(用过UI播放进度更新)。
UI 端收到 MediaMession 的歌曲信息,进行 UI 更新。
这里也可以看到,如果 UI 端需要显示进度条,但是 MediaSession 并不会一直回传进度给 UI 端。
这是使用 handle 执行定时循环任务,去通过计算得到当前的进度,注意 handler 的处理,防止内存泄漏。
以上就是整个音乐播放器的初始化,播放暂停的过程。
前台通知保持音乐播放
由于 Service 在退到后台之后会被销毁,音乐就会停止播放。后面介绍使用前台通知的方式,在通知栏显示播放信息及控制按钮,防止 Service 被销毁;并在锁屏界面也支持控制播放。
在切换不同播放状态的基础上,创建并启动通知。
根据当前的状态,播放状态则启动前台服务,并显示通知在通知栏上(包括锁屏通知)
暂停状态则更新通知的显示,更新相关按钮。相关代码参考 MediaNotificationManager 文件。
音频焦点的处理
当播放器 A 在播放音乐,此时其他到播放器播放音乐,此时两个音乐播放器都会在播放,涉及音频焦点的处理。
当耳机拔出时,也要暂停音乐的播放。
回到 onPlay 方法,在播放一首歌之前, 需要主动去获取音频的焦点,有了音频焦点才能播放(其他播放器失去音频焦点暂停音乐播放)。
在请求音频焦点的时候,注册广播接收器,可以在耳机拨出时收到广播,暂停音乐播放。
在请求音频焦点时传入了接口,可以在音频焦点变化时改变播放状态。
线控模式
当耳机连接时,通过耳机上的按钮也要控制音乐的播放。
在耳机上的按钮按下时,Service 端会收到回调。
这个方法有默认实现,包括通知栏的按钮,耳机的按钮。默认实现是:音量加减,单击暂停,单机播放, 双击下一曲。返回值为 true 表示按钮事件被处理。因此可以通过重写该方法满足线控的相关要求。
这里判断如果是耳机按钮的操作,则统计800毫秒内按钮按了几次,来实现自己的线控模式。
总结
到目前为止,已经实现了文章开头说的几个音乐播放器具有的功能,使用到了 MediaSession 来作为 UI端 和 Service 端通信的基础(底层Binder)。
重点在于理解 MediaSession 相关对象的作用及使用,才能更容易的理解播放器的通信机制。
源码:github
The text was updated successfully, but these errors were encountered: