Skip to content

Commit c0dd97e

Browse files
committed
Clear surface from previous player when assigning a new player
The surface must only be used by one player at a time. To ensure that, we can keep a reference to the previously used player and clear its surface reference before assigning to a new one. Note that we do not need to clear the surface in onDispose of a DisposableEffect because the lifecycle management of the surface is moved to the Player and the Player takes care of unregistering its surface reference as soon as the surface is destroyed (which happens when the AndroidView element is no longer is the Composable tree). PiperOrigin-RevId: 745558414 (cherry picked from commit f9617e1)
1 parent f35c59b commit c0dd97e

File tree

3 files changed

+75
-23
lines changed

3 files changed

+75
-23
lines changed

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
* UI:
6666
* Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and
6767
`CompositionPlayer`.
68+
* Fix bug where `PlayerSurface` can't be recomposed with a new `Player`.
6869
* Downloads:
6970
* Add partial download support for progressive streams. Apps can prepare a
7071
progressive stream with `DownloadHelper`, and request a

libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616

1717
package androidx.media3.ui.compose
1818

19+
import android.content.Context
1920
import android.view.SurfaceView
2021
import android.view.TextureView
22+
import android.view.View
2123
import androidx.annotation.IntDef
2224
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.LaunchedEffect
2326
import androidx.compose.runtime.getValue
24-
import androidx.compose.runtime.rememberUpdatedState
27+
import androidx.compose.runtime.mutableStateOf
28+
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.setValue
2530
import androidx.compose.ui.Modifier
2631
import androidx.compose.ui.viewinterop.AndroidView
2732
import androidx.media3.common.Player
@@ -47,37 +52,53 @@ fun PlayerSurface(
4752
modifier: Modifier = Modifier,
4853
surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW,
4954
) {
50-
// Player might change between compositions,
51-
// we need long-lived surface-related lambdas to always use the latest value
52-
val currentPlayer by rememberUpdatedState(player)
53-
5455
when (surfaceType) {
5556
SURFACE_TYPE_SURFACE_VIEW ->
56-
AndroidView(
57-
factory = {
58-
SurfaceView(it).apply {
59-
if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
60-
currentPlayer.setVideoSurfaceView(this)
61-
}
62-
},
63-
onReset = {},
64-
modifier = modifier,
57+
PlayerSurfaceInternal(
58+
player,
59+
modifier,
60+
createView = { SurfaceView(it) },
61+
setViewOnPlayer = { player, view -> player.setVideoSurfaceView(view) },
62+
clearViewFromPlayer = { player, view -> player.clearVideoSurfaceView(view) },
6563
)
6664
SURFACE_TYPE_TEXTURE_VIEW ->
67-
AndroidView(
68-
factory = {
69-
TextureView(it).apply {
70-
if (currentPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
71-
currentPlayer.setVideoTextureView(this)
72-
}
73-
},
74-
onReset = {},
75-
modifier = modifier,
65+
PlayerSurfaceInternal(
66+
player,
67+
modifier,
68+
createView = { TextureView(it) },
69+
setViewOnPlayer = { player, view -> player.setVideoTextureView(view) },
70+
clearViewFromPlayer = { player, view -> player.clearVideoTextureView(view) },
7671
)
7772
else -> throw IllegalArgumentException("Unrecognized surface type: $surfaceType")
7873
}
7974
}
8075

76+
@Composable
77+
private fun <T : View> PlayerSurfaceInternal(
78+
player: Player,
79+
modifier: Modifier,
80+
createView: (Context) -> T,
81+
setViewOnPlayer: (Player, T) -> Unit,
82+
clearViewFromPlayer: (Player, T) -> Unit,
83+
) {
84+
var view by remember { mutableStateOf<T?>(null) }
85+
var registeredPlayer by remember { mutableStateOf<Player?>(null) }
86+
AndroidView(factory = { createView(it).apply { view = this } }, onReset = {}, modifier = modifier)
87+
view?.let { view ->
88+
LaunchedEffect(view, player) {
89+
registeredPlayer?.let { previousPlayer ->
90+
if (previousPlayer.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE))
91+
clearViewFromPlayer(previousPlayer, view)
92+
registeredPlayer = null
93+
}
94+
if (player.isCommandAvailable(Player.COMMAND_SET_VIDEO_SURFACE)) {
95+
setViewOnPlayer(player, view)
96+
registeredPlayer = player
97+
}
98+
}
99+
}
100+
}
101+
81102
/**
82103
* The type of surface used for media playbacks. One of [SURFACE_TYPE_SURFACE_VIEW] or
83104
* [SURFACE_TYPE_TEXTURE_VIEW].

libraries/ui_compose/src/test/java/androidx/media3/ui/compose/PlayerSurfaceTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ import androidx.compose.runtime.MutableIntState
2121
import androidx.compose.runtime.mutableIntStateOf
2222
import androidx.compose.runtime.remember
2323
import androidx.compose.ui.test.junit4.createComposeRule
24+
import androidx.media3.common.ForwardingPlayer
2425
import androidx.media3.common.Player
2526
import androidx.media3.ui.compose.utils.TestPlayer
2627
import androidx.test.ext.junit.runners.AndroidJUnit4
2728
import com.google.common.truth.Truth.assertThat
2829
import org.junit.Rule
2930
import org.junit.Test
3031
import org.junit.runner.RunWith
32+
import org.mockito.ArgumentMatchers.any
33+
import org.mockito.Mockito.inOrder
34+
import org.mockito.Mockito.spy
3135

3236
/** Unit test for [PlayerSurface]. */
3337
@RunWith(AndroidJUnit4::class)
@@ -87,4 +91,30 @@ class PlayerSurfaceTest {
8791

8892
assertThat(player.videoOutput).isInstanceOf(SurfaceView::class.java)
8993
}
94+
95+
@Test
96+
fun playerSurface_withNewPlayer_unsetsSurfaceOnOldPlayerFirst() {
97+
val player0 = TestPlayer()
98+
val player1 = TestPlayer()
99+
val spyPlayer0 = spy(ForwardingPlayer(player0))
100+
val spyPlayer1 = spy(ForwardingPlayer(player1))
101+
102+
lateinit var playerIndex: MutableIntState
103+
composeTestRule.setContent {
104+
playerIndex = remember { mutableIntStateOf(0) }
105+
PlayerSurface(
106+
player = if (playerIndex.intValue == 0) spyPlayer0 else spyPlayer1,
107+
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
108+
)
109+
}
110+
composeTestRule.waitForIdle()
111+
playerIndex.intValue = 1
112+
composeTestRule.waitForIdle()
113+
114+
assertThat(player0.videoOutput).isNull()
115+
assertThat(player1.videoOutput).isNotNull()
116+
val inOrder = inOrder(spyPlayer0, spyPlayer1)
117+
inOrder.verify(spyPlayer0).clearVideoSurfaceView(any())
118+
inOrder.verify(spyPlayer1).setVideoSurfaceView(any())
119+
}
90120
}

0 commit comments

Comments
 (0)