Repository navigation
Black screen when ExoPlayer is used inside LazyColumn > LazyRow in Jetpack Compose #2493
Copy link
Copy link
Closed
Labels
Description
Version
Media3 main branch
More version details
When using ExoPlayer in Jetpack Compose within a LazyColumn that contains a LazyRow, the player sometimes renders as a black screen instead of displaying the video.
This issue appears only when the player is deeply nested within scrollable composables like LazyColumn > LazyRow. The audio plays, but the video surface is blank.
Devices that reproduce the issue
Using next layouts in demo-compose app (AndroidMedia)
LazyColumn(
modifier = Modifier.padding(top = 245.dp),
) {
// Row with videos
item {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
) {
items(videos.size) { index ->
val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(videos[index]))
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ONE
}
}
PlayerSurface(
player = exoPlayer,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = Modifier
.width(140.dp)
.height(245.dp)
.padding(8.dp),
)
}
}
}
// Add dummy views to fill the column
items(30) { index ->
Text(
text = "Dummy item #$index",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
Scrolling up/down .. in some point a black screen appear. Please see video
bug_videos.mp4
Devices reproduce the issue
Samsung S21
Emulator
Reproducible in the demo app?
Yes
Reproduction steps
- Open compose demo app
- Set layout share in this ticket
- Scroll up/down and horizontally for a while
- Observer a black screen appear.
Expected result
Player should work without getting black screen
Actual result
Player show black screen after several scrolls
Media
No DRM (use same videos as demo app)
Bug Report
- You will email the zip file produced by
adb bugreportto android-media-github@google.com after filing this issue.
Reactions are currently unavailable
Activity
asos-luisalcantara commentedon Jun 1, 2025
I tried to release the player instance
Problem is still present
oceanjules commentedon Jun 2, 2025
@asos-luisalcantara, you are creating a player PER video
item, it seems, which is very expensive. How big is yourvideosarray? And how come all your video boxes show the same cat video if the index in thevideosarray should be changing?asos-luisalcantara commentedon Jun 3, 2025
I'm using the same video URL and have implemented caching, but I'm still getting the same result. I checked the ExoPlayer logs—the player is loading correctly, but the surface remains black.
I've noticed significant memory usage, and I suspect that PlayerSurface might be the root cause. Internally, it's using AndroidView, which calls createView on each recomposition—potentially leading to repeated view allocations and surface creation.
Also I am guessing that we need to release the player when view is dispose?
asos-luisalcantara commentedon Jun 3, 2025
Could we have a memory leak on
asos-luisalcantara commentedon Jun 3, 2025
So issue is when PlayerSurface is in a list..??
asos-luisalcantara commentedon Jun 3, 2025
@oceanjules My video array is just 5 items .. No should not be change I am using just the same video.
bubenheimer commentedon Jun 3, 2025
@asos-luisalcantara your modifications make more sense to me than what's in the release. Wonder what might happen if you disable reuse entirely? (Not performant enough, I guess, but does it show a black screen?)
I've had similar issues (blank screen) with this component, when app came back to the foreground, no LazyColumn or ReusableComposition in my case, but in a dialog. May be related - apparently there are other cases where the component fails in this manner.
asos-luisalcantara commentedon Jun 5, 2025
@bubenheimer I found that the issue was caused by the remember block being called multiple times.
The player is inflated several times causing OOM.
I think AndroidView I think should be updated to take into account when is within a list.
Setting the surface view on update and resetting it on reset as a shared above. Or at least have an example in Exoplayer demo app about it.
Using a pool of player helps to prevent memory exception issues.
@oceanjules – would it be okay if I created a PR to include an example where the player is used within a list? Or is there another way we could include this in the demo?
bubenheimer commentedon Jun 5, 2025
@asos-luisalcantara @oceanjules The main problem I see with the current implementation here is
onReset = {}. This will trigger reuse in a ReusableComposition, but the surrounding code does not support reuse, so things would break. It should sayonReset = null, or don't specify it, or actually support reuse, but that's more work.@asos-luisalcantara I'd hope that your PR would address this.
ReusableComposition would occur in LazyColumn/LazyRow/etc., but also in paging, and other, user-controlled contexts.
media/libraries/ui_compose/src/main/java/androidx/media3/ui/compose/PlayerSurface.kt
Line 89 in 52628e9
lostdawg3 commentedon Jun 10, 2025
Do the videos work if you just place them inside of a lazycolumn (so that the videos are not nested) in order to see if they display correctly?
oceanjules commentedon Jun 11, 2025
After some testing of different
onResetvalues, I do not actually observe noticeable difference in behaviour or performance. Perhaps my setup is not ideal, I'm using a pool of 3 playersLaunchedEffectthat handles cleanup. It manages the association between the Player and the SurfaceView/TextureView. When player changes or becomes null or when the view instance itself changes, we make sure thatclearVideoViewis called on the previously registered player. This is the most important cleanup step for the Player at least.I understand that the docs say
onResetis specified,Viewinstances MAY be reused"onResetcallback"However, after extensive logging and stress testing, I cannot see how opting OUT of view reuse helps the app (even though theoretically I agree that opting out and documenting it is safer than opting in and trying to sort out the mess). I see the black frames with all sorts of
onResetvalues, making me think that the bottleneck is in the large number of players/limited resources like decoders. Or maybe if the player is stopped or its surface is cleared when an item scrolls out of view, it needs to be re-prepared or re-buffered when it comes back. Maybe given that setting a surface on a player and the player rendering to it are asynchronous operations, the delay is too noticeable. MaybesetMediaItem() and prepare()haven't completed etc etc.Realistically, there should be as good of an app architecture as possible to ensure a smooth transition. Maintaining a player pool, presetting a media item, controlling playWhenReady for players who have views out of scope, using a placeholder or thumbnail to cover the black frame and so on.
The problem to this approach is more of a human-hours bottleneck: the current compose demo app is targetting the same features that
PlayerViewsupports - long-form content with minimal center controls, seekabe progress bar, ads markers, settings menu, fullscreen, track selection, subtitles.What you are describing is another initiative that is happening in parallel over at https://github.com/androidx/media/tree/release/demos/shortform - unfortunately it's in Java and xml layouts, but you can see the difficulty of building a tiktok-style app with short videos that benefit from preloading, plus a PlayerPool -- you can see that it still assumes one video per window and yet the complexity is growing. Whereas you are building something similar to Pinterest grid and the issues that come with which video is currently in focus, whether to play multiple videos at the same time, how many videos are on screen at any one point vs the size of the PlayerPool etc.
If you can provide a clear before-and-after with changing the onReset callback (whether through a decrease in logs or even just a more visually pleasing user experience), I would be more convinced.
Leaving my code below for playing around. Click to expand
lostdawg3 commentedon Jun 11, 2025
Is there a recommended limit on how many players should be utilized in a setup like this?
bubenheimer commentedon Jun 12, 2025
@oceanjules using non-null
onResetmeans that aViewinstance previously created byfactorywill (or may) be reused when theAndroidViewcomponent usage occurs within aReusableComposition, when the composition is reused. WhenonResetis null, then theViewinstance is instead recreated viafactorywhen the composition is reused.The code sets the remembered
viewstate as a side effect of callingfactory. Slots (a.k.a.remember) are cleared prior to composition being reused. This means that theviewstate will always be null upon reuse and all access to the View object is lost.Using non-null
onResethere is a bug. It also provides no benefit in this scenario in the first place.There is various Compose API documentation available around reusable compositions that describes its mechanisms, to help shed more light on the behavior I described above, but it's not exactly easy reading.
(Clarification: I am referring to the project's code here, not the code changes by @asos-luisalcantara, those may be reasonable.)
tonihei commentedon Jun 20, 2025
Thanks for pointing out the issue again. We were able to reproduce it and found a way to make
PlayerSurfacereusable inside these lazy containers. Updating the local variable inupdateas done in #2493 (comment) is a key change, but that still left some issue with surface reuse on multiple players.@asos-luisalcantara Just to clarify - I don't think the view instances can leak because the remembered variable in the composable only stays assigned for as long as the composable is used inside your composition. Managing players in a LazyColumn is a slightly different topic, and the main issue was the surfaces were not correctly assigned to the player instances when the AndroidView was reused.
lostdawg3 commentedon Jun 20, 2025
Is there a recommended limit on how many players should be utilized in a setup like this?
tonihei commentedon Jun 20, 2025
This is unrelated to the bug discussed here, but the general recommendation is to only use as many players as needed to play at the same time (e.g. one or two). It's best to treat the player instance as a sparse resource that is better to be reused and limited (The actual resources this applies to are the codecs used inside the player).
lostdawg3 commentedon Jun 20, 2025
I ask due to the use of the number of players in the pool of players that are used in when recycling in order to prevent the black screen. I used the sample code that @oceanjules provided and could see the number of black screens vary when I changed the size of the pool.
Ensure PlayerSurface views can be reused in reusable containers
Ensure PlayerSurface views can be reused in reusable containers