
Seamless Multimedia in Android: Mastering ExoPlayer with Kotlin
The definitive guide to integrating high-performance audio and video playback into your Android application using the modern Media3 ExoPlayer library and Kotlin.
In today’s media-centric world, a robust, high-quality media player is non-negotiable for any top-tier Android application. While Android’s native MediaPlayer exists, the Google-backed ExoPlayer (now part of the Jetpack Media3 library) is the de facto choice for professional developers. It offers advanced features like DASH and HLS adaptive streaming, easy customization, and predictable behavior across devices.
This guide dives into setting up and using ExoPlayer with Kotlin, focusing on best practices for a smooth, performant, and lifecycle-aware implementation.
🚀 Getting Started: Setup and Permissions
Before any playback can begin, we need to add the necessary dependencies and grant our app internet access. We’ll be using the modern androidx.media3 libraries.
1. Dependencies in build.gradle.kts (Module: app)
The core and UI components are essential for a basic player.
// Define the media3 version once
val media3Version = "1.8.0" // Always check for the latest stable version!
dependencies {
// Core ExoPlayer functionality
implementation("androidx.media3:media3-exoplayer:$media3Version")
// UI components, including the PlayerView
implementation("androidx.media3:media3-ui:$media3Version")
// Optional: For HLS/DASH streaming support (recommended)
implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
implementation("androidx.media3:media3-exoplayer-dash:$media3Version")
}
2. Internet Permission in AndroidManifest.xml
Since we’re streaming media over the network (a common use case), this permission is mandatory.
<uses-permission android:name="android.permission.INTERNET" />
🖼️ Player UI: The PlayerView
The PlayerView is a dedicated UI component that handles rendering video and displaying playback controls (like play/pause, seek bar, etc.).
In your layout file (e.g., activity_main.xml):
<androidx.media3.ui.PlayerView
android:id="@+id/video_player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:resize_mode="fit"
app:show_timeout="5000"
app:use_controller="true" />
app:resize_mode=”fit”: Ensures the video scales to fit the view without cropping, maintaining aspect ratio.app:use_controller=”true”: Automatically includes the default playback controls.
🎬 Kotlin Implementation: Lifecycle-Aware Playback
A crucial best practice is to initialize the player when your activity or fragment is visible and release it when it’s not, to save system resources and battery.
1. Declaring and Initializing the Player
We’ll use a late-initialized variable for the player.
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import android.net.Uri
class PlaybackActivity : AppCompatActivity() {
private var player: ExoPlayer? = null // Use nullable property for proper lifecycle management
private lateinit var playerView: PlayerView
// Private properties to store playback state across lifecycle events
private var playbackPosition = 0L
private var playWhenReady = true
// Example URLs - use your own valid streaming links!
private val videoUrl = "https://example.com/stream/adaptive_video.m3u8"
private val audioUrl = "https://example.com/stream/podcast_episode.mp3"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_playback)
playerView = findViewById(R.id.video_player_view)
}
// ... [onStart(), onResume(), onPause(), onStop(), onDestroy() below] ...
}
2. The initializePlayer() Function
This centralized function creates and configures the ExoPlayer instance.
private fun initializePlayer() {
if (player == null) {
// 1. Create the ExoPlayer instance
player = ExoPlayer.Builder(this)
// Optional: Configure player options for optimal performance
.setHandleAudioBecomingNoisy(true) // Automatically pause when headphones are unplugged
.build()
.also { exoPlayer ->
// 2. Attach the player to the UI view
playerView.player = exoPlayer
// 3. Define the media to play
// Using the HLS stream URL for adaptive video playback
val videoMediaItem = MediaItem.fromUri(Uri.parse(videoUrl))
// OR for audio-only:
// val audioMediaItem = MediaItem.fromUri(Uri.parse(audioUrl))
// 4. Set the media item and restore state
exoPlayer.setMediaItem(videoMediaItem)
exoPlayer.playWhenReady = playWhenReady
exoPlayer.seekTo(playbackPosition)
// 5. Prepare and start loading the media
exoPlayer.prepare()
}
}
}
3. Lifecycle Management (The Key to Performance) 🔑
This is the most critical part for production apps. We use a combination of onStart(), onResume(), onPause(), and onStop() to manage the player’s state efficiently, especially handling API level differences for multi-window support.#
private fun releasePlayer() {
player?.let { exoPlayer ->
// Save current state before releasing
playbackPosition = exoPlayer.currentPosition
playWhenReady = exoPlayer.playWhenReady
// Release resources
exoPlayer.release()
}
player = null
}
// Initialization in lifecycle methods
override fun onStart() {
super.onStart()
// API 24 (Nougat) and above support multi-window,
// so we initialize here to ensure playback resumes when visible.
if (Build.VERSION.SDK_INT > 23) {
initializePlayer()
}
}
override fun onResume() {
super.onResume()
// On API 23 and below, the video surface may be destroyed in onPause().
// We initialize here to ensure the player is ready.
if (Build.VERSION.SDK_INT <= 23 || player == null) {
initializePlayer()
// If it's an audio-only app, you might hide the PlayerView's surface on resume
// playerView.onResume() // Note: PlayerView handles its own surface lifecycle
}
}
// Releasing in lifecycle methods
override fun onPause() {
super.onPause()
// On API 23 and below, release the player immediately in onPause()
// to free up resources as the app loses focus.
if (Build.VERSION.SDK_INT <= 23) {
releasePlayer()
}
}
override fun onStop() {
super.onStop()
// API 24 and above only release the player in onStop()
// because the app might still be visible in multi-window mode during onPause().
if (Build.VERSION.SDK_INT > 23) {
releasePlayer()
}
}
💡 Enhanced Feature: Playing a Playlist (Advanced)
ExoPlayer excels at playlist management. Instead of one MediaItem, you can queue multiple.
// In your initializePlayer() or a separate function:
private fun setupPlaylist() {
val trailer = MediaItem.fromUri(Uri.parse("https://example.com/trailers/trailer.mp4"))
val featureFilm = MediaItem.fromUri(Uri.parse("https://example.com/movies/film.m3u8"))
val adBreak = MediaItem.fromUri(Uri.parse("https://example.com/ads/ad.mp4"))
player?.addMediaItems(listOf(trailer, featureFilm, adBreak))
player?.prepare()
}
// Enhanced control: loop the entire playlist
fun togglePlaylistLooping(loop: Boolean) {
player?.repeatMode = if (loop) {
ExoPlayer.REPEAT_MODE_ALL
} else {
ExoPlayer.REPEAT_MODE_OFF
}
}
Developer Comment: Using player?.addMediaItems() is far more efficient than manually managing MediaSource concatenations. This built-in playlist feature simplifies queueing content and handling seamless transitions.
Job Offers
🌐 Best Practices for Media Apps
Optimizing for the user experience is the best for a mobile app. Here are critical points that affect user retention and store ratings:
- Reduce Startup Latency: Use Pre-warming (calling
player.prepare()slightly before the media is in view, like in a RecyclerView) to reduce “time-to-first-frame.” - Handle Audio Focus: Always configure
setAudioAttributes()to respect system audio focus (e.g., pause when a phone call comes in). - Adaptive Streaming: Prioritize DASH/HLS (
.m3u8or.mpdfiles) over plain MP4. This allows ExoPlayer to dynamically select the best bitrate based on network conditions, minimizing frustrating re-buffering (which dramatically impacts user retention). - Error Handling: Implement a
Player.Listenerto log and report playback errors. For example, if a video URL is broken, log the error and show a user-friendly message.
// Example of error logging
player?.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
Log.e("ExoPlayerError", "Playback failed: ${error.message}")
// Show a "Something went wrong" UI element
}
// Implement other essential listeners here (e.g., onPlaybackStateChanged)
})
Mastering ExoPlayer’s lifecycle and embracing adaptive streaming is the cornerstone of building a high-quality, professional Android media application.
Advanced Topics
For deeper insights into optimization and advanced streaming techniques with ExoPlayer, the article suggests two additional resources:
- Boost Your Android Video App: Mastering Media3 Preloading for Instant Playback (Part 1)🚀
- Elevate Your Android Video Feed: A Deep Dive into Media3’s Advanced PreloadManager (Part 2) 🚀
You can learn more about how to optimize media streaming with ExoPlayer. Optimize Media Streaming with ExoPlayer This video provides deeper insights into using the Jetpack Media3 APIs and ExoPlayer for high-performance streaming.
If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏
This article was previously published on proandroiddev.com



