You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android锁屏后ExoPlayer播放10-20分钟中断的编程修复方法

解决ExoPlayer后台锁屏播放停止的问题

这个问题我之前帮不少开发者处理过——本质上是Android系统在后台资源紧张时,会优先回收非前台进程的资源,再加上没有正确持有唤醒锁和声明播放优先级,才导致ExoPlayer在锁屏十几分钟后被暂停。下面给你一步步的解决方案:

1. 给ExoPlayer配置唤醒模式与音频属性

ExoPlayer确实没有MediaPlayer那样直接的setWakeMode()方法,但它把这些配置整合到了初始化Builder里,用来告诉系统我们需要持续播放音频的需求:

// 配置音频属性,声明这是媒体播放,优先级更高
val audioAttributes = AudioAttributes.Builder()
    .setContentType(C.CONTENT_TYPE_MUSIC)
    .setUsage(C.USAGE_MEDIA)
    .build()

// 创建播放器时设置唤醒模式和音频属性
val player = ExoPlayer.Builder(context)
    .setAudioAttributes(audioAttributes, true) // 第二个参数设为true,让ExoPlayer自动处理音频焦点
    .setWakeMode(C.WAKE_MODE_NETWORK) // 网络电台选这个,保持网络连接+CPU唤醒;本地音频用C.WAKE_MODE_LOCAL
    .build()
  • WAKE_MODE_NETWORK:适合网络电台,确保播放时CPU和网络连接都保持活跃;
  • WAKE_MODE_LOCAL:适合本地音频,只保持CPU唤醒即可。

2. 用前台服务托管播放(核心!)

从Android 8.0开始,后台服务会被系统限制,一段时间后就会被杀死。所以必须把播放逻辑放到前台服务里,通过显示通知告诉系统:这个服务在执行重要的媒体播放任务,不能随便回收。

ExoPlayer提供了PlayerNotificationManager工具类,能自动帮你生成符合要求的前台通知,还能同步播放状态:

class RadioPlaybackService : Service() {
    private lateinit var player: ExoPlayer
    private lateinit var notificationManager: PlayerNotificationManager

    override fun onCreate() {
        super.onCreate()
        // 初始化播放器(复用上面的配置)
        val audioAttributes = AudioAttributes.Builder()
            .setContentType(C.CONTENT_TYPE_MUSIC)
            .setUsage(C.USAGE_MEDIA)
            .build()
        player = ExoPlayer.Builder(this)
            .setAudioAttributes(audioAttributes, true)
            .setWakeMode(C.WAKE_MODE_NETWORK)
            .build()

        // 配置通知渠道(Android 8.0+必需)
        val channelId = "radio_playback_channel"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "电台播放",
                NotificationManager.IMPORTANCE_LOW
            )
            getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
        }

        // 初始化PlayerNotificationManager
        notificationManager = PlayerNotificationManager.Builder(this, 1001, channelId)
            .setMediaDescriptionAdapter(object : PlayerNotificationManager.MediaDescriptionAdapter {
                override fun getCurrentContentTitle(player: Player): CharSequence {
                    return "你的电台名称"
                }

                override fun createCurrentContentIntent(player: Player): PendingIntent? {
                    // 点击通知打开主界面
                    val intent = Intent(this@RadioPlaybackService, MainActivity::class.java)
                    return PendingIntent.getActivity(
                        this@RadioPlaybackService,
                        0,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                    )
                }

                override fun getCurrentContentText(player: Player): CharSequence? {
                    return "正在播放当前节目"
                }

                override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback): Bitmap? {
                    // 可选:设置电台通知图标
                    return BitmapFactory.decodeResource(resources, R.drawable.ic_radio_icon)
                }
            })
            .setNotificationListener(object : PlayerNotificationManager.NotificationListener {
                override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
                    super.onNotificationCancelled(notificationId, dismissedByUser)
                    // 用户取消通知时,停止播放并销毁服务
                    player.stop()
                    stopSelf()
                }
            })
            .build()
        notificationManager.setPlayer(player)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 启动前台服务,PlayerNotificationManager会自动处理通知的显示
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        // 释放资源
        notificationManager.setPlayer(null)
        player.release()
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }
}

3. 声明必要的权限与服务

AndroidManifest.xml里添加权限和注册服务:

<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 唤醒锁权限 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 网络电台需要的网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- 注册播放服务,Android 12+需要指定foregroundServiceType -->
<service 
    android:name=".RadioPlaybackService" 
    android:foregroundServiceType="mediaPlayback" />

4. 额外注意:处理音频焦点

虽然我们在初始化播放器时已经设置了setAudioAttributes(audioAttributes, true)让ExoPlayer自动处理音频焦点(比如来电时暂停,来电结束后恢复),如果需要自定义行为,可以添加AudioFocusListener来监听焦点变化事件,不过默认逻辑已经能满足大部分电台应用的需求。

总结

把这几点结合起来,就能解决后台锁屏播放停止的问题:

  • 前台服务保证系统不会轻易杀死播放进程;
  • 唤醒模式让CPU/网络保持活跃,不会进入休眠;
  • 正确的音频属性声明让系统知道这是高优先级的媒体播放任务。

内容的提问来源于stack exchange,提问作者Vadim Fedchuk

火山引擎 最新活动