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

多ExoPlayer实例独立停止/释放异常问题求助

问题描述
  • 管理多个独立ExoPlayer实例时,单个播放器停止/释放出现异常:
    1. 释放屏幕上第一个播放器时,虽能正常关闭该播放器,但stop()、release()方法会同时影响第二个播放器;若有三个播放器运行,关闭第一个仅影响第二个,第三个不受影响,受影响的总是下一个播放器;但通过屏幕控件暂停无异常。
    2. 直接从Map/List中删除播放器但不执行停止/释放操作时,不会对其他运行的播放器产生影响。
  • 已测试最小复现版本(MRE),问题依然存在,怀疑是MutableStateFlow或集合使用导致,需实现多个视频独立关闭、释放的功能。
问题分析

核心问题出在Compose中AndroidView的重组逻辑与ExoPlayer实例绑定的生命周期管理不匹配:

  1. 当Map中移除第一个播放器后,Compose重组时,players.forEach会重新遍历剩余的播放器,此时第二个播放器的PlayerView会被复用之前第一个播放器的View实例,旧的PlayerView已与被释放的播放器解绑,导致新的播放器被错误关联到已释放资源,或生命周期被干扰。
  2. 释放播放器时先调用release()再移除,此时UI尚未重组,PlayerView可能仍持有旧引用,引发冲突。
解决方案
  1. 为每个PlayerView指定唯一key:确保Compose不会复用不同播放器的View实例,每个播放器对应独立的PlayerView。
  2. 调整释放逻辑顺序:先从Map中移除播放器触发UI重组解绑PlayerView,再延迟释放播放器实例,避免UI持有已释放的播放器引用。
  3. 优化StateFlow更新逻辑:使用update方法安全修改StateFlow的值,避免并发问题。
  4. 修正初始化逻辑的触发条件:监听players变化,确保初始化逻辑正确执行。
修改后的代码

ViewModel代码

// ViewModel to manage the ExoPlayers (Media3)
class VideoPlayerViewModel(private val context: Context) : ViewModel() {
    // Use a StateFlow to manage all players in a map
    private val _players = MutableStateFlow(mapOf<String, ExoPlayer>())
    val players: StateFlow<Map<String, ExoPlayer>> = _players

    // Initialize a player with a given URL and playerId
    fun initializePlayer(playerId: String, url: String) {
        _players.update { currentPlayers ->
            if (currentPlayers.containsKey(playerId)) return@update currentPlayers

            val player = ExoPlayer.Builder(context).build().apply {
                val mediaItem = MediaItem.fromUri(Uri.parse(url))
                setMediaItem(mediaItem)
                prepare()
            }
            currentPlayers.toMutableMap().apply { this[playerId] = player }
        }
    }

    // Release a specific player
    fun releasePlayer(playerId: String) {
        val playerToRelease = _players.value[playerId] ?: return

        // 先从Map移除,触发UI重组解绑PlayerView
        _players.update { currentPlayers ->
            currentPlayers.toMutableMap().apply { remove(playerId) }
        }

        // 延迟释放,确保UI已完成重组
        viewModelScope.launch {
            delay(100) // 短暂延迟确保View解绑
            playerToRelease.release()
        }
    }

    // Release all players
    fun releaseAllPlayers() {
        val playersToRelease = _players.value.values.toList()
        _players.value = emptyMap()

        viewModelScope.launch {
            delay(100)
            playersToRelease.forEach { it.release() }
        }
    }

    override fun onCleared() {
        super.onCleared()
        releaseAllPlayers()
    }
}

Composable代码

@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel) {
    val players by viewModel.players.collectAsStateWithLifecycle()

    // 初始化播放器,监听players变化确保逻辑正确
    LaunchedEffect(players.keys) {
        if (!players.containsKey("player1")) {
            viewModel.initializePlayer("player1", "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")
        }
        if (!players.containsKey("player2")) {
            viewModel.initializePlayer("player2", "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        players.forEach { (playerId, player) ->
            // 关键:为每个AndroidView指定唯一key,防止View复用
            AndroidView(
                factory = { context ->
                    PlayerView(context).apply {
                        this.player = player
                    }
                },
                modifier = Modifier.weight(1f),
                key = playerId // 唯一key,确保每个播放器对应独立的View
            )
        }

        Button(
            onClick = { viewModel.releasePlayer("player1") },
            modifier = Modifier
                .padding(16.dp)
                .height(48.dp) // 修正高度,原15dp过小无法正常点击
        ) {
            Text(text = "释放Player1", color = Color.White)
        }
    }
}

调用代码

lateinit var viewModel: VideoPlayerViewModel

viewModel = VideoPlayerViewModel(context)

VideoPlayerScreen(viewModel = viewModel)

内容的提问来源于stack exchange,提问作者Sonika Batth

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 23:29:56