ExoPlayer is among the most popular libraries used in Android for media playback. In this tutorial, we will build a video player activity for your Android app using ExoPlayer, which is now part of Google’s AndroidX Media3.
This solution ensures that the video playback continues seamlessly even during configuration changes, like when the screen rotates.
Add Dependencies
In your module level build.gradle
or build.gradle.kts
file, add the required dependencies –
implementation("androidx.media3:media3-exoplayer:1.4.1")
implementation("androidx.media3:media3-ui:1.4.1")
You can check for the latest version over here –https://github.com/androidx/media/blob/release/RELEASENOTES.md
Create a new Activity
I’ve used view-binding in my project and this example, but this is completely optional and is recommended to speed up your development.
Now, create a new Activity manually or directly using the menu bar in Android Studio with File -> New -> Activity -> Empty Views Activity.

Ready-made UI Options
Media3 provides an out-of-the-box simple UI for media playback.
PlayerView which extends the PlayerControlView can be used for both video, image and audio playbacks. It renders video and subtitles in the case of video playback (if a valid subtitle file is present).
You can include it in your layout files like any other UI component. For example, a PlayerView
can be included with the following XML:
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_buffering="when_playing"
app:show_shuffle_button="true"/>
It offers many customizable properties like custom icons for the button controls, fullscreen icons, shuffle buttons, skip, fast forward/play back speed and a lot more. You can read more about its customisable attributes over here.
Add PlayerView to your UI
Now, let’s head to the newly created activity’s layout resource file and add a full screen PlayerView.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".VideoPlayerActivity">
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_buffering="when_playing"
app:use_controller="true"
app:resize_mode="fit" />
</LinearLayout>
Over here, we’ve used the show_buffering
, use_controller
and resize_mode
attributes. I’d recommend playing around with them to get a good idea over all the functionalities provided by PlayerView.
Setup the Player
Now, go to your Activity and fetch the URL of the video you want to play. Here, I’m using a sample video from Google – https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
Next, let’s follow the steps mentioned below to see our first video playback.
- Create an instance of
ExoPlayer
usingExoPlayer.Builder
. There are many customisable attributes over here, such as adding caching, seeking modes, video scaling, and so on. But, for now, let’s go with the simple, default builder. - Create a
MediaItem
using the video/audio URL. - Set the
MediaItem
to theplayer
object. - Set the player’s
playWhenReady
attribute totrue
. What this does is that it automatically starts the playback of the video without having to wait for the user to press play. - Call
player.prepare()
. Now the player will start loading the media file and acquire the resources needed for playback. - Bind the player created in step 1 with the
PlayerView
in our layout file.
private lateinit var binding: ActivityVideoPlayerBinding
private var player: ExoPlayer? = null
private var playWhenReady = true
private var videoUrl: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
videoUrl = intent.getStringExtra("videoUrl")
initializePlayer()
}
private fun initializePlayer() {
if (player == null && videoUrl != null) {
player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse(videoUrl)))
playWhenReady = this@VideoPlayerActivity.playWhenReady
prepare()
}
binding.playerView.player = player
}
}
Hit Run on Android Studio and now, you should be seeing the video play. Nice.
But wait. Try pressing back. Try changing your phone from landscape to portrait or vice versa. Press the power button. Or exit the app and reopen. You might notice that it isn’t working as expected. The video keeps restarting, the audio doesn’t stop when you leave the screen and you might see some other errors.
Why is this happening? This is because the player instance remains in memory even when you’ve moved out. To fix this, we should release and restart the player appropriately based on the lifecycle states.
Let’s also save the playback position (i.e. the position in the current content, in milliseconds) so that we can restart at that position.
override fun onStart() {
super.onStart()
initializePlayer()
}
override fun onResume() {
super.onResume()
if (player == null) {
initializePlayer()
}
}
override fun onPause() {
super.onPause()
releasePlayer()
}
override fun onStop() {
super.onStop()
releasePlayer()
}
private fun releasePlayer() {
player?.let { exoPlayer ->
playbackPosition = exoPlayer.currentPosition
playWhenReady = exoPlayer.playWhenReady
exoPlayer.release()
player = null
}
}
We’ve moved the initializePlayer()
function from onCreate()
to onStart()
. If you’re wondering why, this is because of the way Android handles Activity Lifecycle callbacks. Assume you start playing the video, and then you lock your phone by pressing the power button. Now, unlock the phone and when the activity is seen, onCreate()
would not be invoked. This is because the activity was not destroyed, just stopped.
If the activity is stopped and started again, the player will be reinitialized in onStart()
.
In some rare scenarios, it’s possible for onResume()
to be called without onStart()
being called first, especially if the activity process was killed in the background due to memory pressure. Also, if you’re using multi-window mode or PiP (Picture-in-Picture), the lifecycle can behave differently. So, as a defensive measure, we also check and restart the player in onResume()
, but this is completely optional as per your use case.
This approach avoids redundant initialization ( if (player == null)
) while still ensuring that the player is available when needed. It also maintains the ability to pause and resume playback when the device is locked and unlocked.
With the code above, we release the player when we move out of the app/activity or if the device is locked. And we restart it when the user reopens the app/activity.
We’ve also modified the initializePlayer()
function to start at the given playbackPosition
. This way the playback resumes from the last position.
private var playbackPosition = 0L
private fun initializePlayer() {
if (player == null && videoUrl != null) {
player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse(videoUrl)))
playWhenReady = this@VideoPlayerActivity.playWhenReady
seekTo(playbackPosition)
prepare()
}
binding.playerView.player = player
}
}
Additionally, we need to modify our Manifest
file to ensure our activity is not recreated for config changes, like for screen rotation. This configuration tells Android that your activity will handle orientation changes and screen size changes itself, rather than being destroyed and recreated by the system.
<activity
android:name=".VideoPlayerActivity"
android:exported="false"
android:configChanges="orientation|screenSize"/>
Complete Code
Sharing the complete code for you to copy and use.
package com.androiddd.exovideoplayer
import android.net.Uri
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.androiddd.exovideoplayer.databinding.ActivityVideoPlayerBinding
class VideoPlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityVideoPlayerBinding
private var player: ExoPlayer? = null
private var playbackPosition = 0L
private var playWhenReady = true
private var videoUrl: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
videoUrl = intent.getStringExtra("videoUrl")
}
private fun initializePlayer() {
if (player == null && videoUrl != null) {
player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse(videoUrl)))
playWhenReady = this@VideoPlayerActivity.playWhenReady
seekTo(playbackPosition)
prepare()
}
binding.playerView.player = player
}
}
override fun onStart() {
super.onStart()
initializePlayer()
}
override fun onResume() {
super.onResume()
if (player == null) {
initializePlayer()
}
}
override fun onPause() {
super.onPause()
releasePlayer()
}
override fun onStop() {
super.onStop()
releasePlayer()
}
private fun releasePlayer() {
player?.let { exoPlayer ->
playbackPosition = exoPlayer.currentPosition
playWhenReady = exoPlayer.playWhenReady
exoPlayer.release()
player = null
}
}
}
Demo Video
You can also find the complete code on GitHub over here – https://github.com/androiddd-com/exo-video-player-demo.
Conclusion
In this article, we’ve implemented a decently working implementation of video player. In the following articles, let’s look at more advanced concepts like – customising the video player, implementing caching, having PIP modes and a lot more.
If you have any questions, feel free to drop a comment below or raise an issue on GitHub. Thanks. Have a good day.
Could you please write an article on how to implement caching? While using the default builder is simple, I’ve been struggling with implementing caching over here.
Hi Arya,
will be dropping a new article this week on it. Thanks for the suggestion.
Is there any specific issue you’re stuck with?
Pingback: Implementing Caching for Media3 ExoPlayer - Androiddd