Skip to content

A Simple Video Player for Android Using Media3 ExoPlayer

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.

The default UI of PlayerView.

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.

  1. Create an instance of ExoPlayer using ExoPlayer.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.
  2. Create a MediaItem using the video/audio URL.
  3. Set the MediaItem to the player object.
  4. Set the player’s playWhenReady attribute to true. What this does is that it automatically starts the playback of the video without having to wait for the user to press play.
  5. Call player.prepare(). Now the player will start loading the media file and acquire the resources needed for playback.
  6. 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.

3 thoughts on “A Simple Video Player for Android Using Media3 ExoPlayer”

  1. 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.

  2. Pingback: Implementing Caching for Media3 ExoPlayer - Androiddd

Leave a Reply

Your email address will not be published. Required fields are marked *