跳转至

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,播放器归服务。
        // 留一个空对象只是为了让文件结构与其他服务一致 —— 删掉它反而要看两遍
        // 才知道"这里没有别的入口"。
    }
}