Sitemap

Building an Android Video Player with Media3 and Jetpack Compose

7 min readMay 6, 2025

Thanks to the Media3 library, Jetpack Compose finally gets native support for media playback. After experimenting with the new PlayerSurface API, I decided to share a beginner-friendly guide for those starting their journey into video playback on Android.

This is not an advanced tutorial. Instead, it’s meant to help you take your first steps with Media3 in Jetpack Compose. If you’re already experienced, this post might feel basic , but feel free to contribute in the comments!

The Media3 Library

Maybe you haven’t heard about Media3 yet, so let’s give it a little introduction. The Media3 library is the new and official library for Android media playback development. It was released in March 2023 as a new home for Audio and Video APIs [1]. It simplifies the creation of a media player by unifying Exoplayer and Media2 and expanding to cover new use cases, such as video editing and other features.

The Media3 Compose

Until recently, integrating Media3 with Jetpack Compose required workarounds. You had to use an AndroidView component that was responsible for inflating the XML view, and I already got some bugs because of it. Now you can use a compose component directly.

Your First Player

By the end of this post, you’ll have built your first video player using Jetpack Compose. Not everyone takes time to explore how things work, so congrats on being curious!

For this post, I've chosen to replicate the layout of the Netflix player. We won't be developing every feature it has, but we'll focus on the main ones that every player should have. This practical approach is designed to keep you engaged and focused on the player's essential elements.

Image from: UX Design

Hands On

First, we need to add some dependencies to work with Media3, so go ahead and create a new Android project or open one you already have and add the following dependencies.

dependencies {
val media3_version = "1.6.1"

// Common functionality used across multiple media libraries
implementation("androidx.media3:media3-common:$media3_version")

// For building media playback UIs using Jetpack Compose
implementation("androidx.media3:media3-ui-compose:$media3_version")

// For media playback using ExoPlayer
implementation("androidx.media3:media3-exoplayer:$media3_version")
}

You can find all Media3 libraries on the Android developers’ site [2]

Once you have your dependencies set up, you can start creating the player, so let's start by creating a simple component with a PlayerSurface from the media3-ui-compose.

@Composable
fun NetflixPlayer(
player: Player,
media: MediaDataHolder,
modifier: Modifier = Modifier,
) {
Box(modifier) {
PlayerSurface(
player = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
)
}
}

The PlayerSurface is the view that reproduces your media playback. It replaces the PlayerView from XML-based code.

And I will also create a player screen that will use the NetflixPlayer

data class MediaDataHolder(
val uri: String,
val title: String,
)

@Composable
fun StandAlonePlayerScreen(
media: MediaDataHolder,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
var player by remember {
mutableStateOf<Player?>(null)
}

LifecycleStartEffect(Unit) {
player = ExoPlayer.Builder(context).build().apply {
setMediaItem(MediaItem.fromUri(media.uri))
prepare()
play()
}

onStopOrDispose {
player?.release()
player = null
}
}

player?.let {
NetflixPlayer(
player = it,
media = media,
modifier = modifier.fillMaxSize(),
)
}
}

Okay, this basic screen receives a MediaDataHolder, a data class containing all the information about the media you want to play. For now, it is just the media URI and title.

On the player screen, you will initialize the player by using the Exoplayer Builder and creating a MediaItem from the media URI. Then, call the prepare method and play method if you want it to start when you open your screen.

Now you need to start your screen passing a MediaDataHolder for it.

val media by remember {
mutableStateOf(
MediaDataHolder(
uri = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
title = "Big Buck Bunny"
)
)
}

StandAlonePlayerScreen(media)

If you run your app, you should get a video on your screen. But it has no control; you can't play or pause the video, so let's add a basic control for the player.

This part will be a boilerplate copied from the Media3 Compose demo [3]. The whole code is on its GitHub repository. The only things new here are the title and the time bar.

You will see how to add the play and pause buttons here. The rest of the controls will follow the same structure, which will be easy to follow once you understand.

In the NetflixPlayer, you need to add a flag that will control whether to show or not the player controllers, and modify the PlayerSurface to be clickable, toggling the showControls flag.

Also, you need to set up the content scale for the video, and you can do it by getting the presentationState using the rememberPresentationState and setting up the resizeWithContentScale.

@OptIn(UnstableApi::class)
@Composable
fun NetflixPlayer(
player: Player,
media: MediaDataHolder,
modifier: Modifier = Modifier,
) {
var showControls by remember { mutableStateOf(true) }
val presentationState = rememberPresentationState(player)

Box(modifier) {
PlayerSurface(
player = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = Modifier
.resizeWithContentScale(
ContentScale.Fit,
presentationState.videoSizeDp
)
.clickable(
interactionSource = remember {
MutableInteractionSource()
},
indication = null,
) {
showControls = !showControls
},
)
}
}

Now, the player is ready to receive their controls. As mentioned, you will add the play and pause buttons, which are the same as those in the Demo app.

The Media3-ui-compose exposes a remember state method for its controls. For play and pause, you have the rememberPlayPauseButtonState, which returns if the future is enabled and which button you should show.

If you look inside the code, you will notice that it creates a PlayPauseButtonState class that listens to the player events and updates the state based on them.

In the end, you will need only a component with a state, and the icon will be changed based on its state.

@Composable
internal fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
val state = rememberPlayPauseButtonState(player)
val icon = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause

IconButton(
onClick = state::onClick,
modifier = modifier,
enabled = state.isEnabled
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier,
tint = Color.White
)
}
}

Now, go back to the NetflixPlayer and add the controls inside the parent Box component.

@Composable
fun NetflixPlayer(
player: Player,
media: MediaDataHolder,
modifier: Modifier = Modifier,
) {
var showControls by remember { mutableStateOf(true) }
val presentationState = rememberPresentationState(player)

Box(modifier) {
PlayerSurface // [...]

if (showControls) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(0.1f))
) {
MediaInfo(
media, Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
)

MinimalControls(player, Modifier.align(Alignment.Center))
}
}
}
}
@Composable
fun MediaInfo(
media: MediaDataHolder,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
text = media.title,
style = MaterialTheme.typography.titleSmall,
color = Color.White
)
}
}
@Composable
fun MinimalControls(player: Player, modifier: Modifier = Modifier) {
val graySemiTransparentBackground = Color.Gray.copy(alpha = 0.1f)
val modifierForIconMutton = modifier
.size(80.dp)
.background(graySemiTransparentBackground, CircleShape)

Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
PlayPauseButton(player, modifierForIconMutton)
}
}

Here, you centralized the play/pause button on your video screen and added the title info at the top of the video.

The last thing you will implement is the Timebar. By the time I wrote this post, I had not found an official component like the DefaultTimebar XML-based UI. I came up with a non-production timebar just for the post proposal. I don't recommend using it in production. It will follow the same principle as the play/pause remember state, so I created a rememberPlayerPositionState and a DefaultTimeBar component. Then, I can use it in my PlaybackTimeBar component, which will have a state and a Timebar.

@Composable
fun PlaybackTimeBar(player: Player, modifier: Modifier = Modifier) {
val state = rememberPlayerPositionState(player)

Row(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp
),
verticalAlignment = Alignment.CenterVertically
) {
DefaultTimeBar(
currentPosition = state.position,
bufferingPosition = state.currentBufferedPosition,
duration = state.currentDuration,
onSeek = { position ->
state.seekTo(position)
}
)
}
}

Now, back on the player component, you can add the time bar and run the player to see what you got.

if (showControls) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(0.1f))
) {
// [...]

ExtraControls(
player,
Modifier.fillMaxWidth()
.align(Alignment.BottomCenter)
.navigationBarsPadding()
)
}
}
@Composable
internal fun ExtraControls(player: Player, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
PlaybackTimeBar(player, modifier)
}
}

As you can see, we have a player with basic features, I would like to go a little deeper and talk about media session and other things, but this post is quite big already, I will try to create another post introducing new topics, but for now, I will stop here, and let you with a chalange to add the forward and rewind button like in the netflix player screenshoot.

Conclusion

Would I use Media3 Compose in production? Not yet. I think it has some room to evolve and some basic components that need to be created, but when we have an official way to handle all the UI components we need in a player, I will probably migrate to Media3-compose-ui.

I’ll stop here and leave you with a challenge: try adding forward and rewind buttons, like in the Netflix screenshot.

If you want to see the whole implementation, you can find it in the GitHub repository:
https://github.com/ZaqueuLima3/media3-demo

--

--

Zaqueu Santos
Zaqueu Santos

Written by Zaqueu Santos

A Software Engineer working with Android development @Hotmart

No responses yet