apps\player\src\main\java\cn\nfarms\player\play\PlaybackService.kt¶
源码路径:
apps\player\src\main\java\cn\nfarms\player\play\PlaybackService.kt· 78 行
package cn.nfarms.player.play
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
/**
* 播放服务。
*
* # 为什么必须是 MediaSessionService
*
* 光有 Activity 里的 ExoPlayer 会出三个问题,每一个都很致命:
* 1. 切到后台一会儿就被系统挂起 —— 音乐停了,而且**不报错**;
* 2. 锁屏/通知栏没有控制条,耳机的播放/暂停键也收不到;
* 3. 来电话时不会自动暂停(没有 audio focus 协商)。
*
* `MediaSessionService` 一次解决三件:它自己挂前台通知、自己注册 MediaSession
* (媒体键 → 播放器),并且把 `AudioAttributes` 交给系统做焦点管理。
*
* # 耳机控制
*
* 不是我们监听 `ACTION_MEDIA_BUTTON` —— 是由 MediaSession 接管,
* 系统把媒体键分发给当前活跃的 session。自己做那套反而会在 Android 13+
* 因为 `MediaButtonReceiver` 的导出限制而失效。
*/
class PlaybackService : MediaSessionService() {
private var session: MediaSession? = null
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this)
// 音频焦点/来电话:交给系统协商,不要自己抢
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
/* handleAudioFocus = */ true,
)
// 播放中按耳机键要能唤醒 —— 不设这个,锁屏后按一下只是亮屏
.setHandleAudioBecomingNoisy(true)
.build()
// 断点续播的「记住进度」不放在这里:进度落盘由 ViewModel 在暂停/切曲时写,
// 服务只负责播。这样即使服务被系统杀掉,进度也不会丢。
session = MediaSession.Builder(this, player).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = session
override fun onTaskRemoved(rootIntent: android.content.Intent?) {
// 用户从最近任务里划掉 App:如果正在播就继续播(音乐 App 的惯例),
// 没在播就收摊。
val player = session?.player
if (player == null || !player.playWhenReady || player.mediaItemCount == 0) {
stopSelf()
}
}
override fun onDestroy() {
session?.run {
player.release()
release()
session = null
}
super.onDestroy()
}
companion object {
// 刻意不暴露「拿播放器」的入口:控制器走 MediaController,播放器归服务。
// 留一个空对象只是为了让文件结构与其他服务一致 —— 删掉它反而要看两遍
// 才知道"这里没有别的入口"。
}
}