任务二 使用 MediaPlayer 播放歌曲
服务绑定完成后,页面已经可以访问 MusicService。现在在服务中加入 MediaPlayer,建立从歌曲对象到音频地址再到播放器的处理流程。
一、任务目标
完成后,MusicService 将能够:
- 保存网络歌曲列表和当前歌曲索引。
- 根据
Song.musicPath设置音频数据源并异步准备播放器。 - 在准备完成后开始播放,并在服务销毁时释放
MediaPlayer。
二、创建播放器和歌曲列表
1. 添加 MediaPlayer 成员
打开 app/src/main/java/com/sziit/musicplayer/service/MusicService.kt,导入 MediaPlayer 和 Song,并在服务类中保存播放器、歌曲列表和当前索引。
// ... 省略 package 声明和已有导入。import android.media.MediaPlayerimport com.sziit.musicplayer.model.Song
class MusicService : Service() { private val binder = MusicBinder() // 服务持有播放器和歌曲数据,供绑定它的页面共同使用。 private val player = MediaPlayer() private val musicList = mutableListOf<Song>() // -1 表示尚未选中歌曲。 private var currentIndex = -1 // ... 省略服务中的其他成员和方法。}musicList 用于接收网络请求返回的歌曲数据。
2. 接收歌曲列表
在服务中添加 setMusicList(),由歌曲列表 Fragment 在接口请求成功后调用。
class MusicService : Service() { // ... 省略已有成员。
// 用最新请求结果替换服务中保存的歌曲列表。 fun setMusicList(list: List<Song>) { musicList.clear() musicList.addAll(list) // 记录收到的歌曲数量,便于在 Logcat 中验证数据传递。 Log.i(TAG, "MusicService: 歌曲列表已更新,共 ${musicList.size} 首") }
// ... 省略服务中的其他方法。}在 MusicListFragment 的在线请求成功回调中,将歌曲列表同步给服务。
class MusicListFragment : Fragment() { // ... 省略已有字段和其他生命周期方法。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // ... 省略 RecyclerView 初始化代码。 MusicApi.fetchSongs( onSuccess = { songs -> // 将歌曲数据交给服务,再在主线程更新列表。 musicService?.setMusicList(songs) binding.root.post { adapter.setData(songs) } }, onFailure = { message -> // ... 保留已有的失败处理。 } ) }}运行应用,在 Logcat 搜索框中输入 tag:MusicLifecycle 并按 Enter。截图中最后一行显示 MusicService 已收到 16 首歌曲,说明列表数据已传入服务;实际数量以接口返回结果为准。
列表界面仍应正常显示;点击歌曲暂时不会播放,播放入口会在下一任务中接入。
页面负责显示,服务保存播放所需的歌曲数据。
三、根据歌曲地址准备播放
1. 校验歌曲并设置音频地址
先在 MusicService 中添加 playImpl(),根据歌曲索引取得歌曲,检查音频地址,再重置播放器并设置新的数据源。
class MusicService : Service() { // ... 省略已有成员和其他方法。
private fun playImpl(index: Int) { // 索引或音频地址无效时,不继续操作播放器。 val song = musicList.getOrNull(index) ?: return val url = song.musicPath ?: return
try { // 切歌前清除旧状态,再设置新的音频地址。 player.reset() player.setDataSource(url) } catch (e: Exception) { // 打印设置数据源或播放器操作时发生的异常。 e.printStackTrace() } }
// ... 省略服务中的其他方法。}索引无效或歌曲没有音频地址时,方法直接结束;切换歌曲前调用 reset(),避免复用播放器的旧状态。
2. 添加播放入口和索引查找
play() 接收列表中的 Song 对象,根据歌曲 id 查找它在服务列表中的位置,再把索引交给上一步实现的 playImpl()。缺少 id 或未找到歌曲时,直接结束。
class MusicService : Service() { // ... 省略已有成员和其他方法。
// 按歌曲 id 查找索引,再交给 playImpl() 处理。 fun play(song: Song) { val id = song.id ?: return val index = musicList.indexOfFirst { it.id == id } if (index < 0) return playImpl(index) }
// ... 省略服务中的其他方法。}缺少 id 或列表中没有匹配项时,play() 直接结束,不会调用 playImpl()。
3. 异步准备并开始播放
在 setDataSource() 后加入准备完成回调和异步准备调用。
class MusicService : Service() { // ... 省略已有成员和其他方法。
private fun playImpl(index: Int) { // 根据索引取得歌曲,没有对应歌曲或地址时结束操作。 val song = musicList.getOrNull(index) ?: return val url = song.musicPath ?: return try { player.reset() player.setDataSource(url) // 准备完成后记录当前歌曲并开始播放。 player.setOnPreparedListener { mediaPlayer -> currentIndex = index mediaPlayer.start() } // true 表示错误已由监听器处理。 player.setOnErrorListener { _, _, _ -> true } // 异步准备在线音频,避免阻塞当前线程。 player.prepareAsync() } catch (e: Exception) { e.printStackTrace() } }
// ... 省略服务中的其他方法。}在线音频必须准备完成后才能播放。prepareAsync() 不阻塞服务线程,准备成功后由回调设置索引并启动播放;错误监听返回 true 表示错误已被处理。
四、释放播放器资源
在服务销毁时释放 MediaPlayer。在 MusicService 类中添加 onDestroy()。
class MusicService : Service() { // ... 省略已有成员和其他方法。
override fun onDestroy() { // 服务销毁时释放播放器占用的系统资源。 player.release() super.onDestroy() }}MediaPlayer 持有音频解码资源,必须在服务结束时显式释放,避免资源泄漏。
任务小结
服务现在能够保存歌曲列表、准备在线音频并释放播放器资源。下一任务将让用户点击列表歌曲开始播放。