You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android音频流App中ExoPlayer切换Fragment后音频控制异常问题

问题:Fragment切换后ExoPlayer音频无法正确停止/重复播放

当前场景是托管在MainActivity中的RadioPlayerFragment,使用ExoPlayer实现电台播放,需求为:

  • 播放音频时切换到其他Fragment,音频需继续播放
  • 返回该Fragment时,根据实际播放状态展示对应UI

但出现异常:切换到其他Fragment再返回后,UI状态显示正确,但点击停止按钮时,UI更新为停止状态但音频未停止;再次点击播放按钮会同时播放两个音频(未停止的旧音频和新启动的音频),仅在Fragment切换返回后出现此问题,正常操作时无异常。


原Fragment代码

class RadioPlayerFragment : Fragment() {

    private lateinit var player: SimpleExoPlayer
    private lateinit var mBinding: FragmentRadioPlayerBinding
    private var isPlaying: Boolean = false
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mBinding = FragmentRadioPlayerBinding.inflate(inflater, container, false)
        return mBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        Log.d(TAG, "onViewCreated \n $isPlaying")
        isPlaying = SharedPrefs.getBoolean(requireContext(), "isPlaying")
        initializePlayer()
        updateUI()
        mBinding.btnPlayPause.setOnClickListener {
            if (isPlaying) {
                Log.d(TAG, "onViewCreated: $isPlaying pausing audio")
                pauseRadio()
            } else {
                Log.d(TAG, "onViewCreated: $isPlaying playing audio")
                playRadio()
            }
        }

        requireContext().registerReceiver(
            notificationActionReceiver, IntentFilter("ACTION_STOP")
        )
    }
    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart")
        if (isPlaying) {
            Log.d(TAG, "onStart: isPlaying")
            player.playWhenReady = true
            updateUI()
            updateNotification()
        }
    }

    private fun initializePlayer() {
        if (::player.isInitialized && isPlaying) {
            player.stop()
            player.release()
        }

        player = SimpleExoPlayer.Builder(requireContext())
            .setTrackSelector(DefaultTrackSelector(requireContext()))
            .setLoadControl(DefaultLoadControl())
            .build()

             val mediaItem = MediaItem.fromUri(Uri.parse(Constants.k_STREAM_URL))

        player.setMediaItem(mediaItem)
        player.prepare()
        Log.d(TAG, "initializePlayer: $isPlaying")
        player.playWhenReady = false
    }
    private fun playRadio() {
        player.playWhenReady = true
        isPlaying = true
        SharedPrefs.setBoolean(requireContext(), "isPlaying", true)
        updateNotification()
        updateUI()
    }
    private fun pauseRadio() {
        player.playWhenReady = false
        isPlaying = false
        Log.d(TAG, "pauseRadio: $isPlaying")
        SharedPrefs.setBoolean(requireContext(), "isPlaying", false)
        updateNotification()
        updateUI()
    }
    private fun updateUI() {
        if (isPlaying) {
            mBinding.gif.setImageResource(R.drawable.vuplay)
            mBinding.btnPlayPause.setImageResource(R.drawable.stopbutton)
        } else {
            mBinding.gif.setImageResource(R.drawable.vustop)
            mBinding.btnPlayPause.setImageResource(R.drawable.playbutton)
        }
    }
    private fun updateNotification() {
        createNotificationChannel()

        val stopIntent = Intent(requireContext(), NotificationActionReceiver::class.java).apply {
            action = "ACTION_STOP"
        }
        val stopPendingIntent: PendingIntent = PendingIntent.getBroadcast(requireContext(), 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

        val notification = NotificationCompat.Builder(requireContext(), "radio_channel")
            .setContentTitle("GUGS FM is playing")
            .setContentText("App is running in the background")
            .setSmallIcon(R.drawable.logo_2)
            .addAction(R.drawable.stopbutton, "Stop", stopPendingIntent)
            .build()

        if (ActivityCompat.checkSelfPermission(
                requireContext(),
                Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }
        NotificationManagerCompat.from(requireContext()).notify(1, notification)
    }
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = "Radio Channel"
            val descriptionText = "Channel for radio notifications"
            val importance = NotificationManager.IMPORTANCE_LOW
            val channel = NotificationChannel("radio_channel", name, importance).apply {
                description = descriptionText
            }
            val notificationManager = requireContext().getSystemService(NotificationManager::class.java)
            notificationManager.createNotificationChannel(channel)
        }
    }

    private val notificationActionReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == "ACTION_STOP") {
                pauseRadio()
            }
        }
    }

    companion object{
        val TAG = "RadioPlayerFragment"
    }
}

问题原因

  1. 重复创建Player实例:Fragment切换后重建时,onViewCreated会再次调用initializePlayer,但原判断逻辑仅在player已初始化且正在播放时才释放旧实例,实际切换时旧player仍在后台运行,但Fragment重建后player变量已不是原引用,导致新实例被创建,旧实例继续播放。
  2. 广播接收器未注销:每次进入Fragment都注册接收器,但未在销毁时注销,可能导致多个接收器关联不同的player实例。
  3. Player生命周期管理错误:Fragment切换时(如replace操作)原Fragment会执行onDestroyView,但未对player做保留处理,旧实例脱离控制继续运行。

修复方案

方案1:使用ViewModel托管Player(推荐)

ViewModel生命周期与Activity绑定,能确保切换Fragment时player实例不被销毁,彻底解决重复实例问题:

1. 创建RadioPlayerViewModel

class RadioPlayerViewModel : ViewModel() {
    var player: SimpleExoPlayer? = null
    var isPlaying: Boolean = false

    fun initializePlayer(context: Context) {
        if (player == null) {
            player = SimpleExoPlayer.Builder(context)
                .setTrackSelector(DefaultTrackSelector(context))
                .setLoadControl(DefaultLoadControl())
                .build().apply {
                    val mediaItem = MediaItem.fromUri(Uri.parse(Constants.k_STREAM_URL))
                    setMediaItem(mediaItem)
                    prepare()
                    playWhenReady = isPlaying
                }
        }
    }

    fun playRadio() {
        player?.playWhenReady = true
        isPlaying = true
        SharedPrefs.setBoolean(context, "isPlaying", true)
    }

    fun pauseRadio() {
        player?.playWhenReady = false
        isPlaying = false
        SharedPrefs.setBoolean(context, "isPlaying", false)
    }

    override fun onCleared() {
        super.onCleared()
        player?.release()
        player = null
    }
}

2. 修改RadioPlayerFragment

class RadioPlayerFragment : Fragment() {
    private lateinit var mBinding: FragmentRadioPlayerBinding
    private val viewModel: RadioPlayerViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mBinding = FragmentRadioPlayerBinding.inflate(inflater, container, false)
        return mBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 从SharedPref恢复播放状态
        viewModel.isPlaying = SharedPrefs.getBoolean(requireContext(), "isPlaying")
        viewModel.initializePlayer(requireContext())
        updateUI()

        mBinding.btnPlayPause.setOnClickListener {
            if (viewModel.isPlaying) {
                viewModel.pauseRadio()
            } else {
                viewModel.playRadio()
            }
            updateUI()
            updateNotification()
        }

        requireContext().registerReceiver(
            notificationActionReceiver, IntentFilter("ACTION_STOP")
        )
    }

    override fun onStart() {
        super.onStart()
        if (viewModel.isPlaying) {
            viewModel.player?.playWhenReady = true
            updateNotification()
        }
    }

    private fun updateUI() {
        if (viewModel.isPlaying) {
            mBinding.gif.setImageResource(R.drawable.vuplay)
            mBinding.btnPlayPause.setImageResource(R.drawable.stopbutton)
        } else {
            mBinding.gif.setImageResource(R.drawable.vustop)
            mBinding.btnPlayPause.setImageResource(R.drawable.playbutton)
        }
    }

    private val notificationActionReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == "ACTION_STOP") {
                viewModel.pauseRadio()
                updateUI()
                updateNotification()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        requireContext().unregisterReceiver(notificationActionReceiver)
    }

    companion object{
        val TAG = "RadioPlayerFragment"
    }
}

方案2:不使用ViewModel的临时修复

如果不想引入ViewModel,可通过调整player生命周期逻辑避免重复实例:

class RadioPlayerFragment : Fragment() {
    private var player: SimpleExoPlayer? = null // 改为可空变量
    private lateinit var mBinding: FragmentRadioPlayerBinding
    private var isPlaying: Boolean = false

    override fun onCreateView(...) {
        // 原代码不变
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        isPlaying = SharedPrefs.getBoolean(requireContext(), "isPlaying")
        
        // 仅当player为null时初始化
        if (player == null) {
            initializePlayer()
        } else {
            player?.playWhenReady = isPlaying
        }
        updateUI()
        
        // 原点击事件、广播注册逻辑不变
    }

    private fun initializePlayer() {
        player = SimpleExoPlayer.Builder(requireContext())
            .setTrackSelector(DefaultTrackSelector(requireContext()))
            .setLoadControl(DefaultLoadControl())
            .build().apply {
                val mediaItem = MediaItem.fromUri(Uri.parse(Constants.k_STREAM_URL))
                setMediaItem(mediaItem)
                prepare()
                playWhenReady = isPlaying
            }
    }

    // 修改播放/暂停方法,使用可空player
    private fun playRadio() {
        player?.playWhenReady = true
        isPlaying = true
        SharedPrefs.setBoolean(requireContext(), "isPlaying", true)
        updateNotification()
        updateUI()
    }

    private fun pauseRadio() {
        player?.playWhenReady = false
        isPlaying = false
        SharedPrefs.setBoolean(requireContext(), "isPlaying", false)
        updateNotification()
        updateUI()
    }

    // 仅在Activity销毁时释放player
    override fun onDestroy() {
        super.onDestroy()
        player?.release()
        player = null
    }

    override fun onDestroyView() {
        super.onDestroyView()
        requireContext().unregisterReceiver(notificationActionReceiver)
    }

    // 其他方法不变
}

关键修复点总结

  • 单例Player实例:确保整个播放流程中只有一个Player实例,ViewModel是最可靠的实现方式。
  • 注销广播接收器:在onDestroyView中注销接收器,防止内存泄漏和多实例触发问题。
  • 统一状态管理:使用ViewModel或共享变量同步UI与Player的播放状态,避免状态不一致。

内容的提问来源于stack exchange,提问作者Sahar Batool

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 17:37:32