Background Audio in Android With MediaSessionCompat

One of the most popular uses for mobile devices is playing back audio through music streaming services, downloaded podcasts, or any other number of audio sources. While this is a fairly common feature, it's hard to implement, with lots of different pieces that need to be built correctly in order to give your user the full Android experience. 

In this tutorial you will learn about MediaSessionCompat from the Android support library, and how it can be used to create a proper background audio service for your users.

Setup

The first thing you will need to do is include the Android support library into your project. This can be done by adding the following line into your module's build.gradle file under the dependencies node.

After you have synced your project, create a new Java class. For this example I will call the class BackgroundAudioService. This class will need to extend MediaBrowserServiceCompat. We will also implement the following interfaces: MediaPlayer.OnCompletionListener and AudioManager.OnAudioFocusChangeListener.

Now that your MediaBrowserServiceCompat implementation is created, let's take a moment to update AndroidManifest.xml before returning to this class. At the top of the class, you will need to request the WAKE_LOCK permission.

Next, within the application node, declare your new service with the following intent-filter items. These will allow your service to intercept control buttons, headphone events and media browsing for devices, such as Android Auto (although we won't do anything with Android Auto for this tutorial, some basic support for it is still required by MediaBrowserServiceCompat).

Finally, you will need to declare the use of the MediaButtonReceiver from the Android support library. This will allow you to intercept media control button interactions and headphone events on devices running KitKat and earlier.

Now that your AndroidManifest.xml file is finished, you can close it. We're also going to create another class named MediaStyleHelper, which was written by Ian Lake, Developer Advocate at Google, to clean up the creation of media style notifications.

Once that's created, go ahead and close the file. We will focus on the background audio service in the next section.

Building Out the Background Audio Service

Now it's time to dig into the core of creating your media app. There are a few member variables that you will want to declare first for this sample app: a MediaPlayer for the actual playback, and a MediaSessionCompat object that will manage metadata and playback controls/states.

In addition, you will need a BroadcastReceiver that listens for changes in the headphone state. To keep things simple, this receiver will pause the MediaPlayer, if it is playing.

For the final member variable, you will to create a MediaSessionCompat.Callback object, which is used for handling playback state when media session actions occur.

We will revisit each of the above methods later in this tutorial, as they will be used to drive operations in our media app.

There are two methods that we will also need to declare, though they won't need to do anything for the purposes of this tutorial: onGetRoot() and onLoadChildren(). You can use the following code for your defaults.

Lastly, you will want to override the onStartCommand() method, which is the entry point into your Service. This method will take the Intent that is passed to the Service and send it to the MediaButtonReceiver class.

Initializing All the Things

Now that your base member variables are created, it's time to initialize everything. We'll do this by calling various helper methods in onCreate().

The first method, initMediaPlayer(), will initialize the MediaPlayer object that we created at the top of the class, request partial wake lock (which is why we required that permission in AndroidManifest.xml), and set the player's volume.

The next method, initMediaSession(), is where we initialize the MediaSessionCompat object and wire it to the media buttons and control methods that allow us to handle playback and user input. This method starts by creating a ComponentName object that points to the Android support library's MediaButtonReceiver class, and uses that to create a new MediaSessionCompat. We then pass the MediaSession.Callback object that we created earlier to it, and set the flags necessary for receiving media button inputs and control signals. Next, we create a new Intent for handling media button inputs on pre-Lollipop devices, and set the media session token for our service.

Finally, we'll register the BroadcastReceiver that we created at the top of the class so that we can listen for headphone change events.

Handling Audio Focus

Now that you've finished initializing BroadcastReceiver, MediaSessionCompat and MediaPlayer objects, it's time to look into handling audio focus. 

While we may think our own audio apps are the most important at the moment, other apps on the device will be competing to make their own sounds, such as an email notification or mobile game. In order to work with these various situations, the Android system uses audio focus to determine how audio should be handled. 

The first case we will want to handle is starting playback and attempting to receive the device's focus. In your MediaSessionCompat.Callback object, go into the onPlay() method and add the following condition check.

The above code will call a helper method that attempts to retrieve focus, and if it cannot, it will simply return. In a real app, you would want to handle failed audio playback more gracefully. successfullyRetrievedAudioFocus() will get a reference to the system AudioManager, and attempt to request audio focus for streaming music. It will then return a boolean representing whether or not the request succeeded.

You'll notice that we are also passing this into the requestAudioFocus() method, which associates the OnAudioFocusChangeListener with our service. There are a few different states that you'll want to listen for in order to be a "good citizen" in the device's app ecosystem.

  • AudioManager.AUDIOFOCUS_LOSS: This occurs when another app has requested audio focus. When this happens, you should stop audio playback in your app.
  • AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: This state is entered when another app wants to play audio, but it only anticipates needing focus for a short time. You can use this state to pause your audio playback.
  • AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: When audio focus is requested, but throws a 'can duck' state, it means that you can continue your playback, but should bring the volume down a bit. This can occur when a notification sound is played by the device.
  • AudioManager.AUDIOFOCUS_GAIN: The final state we will discuss is AUDIOFOCUS_GAIN. This is the state when a duckable audio playback has completed, and your app can resume at its previous levels.

A simplified onAudioFocusChange() callback may look like this:

Understanding the MediaSessionCompat.Callback

Now that you have a general structure together for your Service, it's time to dive into the MediaSessionCompat.Callback. In the last section you added a little bit to onPlay() to check if audio focus was granted. Below the conditional statement, you will want to set the MediaSessionCompat object to active, give it a state of STATE_PLAYING, and assign the proper actions necessary to create pause buttons on pre-Lollipop lock screen controls, phone and Android Wear notifications.

The setMediaPlaybackState() method above is a helper method that creates a PlaybackStateCompat.Builder object and gives it the proper actions and state, and then builds and associates a PlaybackStateCompat with your MediaSessionCompat object.

It's important to note that you will need both the ACTION_PLAY_PAUSE and either ACTION_PAUSE or ACTION_PLAY flags in your actions in order to get proper controls on Android Wear.

Media notification on Android Wear

Back in onPlay(), you will want to show a playing notification that is associated with your MediaSessionCompat object by using the MediaStyleHelper class that we defined earlier, and then show that notification.

Finally, you will start the MediaPlayer at the end of onPlay().

Media control notification on an Android Nougat device

When the callback receives a pause command, onPause() will be called. Here you will pause the MediaPlayer, set the state to STATE_PAUSED, and show a paused notification.

Our showPausedNotification() helper method will look similar to the showPlayNotification() method.

The next method in the callback that we'll discuss, onPlayFromMediaId(), takes a String and a Bundle as parameters. This is the callback method that you can use for changing audio tracks/content within your app. 

For this tutorial, we will simply accept a raw resource ID and attempt to play that, and then reinitialize the session's metadata. As you are allowed to pass a Bundle into this method, you can use it to customize other aspects of your media playback, such as setting up a custom background sound for a track.

Now that we've discussed the two main methods in this callback that you will use in your apps, it's important to know that there are other optional methods that you can use to customize your service. Some methods include onSeekTo(), which allows you to change the playback position of your content, and onCommand(), which will accept a String denoting the type of command, a Bundle for extra information about the command, and a ResultReceiver callback, which will allow you to send custom commands to your Service.

Tearing Down

When our audio file has completed, we will want to decide what our next action will be. While you may want to play the next track in your app, we'll keep things simple and release the MediaPlayer.

Finally, we'll want to do a few things in the onDestroy() method of our Service. First, get a reference to the system service's AudioManager, and call abandonAudioFocus() with our AudioFocusChangeListener as a parameter, which will notify other apps on the device that you are giving up audio focus. Next, unregister the BroadcastReceiver that was set up to listen for headphone changes, and release the MediaSessionCompat object. Finally, you will want to cancel the playback control notification.

At this point, you should have a working basic background audio Service using MediaSessionCompat for playback control across devices. While there has already been a lot involved in just creating the service, you should be able to control playback from your app, a notification, lock screen controls on pre-Lollipop devices (Lollipop and above will use the notification on the lock screen), and from peripheral devices, such as Android Wear, once the Service has been started.

Media lock screen controls on Android Kit Kat

Starting and Controlling Content From an Activity

While most controls will be automatic, you will still have a bit of work to start and control a media session from your in-app controls. At the very least, you will want a MediaBrowserCompat.ConnectionCallback, MediaControllerCompat.CallbackMediaBrowserCompat, and MediaControllerCompat objects created in your app.

MediaControllerCompat.Callback will have a method called onPlaybackStateChanged() that receives changes in playback state, and can be used to keep your UI in sync.

MediaBrowserCompat.ConnectionCallback has an onConnected() method that will be called when a new MediaBrowserCompat object is created and connected. You can use this to initialize your MediaControllerCompat object, link it to your MediaControllerCompat.Callback, and associate it with MediaSessionCompat from your Service. Once that is completed, you can start audio playback from this method.

You'll notice that the above code snippet uses getSupportMediaController().getTransportControls() to communicate with the media session. Using the same technique, you can call onPlay() and onPause() in your audio service's MediaSessionCompat.Callback object.

When you're done with your audio playback, you can pause the audio service and disconnect your MediaBrowserCompat object, which we'll do in this tutorial when this Activity is destroyed.

Wrapping Up

Whew! As you can see, there are a lot of moving pieces involved with creating and using a background audio service correctly. 

In this tutorial, you have created a Service that plays a simple audio file, listens for changes in audio focus, and links to MediaSessionCompat to provide universal playback control on Android devices, including handsets and Android Wear. If you run into roadblocks while working through this tutorial, I strongly recommend checking out the associated Android project code on Envato Tuts+'s GitHub.

And check out some of our other Android courses and tutorials here on Envato Tuts+!


Tags:

Comments

Related Articles