Home Android & Kotlin Tutorials

MediaPlayer: Simplified Video Playback on Android

Playing videos is a common requirement in Android apps. In this tutorial learn about handling video playback via the MediaPlayer API on Android.

Version

  • Kotlin 1.4, Android 4.4, Android Studio 4.0

Playing audio or video in Android apps is a common requirement for many projects. Many apps in the Google Play Store, even some non-streaming ones, provide audio and video playback. Above all, it’s an important topic that will lead you to many job opportunities.

In this tutorial, you’ll build an Android app that plays video from various sources, such as videos locally stored in your phone, the res folder, gallery and a URL using MediaPlayer.

Along the way you’ll learn about:

  • MediaPlayer.
  • The states of MediaPlayer.
  • Playing video from the res/raw folder.
  • Playing video from a URL.
  • Best practices for MediaPlayer.
  • Digital Right Management.
Note: This tutorial assumes you’re familiar with Kotlin and Android development. If you’re a beginner, check out our Android: Beginners tutorial first.

Getting Started

Download the materials using the Download Materials button at the top or the bottom of this page. Extract and open the starter project in Android Studio.

Build and run. You’ll see something like this:


Starter app

The app isn’t interactive yet because the starter project only consists of UI and some basic code. You’ll implement the functionality throughout this tutorial.

Understanding the code

Before the hands-on part of this tutorial, take some time to understand the codebase you’ll build on. Navigate to these three files and check out their contents:

  1. AndroidManifest.xml: At the top of the manifest file you’ll find android.permission.INTERNET. In this tutorial, you’ll play a video from a URL, so you’ll need the INTERNET permission.
  2. activity_video.xml: This is the one and only layout file in the project. It consists of:
    • A VideoView to play video.
    • A ProgressBar to shows the user it’s loading a video.
    • Two TextViews and a SeekBar to show progress.
    • An ImageButton to play and pause a video.
  3. VideoActivity.kt: This might look a bit overwhelming, but if you skim through and read the comments, you’ll find it quite simple.

    The class implements a few interface classes to manage MediaPlayer and seekBar callbacks. You’ll understand these implementations once you start working on the functionality.

    Also, to keep your app from falling asleep, you’ll need to keep your screen on. Notice this line inside onCreate() which adds a flag window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON). Setting this flag ensures your screen stays on, while the app is in use. You don’t want the user’s device to fall asleep and lock while they’re watching a video on your app, do you?

MediaPlayer

MediaPlayer is a part of the Android multimedia framework that plays audio or video from the resource directory and gallery. It also streams music or video from a URL.

When a media file plays, the MediaPlayer API goes through several states, which are summarized in the diagram below:

MediaPlayer state diagram

That’s a lot of information to take in, but before you can use the MediaPlayer API efficiently, you need to understand these states, so let’s take a look at each of them.

  1. Idle State: MediaPlayer is in an idle state when you first instantiate it, or first create it using the new keyword. You also reach this state after you call reset().

    At this stage, you can’t play, pause or stop the media. If you try to force it, the app might crash.

  2. End State: Calling MediaPlayer‘s release() method frees resources and moves it to the end state. At this stage, you can’t play or pause the media.
  3. Error State: You reach this state if you try to play, pause or stop an uninstantiated MediaPlayer object. However, you can catch the error using MediaPlayer’s onErrorListener.onError() callback.
  4. Initialized State: MediaPlayer reaches this state when you set a data source. To set a one, use setDataSource() method. Be aware that you can only set a data source when MediaPlayer is in an idle state or it’ll throw an IllegalStateException.
  5. Prepared State: Before you play any media from a file or a URL, you need to prepare your MediaPlayer by calling either prepare() or prepareAsync(). Once prepared, it reaches this state and calls onPreparedListener().
  6. Started State: Once MediaPlayer is ready, you can play the media by calling start(). While the music or video plays, MediaPlayer is in a started state.
  7. Paused State: When you pause the media, MediaPlayer is in this state. To pause MediaPlayer, call pause().
  8. Stopped State: MediaPlayer is in this state when media stops playing. If MediaPlayer is in this state and you want to play the media again, you have to prepare it again by calling prepare() or prepareAsync().
  9. PlaybackCompleted State: When the playback is complete, MediaPlayer is in this state. Additionally, invokes onCompletion.onCompletion(). If MediaPlayer is in this state you can call the start() and play audio or video again.

Okay, enough theory. It’s time to code!

Let's code

Playing Video From Local Resources

You will start with playing a video file in the raw directory. There’s a video in the starter project named test_video.mp4.

From the states you learned above, you may see that to play the video in your VideoView, you have to:

  1. Set the data source using MediaPlayer
  2. Call the start() function

But you may be wondering where do you add this code? For this, a couple of options may come to your mind.

The most common way to add new code into Android apps is to put it in the onCreate() method, because you know it will be the entry point of your activity. But in the case of Media Player that’s not good practice for two reasons:

  1. First, you don’t know the size of the video or how much time MediaPlayer will take to load it.
  2. Second, MediaPlayer can play a video in VideoView through its surface, which also takes time.
Note: A surface is a low-level drawing area where you can render images and display them on screen.

Take a look at the code inside the onCreate() method of VideoActivity. In there you will see the following line: video_view.holder.addCallback(this), this gets a callback when the VideoView‘s surface is ready for playing video. When this surface is ready, it calls surfaceCreated().

Next, inside surfaceCreated() replace // TODO (1) with:

mediaPlayer.apply {
 setDataSource(applicationContext, 
    // 1
    Uri.parse("android.resource://$packageName/raw/test_video"))
 // 2
 setDisplay(surfaceHolder)
 // 3
 prepareAsync()
}

In this code, you perform three tasks:

  1. First, you pass the location, URI, of the video.
  2. Then, you set MediaPlayer‘s display to the VideoView‘s surface by calling setDisplay(surfaceHolder).
    Note: If you started the MediaPlayer without calling this function, you wouldn’t see any video because MediaPlayer wouldn’t know where to display the video.
  3. Finally, you call prepare(). This function prepares MediaPlayer to playback the video synchronously on the main thread.

MediaPlayer is prepared

When MediaPlayer is prepared, it invokes onPrepared(). This is the point where you will start playing the video.

To do that, replace // TODO (2) inside onPrepared() with:

// 1
progress_bar.visibility = View.GONE
//2
mediaPlayer?.start()

Here you:

  1. Make the progressBar invisible.
  2. Tell MediaPlayer to start the video.

Now Build and Run.

Playing the video from local resource

Tada! Now you can play the video from your raw or resource directory, but still, something is missing. The seekBar isn’t updating, and you can’t pause or play the video.

Next, you’ll implement the missing functionality and make the app more intuitive.

Improving the UX

Before you add the functionality to play, pause or fast-forward video using SeekBar, you need to create a few functions and extension properties.

At the bottom of VideoActivity.kt replace // TODO (3) with:

// 1
private val MediaPlayer.seconds: Int
 get() {
   return this.duration / SECOND
 }

// 2
private val MediaPlayer.currentSeconds: Int
 get() {
   return this.currentPosition / SECOND
 }

These extension properties help you implement those functionalities. MediaPlayer itself provides you with the video duration and currentPosition, therefore you don’t have to worry for tracking them.

  1. seconds returns the total duration of the video in seconds.
  2. currentSeconds returns the current playback position in seconds of the video.

Next, convert the seconds to a more readable format by replacing // TODO (4) with:

private fun timeInString(seconds: Int): String {
 return String.format(
     "%02d:%02d",
     (seconds / 3600 * 60 + ((seconds % 3600) / 60)),
     (seconds % 60)
 )
}

In this function you convert seconds to MM:SS format. If the video is more than 60 seconds long, it’s better to show 2:32 minutes rather than 152 seconds.

Note: You implemented the MM:SS format but you can add the logic to incorporate HH:MM:SS format as well.

In adition, you’ll create three functions that initialize and periodically update seekbar and convert the seconds to a more readable format.

To initialize seekbar, replace // TODO (5) with:

private fun initializeSeekBar() {
  // 1
  seek_bar.max = mediaPlayer.seconds
  // 2
  text_progress.text = getString(R.string.default_value)
  text_total_time.text = timeInString(mediaPlayer.seconds)
  // 3
  progress_bar.visibility = View.GONE
  // 4
  play_button.isEnabled = true
}

When MediaPlayer prepares to play the video this function is executed. The code performs the following:

  1. Sets the maximum value for SeekBar
  2. Sets default values for TextViews which shows the progress and the total duration of the video.
  3. Hides the ProgressBar.
  4. Enables the play button.

Next, to periodically update the seekbar as the video plays, replace // TODO (6) with:

private fun updateSeekBar() {
 runnable = Runnable {
   text_progress.text = timeInString(mediaPlayer.currentSeconds)
   seek_bar.progress = mediaPlayer.currentSeconds
   handler.postDelayed(runnable, SECOND.toLong())
 }
 handler.postDelayed(runnable, SECOND.toLong())
}

In this function, you use Runnable to execute the code periodically after every one second. Runnable is a Java interface and executes on a separate thread. Since it executes on a separate thread, it won't block your UI and the SeekBar and TextViews will update periodically.

Instead of playing the video when MediaPlayer is ready, it would be better if the user could play and pause the video by using the imageButton.

Inside the onCreate() function replace // TODO (7) with:

play_button.setOnClickListener {
 // 1 
 if (mediaPlayer.isPlaying) {
   // 2
   mediaPlayer.pause()
   play_button.setImageResource(android.R.drawable.ic_media_play)
 } else {
   // 3
   mediaPlayer.start()
   play_button.setImageResource(android.R.drawable.ic_media_pause)
 }
}

Here you:

  1. Check if MediaPlayer is playing any video.
  2. If it is, you pause the video and change the button icon to play.
  3. If not, you play the video and change the button icon to pause.

Next, replace all the code added to the onPrepared()'s body with a call to initalizeSeekBar() and updateSeekBar() which you created earlier:

initializeSeekBar()
updateSeekBar()

Now your app is more intuitive.

Build and Run. You can play or pause the video and see the progress on seekBar and TextView.

Enabling play/pause and progress in app

Interacting with the SeekBar

Now the app is more intuitive for the user, except for the SeekBar. Even though the SeekBar updates with time, you can't fast-forward or rewind the video by tapping or dragging it.

For that, you'll use the onProgressChanged() method of SeekBar. Whenever there's a change in the SeekBar's progress, it will invoke this function.

The SeekBar change listener is already in the code, so navigate to onProgressChanged() and replace // TODO (8) with:

if (fromUser){
  mediaPlayer.seekTo(progress * SECOND)
}

The function onProgressChanged() has three parameters:

  1. seekBar: Instance of the seekBar.
  2. progress: Progress of seekBar in seconds.
  3. fromUser: Boolean which tells you if the change is because of user interaction. If the change in progress is due to user interaction, it'll be true. If not, it'll be false.

    You use this parameter to update MediaPlayer's progress if the seekbar's progress level is manually changed.

Now your user can fast-forward or rewind the video using seekBar. Build and run to give it a try. :]

Enabling seeking in the app

Playing Video from the Gallery

The app works great now, but it would be better if users could select a video from their gallery and play it. You'll implement that next.

The options button in the toolbar and its functionality are already in the starter project.

Before you add new code, you need to understand what's happening in the existing code. When the user selects an option the app invokes onOptionItemSelected() with the menuItem as a parameter.

Now, inside the when statement replace // TODO (9) with:

// 1
val intent = Intent()
// 2
intent.type = "video/*"
// 3
intent.action = Intent.ACTION_GET_CONTENT
// 4
startActivityForResult(Intent.createChooser(intent, getString(R.string.select_file)), GET_VIDEO)

Here you use an intent to get the URI for the file the user selected from the gallery. In the code:

  1. You get an Intent which is a messaging object in Android used to request different action types.
  2. You ensure that the intent type is a video format.
  3. Then you specify this is an Intent with an action of GET content type.
  4. Finally, you trigger the intent waiting for a result.

Playing the video after it has been downloaded

Once the activity returns something from the intent, startActivityForResult() invokes onActivityResult(), which passes GET_VIDEO as a request_code.

Inside the startActivityForResult() function replace // TODO (10) with:

// 1
if (resultCode == Activity.RESULT_OK) {
 // 2
 if (requestCode == GET_VIDEO) {
   // 3
   selectedVideoUri = data?.data!!
   // 4
   video_view.holder.addCallback(this)
 }
}

Here you:

  1. Check the resultCode. If the operation was executed successfully, it returns Activity.RESULT_OK.
  2. Check the requestCode to identify the caller and define if the requestCode was actually a video.
  3. Assign URI to the selectedVideoUri variable you declared earlier.
  4. Invoke surfaceCreated() by calling video_view.holder.addCallback(this).

It will probably ask you to import Activity, so add it at the top with your other inputs with:

import android.app.Activity

Next, update setDataSource() so you pass the correct URI in the parameter. Navigate to surfaceCreated() and replace the function body with:

mediaPlayer.apply {
  setDataSource(applicationContext, selectedVideoUri)
  setDisplay(surfaceHolder)
  prepareAsync()
}

Voila! Now your user can open the gallery, select a video and play it in your app. Build and run to see how it works now.

Playing video selected from gallery

Releasing the Resources

MediaPlayer API consumes a lot of resources. So, you should always release those resources by calling release().

As per the official Android documentation, you should release the resources whenever your app is in background or stopped state. Failing to could lead to continuous battery consumption for the device and playback failure for other apps if multiple instances of the same codec aren't supported. It could also degrade the device's performance in general.

In onDestroy(), replace // TODO (11) with:

handler.removeCallbacks(runnable)
mediaPlayer.release()

Here you remove the runnable from the thread or message queue by calling removeCallbacks(). If you didn't, your runnable would keep executing in the background.

Build and Run. It looks like nothing changed, but you ensured the resources release when the user kills the app.

Playing video selected from gallery

Executing Asynchronously

Asynchronous code is very important for Android apps. You may have not noticed, but currently, you are loading the video asynchronously.

In most cases, videos are heavy and take time to load, therefore, you need to make the code asynchronous. When that happens, MediaPlayer will lead the app to ANR, or Application Not Responding, state. In short, your app will become non-responsive and crash.

To load the media file asynchronously what you did was using prepareAsync() instead of prepare() inside the surfaceCreated() function. In other words, if you try to use prepare() in that line and run the app, it will be there loading for a long time.

Seriously, that's it! That's all you had to do.

Next, you'll implement asynchronously streaming a video from a URL.

Streaming a Video From a URL

Fortunately, you don't have to make many changes to prepare MediaPlayer asynchronously and stream a video from a URL.

In surfaceCreated(), replace setDataSource() with:

setDataSource(URL)

Then, in surfaceCreated() replace prepare() with prepareAsync().

After you make those changes, surfaceCreated() will look like this:

override fun surfaceCreated(surfaceHolder: SurfaceHolder) {
 mediaPlayer.apply {
   setDataSource(URL)
   setDisplay(surfaceHolder)
   prepareAsync()
 }
}

Nice! Now your MyTube app can play videos from res directory, gallery and even stream a video from a URL.

All Done!

Before you finish, there's one more topic you need to explore: DRM, or Digital Right Management.

Digital Right Management

DRM, or Digital Right Management, technologies are a set of tools that protect or encrypt video and audio files. There are many technologies to help with that, such as Microsoft's PlayReady and Google's Widevine. Working with DRM-protected files is a topic worthy of it's own tutorial.

MediaPlayer added support to play DRM protected videos in Android 8 with API 26. If you want to play a DRM protected video, either you have to set your app's minSdkVersion to 26, or annotate your class with @RequiresApi(Build.VERSION_CODES.O).

DRM MediaPlayer

Because DRM mostly relies on the security-by-obscurity principle, it's hard to get your hands on any DRM protected media files. You won't play a DRM protected file as it's too complicated for this tutorial. Instead, you'll see an overview of the changes you have to make to play a DRM protected file.

Take a look at onCreate() again. You'll see this:

mediaPlayer.setOnDrmInfoListener(this)

This line of code tells MediaPlayer it could be accessing a DRM protected file. You don't have to worry about the app crashing if the media file is not DRM protected: It takes care of that itself. If the media file is DRM protected the app invokes onDrmInfo().

After that, inside onDrmInfo() replace // TODO (12) with :

mediaPlayer?.apply {
    // 1
    val key = drmInfo?.supportedSchemes?.get(0)
    key?.let {
        // 2
        prepareDrm(key)
        // 2
        val keyRequest = getKeyRequest(
            null, null, null,
            MediaDrm.KEY_TYPE_STREAMING, null
        )
        provideKeyResponse(null, keyRequest.data)
    }
}

Here you:

  1. Fetch the UUID, or key, of the DRM scheme in use. There are several types of DRM protection schemes. Examine the map of UUIDs in drmInfo?.supportedSchemes?.get(0) and retrieve the supported schemes. Schemes supported depend mainly on the device and OEM, if you ever get a DRM, you will need to dive deeper on this topic.
  2. Pass this retrieved key to prepareDrm() to prepare the DRM.
  3. Get the decrypt key from the licensed server using getKeyRequest().
  4. Then provide the key response from the license server to the DRM engine plugin using provideKeyResponse().

Android Studio will most likely ask you to import the DRM library, so add the following line to the top of your file with your other imports:

import android.media.MediaDrm

This code is only for demonstration purposes. Because of this, it won't affect the app since you don't have access to any DRM protected files.

If you ever have to work with a DRM protected media file, a better alternative would be using ExoPlayer because it provides more functionalities.

All done!

Where To Go From Here?

Congratulations on your first MediaPlayer app! You can download the final project using the Download Materials button at the top or bottom of this tutorial.

In this tutorial, you covered some basics of MediaPlayer like:

  • Understanding MediaPlayer and its states.
  • Playing video from the res/raw folder.
  • Playing video from a URL.
  • Async Programming.
  • Best practices for media player.
  • Digital Right Management.

While you covered a lot in this tutorial, there's still more to learn. For a next step, explore ExoPlayer. Google created ExoPlayer, a library that lets you take advantage of features not yet supported by MediaPlayer API, such as DASH and SmoothStreaming. To learn more about ExoPlayer check out our ExoPlayer tutorial.

I hope you enjoyed this tutorial. If you have any questions or comments, please join the discussion below. :]

Add a rating for this content

More like this

Contributors

Comments