多ExoPlayer实例独立停止/释放异常问题求助
问题描述
- 管理多个独立ExoPlayer实例时,单个播放器停止/释放出现异常:
- 释放屏幕上第一个播放器时,虽能正常关闭该播放器,但
stop()、release()方法会同时影响第二个播放器;若有三个播放器运行,关闭第一个仅影响第二个,第三个不受影响,受影响的总是下一个播放器;但通过屏幕控件暂停无异常。 - 直接从Map/List中删除播放器但不执行停止/释放操作时,不会对其他运行的播放器产生影响。
- 释放屏幕上第一个播放器时,虽能正常关闭该播放器,但
- 已测试最小复现版本(MRE),问题依然存在,怀疑是MutableStateFlow或集合使用导致,需实现多个视频独立关闭、释放的功能。
问题分析
核心问题出在Compose中AndroidView的重组逻辑与ExoPlayer实例绑定的生命周期管理不匹配:
- 当Map中移除第一个播放器后,Compose重组时,
players.forEach会重新遍历剩余的播放器,此时第二个播放器的PlayerView会被复用之前第一个播放器的View实例,旧的PlayerView已与被释放的播放器解绑,导致新的播放器被错误关联到已释放资源,或生命周期被干扰。 - 释放播放器时先调用
release()再移除,此时UI尚未重组,PlayerView可能仍持有旧引用,引发冲突。
解决方案
- 为每个PlayerView指定唯一key:确保Compose不会复用不同播放器的View实例,每个播放器对应独立的
PlayerView。 - 调整释放逻辑顺序:先从Map中移除播放器触发UI重组解绑
PlayerView,再延迟释放播放器实例,避免UI持有已释放的播放器引用。 - 优化StateFlow更新逻辑:使用
update方法安全修改StateFlow的值,避免并发问题。 - 修正初始化逻辑的触发条件:监听
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
相关产品推荐
相关产品推荐

