We all love to listening music and watching videos with our phones, but how does it work? The answer is multimedia players. There are a few of them, they differ in complexity and provide different possibilities for customization. The most popular one that is not part of the Android framework is ExoPlayer, but it's too complex for us now, so it will be better to take a look at the MediaPlayer API first, which is part of the Android framework.
In this topic, we will discuss only the audio capabilities of MediaPlayer.
How to play a song
MediaPlayer supports different data sources such as:
App resources
Local URIs using a Content Resolver
External URLs
Here is the easiest way to obtain media from app resources:
var mediaPlayer = MediaPlayer.create(applicationContext, R.raw.song)
mediaPlayer.setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
mediaPlayer.start()
We create an instance with the MediaPlayer.create() method and start playback with the start() method. We get our file from R.raw, which is located in src/main/res/raw and can contain files without specifying any particular type. However, the file should be properly encoded and formatted in one of the supported formats.
You can use setAudioAttributes() to set attributes that provide information about the audio stream. The two attributes we used in the snippet are:
Usage, which describes why you are playing a sound, for example,USAGE_MEDIAandUSAGE_ALARM.ContentType, which describes the type of content you're playing, for exampleCONTENT_TYPE_MOVIEandCONTENT_TYPE_MUSIC.
You can explore other types of AudioAttributes if you are interested. While MediaPlayer can work without specifying the content type, it's better to include them. The platform or audio framework can use this information to adjust volume levels or apply post-processing to improve audio quality.
To play media from an internal URI, you can use this snippet:
val myUri: Uri = .... // initialize Uri here
val mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(applicationContext, myUri)
prepare()
start()
}Here we have two new methods: setDataSource() and prepare(). We'll discuss prepare() later when we cover MediaPlayer states. Let's look at setDataSource() first:
We use setDataSource() to specify which content to play. When using an internal URI, we need to include the Context.
To play media from a remote URL, the code looks similar, but you don't need to include Context:
val url = "http://........" // your URL here
val mediaPlayer = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(url)
prepare()
start()
}When using this method to obtain media, the prepare() call might take a long time. Consider using prepareAsync(), which we'll discuss later in this topic.
This method requires an internet connection, so add this permission to your manifest file:
<uses-permission android:name="android.permission.INTERNET" />Managing the state
An important aspect of MediaPlayer that you should keep in mind is that it is state-based. MediaPlayer has an internal state that you should always be aware of when writing code. Some operations are valid only when the player is in a specific state. There is a diagram of possible MediaPlayer states and the methods that can move from one state to another:
However, this scheme is complex and contains a lot of information, so let's examine each of these states.
Idle.
MediaPlayeris in this state when you first create it or callreset().Initialized.
MediaPlayerreaches this state when you set a data source. If you try to set a data source while not in the idle state, anIllegalStateExceptionoccurs.Prepared. Before you can play any media, you need to prepare
MediaPlayerby callingprepare(). You can also callprepareAsync(), which we will discuss later in the topic. However, if you useMediaPlayer.create()for creation as we did in the first example, yourMediaPlayerwill start in the prepared state.Started. Once your
MediaPlayeris prepared, you can callstart()to reach this state. While the media plays, theMediaPlayerremains in the started state.Paused. You can pause your media by calling
pause().Stopped.
MediaPlayerenters this state when the media is stopped. To play it again, you need to prepare it first.PlaybackCompleted. When the media finishes playing,
MediaPlayerenters this state. You can play the media again by callingstart()and set a callback viasetOnCompletionListener().End.
MediaPlayermoves to this state after you callrelease(), which we will discuss later. In this state, you cannot play or pause media.Error. If you try to perform a playback control operation in an illegal state,
MediaPlayerenters this state. You can catch the error by setting a callback viasetOnErrorListener()
As you can see, there are many different states and possible transitions between them. However, some methods won't change the state. For example, seekTo(), which searches the media to the given time code, doesn't affect the state.
Using MediaPlayer efficiently
Using MediaPlayer can be straightforward, but you need to follow some important guidelines to use it correctly. Since calling prepare() can take a long time to fetch and decode data, you should never call it from the UI thread. This could cause the UI to hang and even trigger an ANR. Instead, use prepareAsync() to start preparing your data in the background. When the preparation finishes, it will trigger a callback that you can configure with setOnPreparedListener()
This example shows how to start asynchronous preparation and play the media as soon as it's ready:
val url = ...
val mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setOnPreparedListener {
start()
}
prepareAsync()
} As you remember, when we create MediaPlayer with MediaPlayer.create(), we should not call prepare() because the builder does it for us. However, this means we cannot use prepareAsync with it!
Keep in mind that MediaPlayer uses valuable system resources. You should never create more instances than necessary. When you finish using MediaPlayer, call release() to free all allocated system resources. Do the same when your Activity receives an onStop() call since there's no reason to keep it running while your activity isn't interacting with the user (unless you want to play media in the background, which we won't cover here). When the activity resumes or restarts, you can create and prepare a new MediaPlayer and restore its state.
Here's how to release and nullify your MediaPlayer when you're done with it:
mediaPlayer?.release()
mediaPlayer = nullWe didn't use a nullable MediaPlayer in the previous examples for brevity, but nullifying it is good practice.
For example, if you don't release MediaPlayer in onStop() and the user rotates their device between portrait and landscape, you might quickly use up all system resources because each orientation change creates a new MediaPlayer that never gets released.
Conclusion
To play media on Android, you can use the MediaPlayer API. MediaPlayer can obtain files from app resources, local URIs, or download them from an external URL. Although using MediaPlayer might seem straightforward, always remember these important points:
Correctly manage
MediaPlayerstates.MediaPlayercan take a long time to download resources, so avoid preparing it in the UI thread.Remember to release resources when you no longer need them.