最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

android - Exoplayer multi instance stop release issue - Stack Overflow

programmeradmin1浏览0评论

i am trying manage multiple exoplayer instances or screen which are working independently.

the issue i am having is stop and releasing a single player.

if i am releasing / stopping first player on screen, it closing the first player properly, but Stop(), release() also affects second player. if there are three player running then closing first player affects second player, but third player have no affect and works fine. its always the next player. pausing with on screen controls have no issue.

Deleting player from map/list without stopping/releasing them works fine and has no effect on next player running.

i tried multiple ways, but no luck ,i am looking to manage multiple videos on screen. which can be closed, released independently of other videos.

i tested a plain version [mre] of dynamic player, same issue, this issue seems to me with mutablestateflow or Map/list usage, plz advise

// 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>() // Initially empty map of players
    )
    val players: StateFlow<Map<String, ExoPlayer>> = _players

    // Initialize a player with a given URL and playerId
    fun initializePlayer(playerId: String, url: String) {
        val player = ExoPlayer.Builder(context).build()
        val mediaItem = MediaItem.fromUri(Uri.parse(url))
        player.setMediaItem(mediaItem)
        player.prepare()

        // Add or update the player in the map
        _players.value = _players.value.toMutableMap().apply {
            this[playerId] = player
        }
    }

    // Release a specific player
    fun releasePlayer(playerId: String) {
        _players.value[playerId]?.release()

        // Remove the player from the map after releasing it
        _players.value = _players.value.toMutableMap().apply {
            remove(playerId)
        }
    }

    // Release all players
    fun releaseAllPlayers() {
        _players.value.forEach { (_, player) ->
            player.release()
        }

        // Clear the map after releasing all players
        _players.value = emptyMap()
    }
}

// Composable function to display the video players
@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel) {
    // Get all players' StateFlow and collect their state
    val players = viewModel.players.collectAsState().value

    // Initialize players if they are not initialized
    LaunchedEffect(Unit) {
        if (!players.containsKey("player1")) {
            viewModel.initializePlayer("player1", ".m3u8")
        }
        if (!players.containsKey("player2")) {
            viewModel.initializePlayer("player2", ".m3u8")
        }
    }

    // UI Layout with dynamic video views based on the players in the map
    Column(modifier = Modifier.fillMaxSize()) {
        players.forEach { (playerId, player) ->
            AndroidView(factory = { context ->
                PlayerView(context).apply {
                    this.player = player
                }
            }, modifier = Modifier.weight(1f))
        }

        // Button to release all players
        Button(
            onClick = {
                viewModel.releasePlayer("player1") // Call releaseAllPlayers on button click
            },
            modifier = Modifier
                .padding(16.dp)
                .height(15.dp)
        ) {
            Text(text = "Release All Players", color = Color.White)
        }
    }
}

lateinit var viewModel: VideoPlayerViewModel

viewModel = VideoPlayerViewModel(context)


VideoPlayerScreen(viewModel = viewModel)

i am trying manage multiple exoplayer instances or screen which are working independently.

the issue i am having is stop and releasing a single player.

if i am releasing / stopping first player on screen, it closing the first player properly, but Stop(), release() also affects second player. if there are three player running then closing first player affects second player, but third player have no affect and works fine. its always the next player. pausing with on screen controls have no issue.

Deleting player from map/list without stopping/releasing them works fine and has no effect on next player running.

i tried multiple ways, but no luck ,i am looking to manage multiple videos on screen. which can be closed, released independently of other videos.

i tested a plain version [mre] of dynamic player, same issue, this issue seems to me with mutablestateflow or Map/list usage, plz advise

// 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>() // Initially empty map of players
    )
    val players: StateFlow<Map<String, ExoPlayer>> = _players

    // Initialize a player with a given URL and playerId
    fun initializePlayer(playerId: String, url: String) {
        val player = ExoPlayer.Builder(context).build()
        val mediaItem = MediaItem.fromUri(Uri.parse(url))
        player.setMediaItem(mediaItem)
        player.prepare()

        // Add or update the player in the map
        _players.value = _players.value.toMutableMap().apply {
            this[playerId] = player
        }
    }

    // Release a specific player
    fun releasePlayer(playerId: String) {
        _players.value[playerId]?.release()

        // Remove the player from the map after releasing it
        _players.value = _players.value.toMutableMap().apply {
            remove(playerId)
        }
    }

    // Release all players
    fun releaseAllPlayers() {
        _players.value.forEach { (_, player) ->
            player.release()
        }

        // Clear the map after releasing all players
        _players.value = emptyMap()
    }
}

// Composable function to display the video players
@Composable
fun VideoPlayerScreen(viewModel: VideoPlayerViewModel) {
    // Get all players' StateFlow and collect their state
    val players = viewModel.players.collectAsState().value

    // Initialize players if they are not initialized
    LaunchedEffect(Unit) {
        if (!players.containsKey("player1")) {
            viewModel.initializePlayer("player1", "https://devstreaming-cdn.apple/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")
        }
        if (!players.containsKey("player2")) {
            viewModel.initializePlayer("player2", "https://devstreaming-cdn.apple/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")
        }
    }

    // UI Layout with dynamic video views based on the players in the map
    Column(modifier = Modifier.fillMaxSize()) {
        players.forEach { (playerId, player) ->
            AndroidView(factory = { context ->
                PlayerView(context).apply {
                    this.player = player
                }
            }, modifier = Modifier.weight(1f))
        }

        // Button to release all players
        Button(
            onClick = {
                viewModel.releasePlayer("player1") // Call releaseAllPlayers on button click
            },
            modifier = Modifier
                .padding(16.dp)
                .height(15.dp)
        ) {
            Text(text = "Release All Players", color = Color.White)
        }
    }
}

lateinit var viewModel: VideoPlayerViewModel

viewModel = VideoPlayerViewModel(context)


VideoPlayerScreen(viewModel = viewModel)
Share Improve this question asked Mar 6 at 1:38 Sonika BatthSonika Batth 171 silver badge2 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 2

In your forEach loop you create an AndroidView for each entry in the map. The players are assigned in iteration order to the AndroidViews.

When you now remove the first player in the map, the second player becomes the first in iteration order. The next recomposition re-executes the forEach loop, but although the first AndroidView would now be correctly assigned the second player (because that is now the first in iteration order), that only happens in the factory lambda which is only executed once when the AndroidView enters the composition, not on recompositions.

That means that the first generated AndroidView still displays the original first player (which is now stopped and removed from the map, but the AndroidView still holds a reference to it), while the second AndroidView that would display the second player (the only one that is still in the map) is removed from the composition because the forEach loop only iterates once (the map only has one entry).

The issue here is that removing any entry from the map results in the last AndroidView to be removed, not necessarily the one associated to the removed entry.

To fix that you can force the AndroidViews to not be associated to players by their position in the map, but by the map's key. There are multiple ways to do that, but a simple solution is to wrap the AndroidView with a key:

players.forEach { (playerId, player) ->
    key(playerId) {
        AndroidView(factory = { context ->
            PlayerView(context).apply {
                this.player = player
            }
        }, modifier = Modifier.weight(1f))
    }
}

Now Compose knows that the first AndroidView is created when key is "player1" and the second AndroidView is created when key is "player2". Whatever entry you now remove from the map, the entire key composable with the associated playerId (including its AndroidView) is removed from the composition. Removing "player1" now correctly removes the first AndroidView, and the second one associated with "player2" isn't touched.


Although not responsible for your current issue, the way you update your MutableStateFlow in the view model is very error prone. Whenever you need the old value to calculate the new value (like removing an entry from the map), you should use the update method instead of the value property:

  • In initializePlayer(), replace

    _players.value = _players.value.toMutableMap().apply {
       this[playerId] = player
    }
    

    with

    _players.update { it + (playerId to player) }
    
  • In releasePlayer(), replace

    _players.value[playerId]?.release()
    
    // Remove the player from the map after releasing it
    _players.value = _players.value.toMutableMap().apply {
        remove(playerId)
    }
    

    with

    _players.update {
        it[playerId]?.release()
        it - playerId
    }
    
  • And in releaseAllPlayers(), replace

    _players.value.forEach { (_, player) ->
        player.release()
    }
    
    // Clear the map after releasing all players
    _players.value = emptyMap()
    

    with

    _players.update {
        it.values.forEach(ExoPlayer::release)
        emptyMap()
    }
    

This is important because update guarantees thread-safety, the value cannot be changed in between. I moved the release in the lambda as well to make sure the removed players are actually the same as the ones that are released, even when in between another player with the same key would be added to the map. Since the update lambda may be repeatedly executed when another update is concurrently running, release may be theoretically executed multiple times on the same player. That shouldn't be an issue, though, because as far as I can tell, release is idempotent (i.e. can be executed repeatedly, resulting in the same outcome).

Furthermore, your approach to cast the map to a MutableMap actually stores this MutableMap in the flow. Although its content type is still an (immutable) Map, the actual object is really a MutableMap that anyone will be able modify after casting, like this:

(viewModel.players.value as? MutableMap<String, ExoPlayer>)?.put(...)

That would not only violate the premise that only the view model can change the flow's value, it would also break the flow's ability to properly notify its collectors on this changed value, resulting in arbitrary runtime behavior.

Better make sure that the type of the map that is actually stored in the flow is really immutable, like what the replacement code above does.

Two Issues that were found in your viewModel class

  1. Improper mutation mutableStateFlow

     // Release a specific player
     fun releasePlayer(playerId: String) {
         _players.value[playerId]?.release()
    
         // Remove the player from the map after releasing it
         _players.value = _players.value.toMutableMap().apply {
             remove(playerId)
         }
     }
    

Instead you should use update function to update the state flow so that it can trigger recomposition.

  1. Shouldn't call initialize Player in Launched Effect

     // Initialize players if they are not initialized
     LaunchedEffect(Unit) {
         if (!players.containsKey("player1")) {
             viewModel.initializePlayer("player1", "https://devstreaming-cdn.apple/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")
         }
         if (!players.containsKey("player2")) {
             viewModel.initializePlayer("player2", "https://devstreaming-cdn.apple/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")
         }
     }
    

Since doing this can meddle with recomposition and whenever recomposition triggers new player will initialized. Use user action or initialize player in the init block of the viewModel class.

Hope that helps.

发布评论

评论列表(0)

  1. 暂无评论