Media3 (ExoPlayer) + Compose: Video & Audio Player Implementation
Source: Dev.to
ExoPlayer.Builder Setup
Create and configure an ExoPlayer instance:
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
val exoPlayer = ExoPlayer.Builder(context)
.setTrackSelector(DefaultTrackSelector(context))
.build()
Loading MediaItems
Add one or multiple media items to play:
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
// Single video
val mediaItem = MediaItem.Builder()
.setUri("https://example.com/video.mp4")
.setMimeType(MimeTypes.APPLICATION_MP4)
.build()
exoPlayer.setMediaItem(mediaItem)
// Playlist with multiple items
val playlist = listOf(
MediaItem.fromUri("https://example.com/video1.mp4"),
MediaItem.fromUri("https://example.com/video2.mp4"),
MediaItem.fromUri("https://example.com/audio.mp3")
)
exoPlayer.setMediaItems(playlist)
exoPlayer.prepare()
PlayerView in AndroidView
Embed PlayerView in Compose using AndroidView:
import androidx.media3.ui.PlayerView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
AndroidView(
factory = { context ->
PlayerView(context).apply {
player = exoPlayer
useController = true
controllerShowTimeoutMs = 5000
controllerHideTimeoutMs = 3000
}
},
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
)
Custom Audio Player UI in Compose
Build a custom player UI for audio without video:
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.Alignment
val currentPosition by exoPlayer.currentPosition.collectAsState()
val duration by exoPlayer.duration.collectAsState()
val isPlaying by exoPlayer.isPlaying.collectAsState()
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Play/Pause Button
Button(onClick = {
if (isPlaying) exoPlayer.pause() else exoPlayer.play()
}) {
Text(if (isPlaying) "⏸ Pause" else "▶ Play")
}
// Progress Slider
Slider(
value = currentPosition.toFloat(),
onValueChange = { exoPlayer.seekTo(it.toLong()) },
valueRange = 0f..duration.toFloat(),
modifier = Modifier.fillMaxWidth()
)
// Time Display
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(formatTime(currentPosition))
Text(formatTime(duration))
}
}
fun formatTime(ms: Long): String {
val seconds = (ms / 1000) % 60
val minutes = (ms / 60000) % 60
return "%02d:%02d".format(minutes, seconds)
}
Lifecycle‑Aware Playback
Pause playback when the app goes to the background:
import androidx.compose.runtime.*
import androidx.lifecycle.*
DisposableEffect(Unit) {
val lifecycleObserver = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> exoPlayer.play()
Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
else -> {}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycle.removeObserver(lifecycleObserver)
}
}
Proper Resource Release with DisposableEffect
Release the player when the Compose component is disposed:
DisposableEffect(Unit) {
onDispose {
exoPlayer.release()
}
}
Playlist Management
Control playback flow within a playlist:
// Move to next item
Button(onClick = { exoPlayer.seekToNextMediaItem() }) {
Text("Next Track")
}
// Move to previous item
Button(onClick = { exoPlayer.seekToPreviousMediaItem() }) {
Text("Previous Track")
}
// Repeat modes
IconButton(onClick = {
exoPlayer.repeatMode = when (exoPlayer.repeatMode) {
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
else -> Player.REPEAT_MODE_OFF
}
}) {
Text("Repeat: ${exoPlayer.repeatMode}")
}
Best Practices
- Always use
DisposableEffectto release player resources. - Pause playback when the app goes to the background to save battery.
- Use lifecycle observers for proper state management.
- Handle network errors and provide fallback to local playback.
- Test with various media formats (MP4, WebM, DASH, HLS).
- Provide users with playback controls and progress indication.
Media3 provides a robust foundation for audio and video playback. Combined with Compose’s declarative UI, you can build modern media experiences with minimal boilerplate.
Explore more Android development patterns: 8 Android App Templates →