任务二 使用 MediaPlayer 播放歌曲

服务绑定完成后,页面已经可以访问 MusicService。现在在服务中加入 MediaPlayer,建立从歌曲对象到音频地址再到播放器的处理流程。

一、任务目标

完成后,MusicService 将能够:

  • 保存网络歌曲列表和当前歌曲索引。
  • 根据 Song.musicPath 设置音频数据源并异步准备播放器。
  • 在准备完成后开始播放,并在服务销毁时释放 MediaPlayer。

二、创建播放器和歌曲列表

1. 添加 MediaPlayer 成员

打开 app/src/main/java/com/sziit/musicplayer/service/MusicService.kt,导入 MediaPlayer 和 Song,并在服务类中保存播放器、歌曲列表和当前索引。

app/src/main/java/com/sziit/musicplayer/service/MusicService.kt
// ... 省略 package 声明和已有导入。
import android.media.MediaPlayer
import 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 在接口请求成功后调用。

app/src/main/java/com/sziit/musicplayer/service/MusicService.kt
class MusicService : Service() {
// ... 省略已有成员。
// 用最新请求结果替换服务中保存的歌曲列表。
fun setMusicList(list: List<Song>) {
musicList.clear()
musicList.addAll(list)
// 记录收到的歌曲数量,便于在 Logcat 中验证数据传递。
Log.i(TAG, "MusicService: 歌曲列表已更新,共 ${musicList.size} 首")
}
// ... 省略服务中的其他方法。
}

在 MusicListFragment 的在线请求成功回调中,将歌曲列表同步给服务。

app/src/main/java/com/sziit/musicplayer/ui/fragment/MusicListFragment.kt
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 首歌曲,说明列表数据已传入服务;实际数量以接口返回结果为准。

MusicService 收到歌曲列表的日志

列表界面仍应正常显示;点击歌曲暂时不会播放,播放入口会在下一任务中接入。

页面负责显示,服务保存播放所需的歌曲数据。

三、根据歌曲地址准备播放

1. 校验歌曲并设置音频地址

先在 MusicService 中添加 playImpl(),根据歌曲索引取得歌曲,检查音频地址,再重置播放器并设置新的数据源。

app/src/main/java/com/sziit/musicplayer/service/MusicService.kt
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 或未找到歌曲时,直接结束。

app/src/main/java/com/sziit/musicplayer/service/MusicService.kt
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() 后加入准备完成回调和异步准备调用。

app/src/main/java/com/sziit/musicplayer/service/MusicService.kt
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()。

app/src/main/java/com/sziit/musicplayer/service/MusicService.kt
class MusicService : Service() {
// ... 省略已有成员和其他方法。
override fun onDestroy() {
// 服务销毁时释放播放器占用的系统资源。
player.release()
super.onDestroy()
}
}

MediaPlayer 持有音频解码资源,必须在服务结束时显式释放,避免资源泄漏。

任务小结

服务现在能够保存歌曲列表、准备在线音频并释放播放器资源。下一任务将让用户点击列表歌曲开始播放。