I’m working on an app where a video player is embedded inside a bottom sheet. When I scroll within the bottom sheet, the video unexpectedly goes out of bounds and overflows outside of the bottom sheet’s visible area. I need the video to remain within the bottom sheet while scrolling, without disrupting the layout.
Here’s the video of the issue I’m facing.
I created a composable video player using this code:
@OptIn(UnstableApi::class)
@Composable
fun VideoPlayer(
modifier: Modifier = Modifier,
file: File,
) {
val context = LocalContext.current
val exoPlayer = remember { ExoPlayer.Builder(context).build() }
// Get the video URI from internal storage
val correctedFile = file.takeIf { it.name.endsWith(".mp4") } ?: File(file.parent, "${file.name}.mp4")
val videoUri = Uri.fromFile(correctedFile)
// Set the media source
val mediaItem = MediaItem.Builder()
.setUri(videoUri)
.setMimeType(MimeTypes.VIDEO_MP4) // Specify the correct MIME type
.build()
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
exoPlayer.playWhenReady = true
// Add a listener to log errors
exoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
Timber.e("VideoPlayer", "Player error: ${error.message}")
}
})
// Use AndroidView to integrate ExoPlayer's PlayerView
AndroidView(
modifier = modifier
.clipToBounds(),
factory = { ctx ->
PlayerView(ctx).apply {
player = exoPlayer
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
}
})
// Clean up the ExoPlayer when the composable is disposed
DisposableEffect(Unit) {
onDispose {
exoPlayer.release()
}
}
}
I initially suspected the issue was related to AndroidView
, but after replacing PlayerView
with an ImageView
, everything worked as expected. This suggests the problem is specifically tied to PlayerView
.
Currently, I’m using a Column
with vertical scrolling to populate the content of the bottom sheet. I also tried using LazyColumn
, but the same issue occurred.
Does anyone know what might be causing this behavior with PlayerView
? How can I resolve it and ensure the video player stays correctly within the bottom sheet during scrolling?
Try to use TextureView
for the player surface.
In compose it is AndroidEmbeddedExternalSurface
.
You can find the relevant code on how to do that in the sample:
https://github.com/androidx/media/blob/release/demos/compose/src/main/java/androidx/media3/demo/compose/PlayerSurface.kt
SurfaceView
vs TextureView
As mentioned by @sdex in this specific case we need to use TextureView
instead of SurfaceView
. Here’s a quick break down on why:
SurfaceView
:
-
Uses a dedicated surface that is drawn outside the view hierarchy.
-
It operates in a separate drawing thread, making it more efficient for
performance-heavy tasks like video playback. -
Does not support transparency natively. It’s always drawn over the
other views unless special handling is applied.
TextureView
:
-
Operates within the view hierarchy, which means it behaves like a
regular View and is rendered within the app’s UI thread. -
Uses OpenGL for rendering, making it flexible for transformations
like scaling, rotating, and applying effects. -
It may not perform as efficiently as SurfaceView for video playback,
because it runs in the same thread as the UI.
Solution
Since I wanted to apply a center crop to the video and it’s difficult achieve a good proportion using Matrix
, I defined PlayerView
inside xml, like this:
<?xml version="1.0" encoding="utf-8"?>
<androidx.media3.ui.PlayerView android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:use_controller="false"
app:resize_mode="zoom"
xmlns:android="http://schemas.android.com/apk/res/android"
app:surface_type="texture_view"
android:id="@+id/playerView"/>
I had to create a xml file since surface_type
cannot be set dynamically from Kotlin code.
Now the VideoPlayer
looks like this:
@OptIn(UnstableApi::class)
@Composable
fun TextureVideoPlayer(
modifier: Modifier = Modifier,
file: File,
onPlayerReady: (playWhenReady: Boolean) -> Unit = {},
onPlayerError: (error: PlaybackException) -> Unit = {},
viewModel: VideoPlayerViewModel = hiltViewModel(),
) {
LaunchedEffect(file) {
viewModel.setMediaItem(file)
}
AndroidView(
modifier = modifier,
factory = { ctx ->
val view = LayoutInflater.from(ctx).inflate(R.layout.player_view, null, false)
val textView = view.findViewById<PlayerView>(R.id.playerView)
textView.player = viewModel.exoPlayer
textView
},
update = { view ->
val playerView = view as PlayerView
playerView.player = viewModel.exoPlayer
}
)
DisposableEffect(Unit) {
val listener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
onPlayerReady(playbackState == Player.STATE_READY)
}
override fun onPlayerError(error: PlaybackException) {
Timber.e("Player Error: ${error.errorCode}")
viewModel.onPlayerError(error)
onPlayerError(error)
super.onPlayerError(error)
}
}
viewModel.exoPlayer.addListener(listener)
onDispose {
viewModel.exoPlayer.removeListener(listener)
}
}
}
I need to give credit to Pzychotix that put me on the right track to solve the issue.