Media3 (ExoPlayer) + Compose: Video & Audio Player Implementation

Published: (March 1, 2026 at 08:18 PM EST)
3 min read
Source: Dev.to

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 DisposableEffect to 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 →

0 views
Back to Blog

Related posts

Read more »