Support for media, both audio and visual, will hopefully be an essential part of your next immersive Android application. Both industries are on the forefront of mobile usage, and many modern mobile applications are leveraging this technology to build thoughtful and engaging applications. To this end, I’ll get you started with the basics for Android’s video and music libraries in this chapter. I’ll also point out a few things you’ll need to be aware of as you build a background media playback service: movies, music playback, background service, and what to watch out for.
Movie playback on an Android device boils down to the VideoView
class. In this section, I’ll use a simple example application that will play through every video saved on a phone’s SD card. Here is the general process:
I’ll use the ContentProvider
(something you’ll remember from our brief discussion in Chapter 6 when we uploaded the most recent photo) to request every video saved to the user’s external card.
After loading a Cursor
(Android’s query result data object) with all the device’s videos, I’ll need a method to play the next one.
I’ll set up an activity as a listener so that when video playback is complete, I can call my playNextVideo
method and move on to the next video in the cursor.
Last, I’ll clean up after my cursor when the user leaves the playback screen.
Before I can do any of these things, however, I need to place a VideoView
on my main layout to work with.
Placing a VideoView
onscreen is as simple as adding it to your XML layout. Here’s what my main.xml
file now looks like:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<VideoView
android:id="@+id/my_video_view"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Once the video view is in the screen’s layout, you can retrieve it, as you would any other view, with findViewById
. I’m going to need access to the video view later when it’s time to switch videos. Instead of retrieving the view with findViewById
each time, I’ll add a private data member to my Activity
class. Next, I’ll need to configure the video player.
In the following code listing, I’m doing many normal onCreate
sorts of things.
VideoView mVideoView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideoView = (VideoView) findViewById(R.id.my_video_view);
mVideoView.setOnCompletionListener(this);
}
Here I’m setting the content view, retrieving and caching the video view with findViewById
, and setting my activity as the video view’s onCompletionListener
.
In order for the activity to pass itself into the VideoView
as the onCompletionListener
, I have to extend OnCompletionListener
and implement my own onCompletion
method. Here is what I’ve added to my activity:
public class MainActivity extends Activity
implements OnCompletionListener{
@Override
public void onCompletion(MediaPlayer mp) {
}
//Rest of Activity code omitted
}
I now have a configured, yet very simplistic, video player. You’ll most likely want to have visual onscreen controls. Android’s VideoView
allows you to implement and set up a MediaController
for the VideoView
class. If you’re looking to go further into video playback after this chapter, this would be an excellent place to start.
Now that my video view is ready to roll, I can start hunting for things for it to actually play. For this simple example, I’m going to play every video on the device one after another until they’re all finished. To achieve this, I’ll use Android’s media ContentProvider
(accessed with a call to getContentResolver
). I’ll show you the code and then dig into some specifics. Here’s what onCreate
looks like with the new code to fetch a cursor with all the media objects:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVideoView = (VideoView) findViewById(R.id.my_video_view);
mVideoView.setOnCompletionListener(this);
requestVideosFromPhone();
}
private void requestVideosFromPhone() {
new AsyncTask<Void, Void, Cursor>() {
@Override
protected Cursor doInBackground(Void... params) {
String projection[] = new String[] { Video.Media.DATA };
final Cursor c = getContentResolver().query(
Video.Media.EXTERNAL_CONTENT_URI,
projection,
null, null, null);
return c;
}
@Override
protected void onPostExecute(Cursor result) {
if (result != null) {
mMediaCursor = result;
mMediaCursor.moveToFirst();
playNextVideo();
}
}
}.execute();
}
As you can see, I’m still fetching and caching the video view, but now I’m firing off an AsyncTask
that will query Android’s ContentProvider
for the Video.Media.DATA
column of all media rows that are videos. Don’t let it scare you that I created an anonymous inner AsyncTask
by calling new AsyncTask
from inside the function. This technique is a great way to kick things off the main thread without having to declare an entire subclass class for it. That query inside the AsyncTask
is fairly simple in that I want all videos on the external drive (SD card), and I only care about the data column for all those rows. This column for any particular row should always contain the path to the actual media content on disk. It’s this path that I’ll eventually hand off to the VideoView
for playback.
Note that it is possible to pass URIs to the video view. The video playback mechanism will find the path to the object for you. I would, however, like to show you the harder way so that you’ll be more informed.
The Cursor
object (a class Android uses to wrap database query responses) can be null
if the external media card is removed (or mounted into USB storage mode), so I’ll need to check for a null
cursor or one with no results before moving on. Typically in this case, I’d display a message to the user about their SD card being unavailable, but I’ll leave that task up to your imagination.
Last, I’ll get and cache the column index for the data row. This will make it easier for my playNextVideo
method to interact with the cursor’s results.
Starting in Android 4.4, the permission READ_EXTERNAL_STORAGE
, which has been in since API 16, is being enforced. To future proof your application and let your users know what permissions you are using, add this permission into your manifest:
<manifest>
...
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
...
</manifest>
At this point, you have a video view, a cursor full of media to play, and a listener configured to tell you when media playback is finished. Let’s put the final piece into the game, the code in playNextVideo
:
private void playNextVideo() {
if (!mMediaCursor.isAfterLast()) {
final String path = mMediaCursor.getString(
mMediaCursor.getColumnIndexOrThrow(
Video.Media.DATA));
Toast.makeText(getBaseContext(),
"Playing: " + path, Toast.LENGTH_SHORT).show();
mVideoView.setVideoPath(path);
mVideoView.start();
// Advance the cursor
mMediaCursor.moveToNext();
} else {
Toast.makeText(getBaseContext(),
"End of Line.", Toast.LENGTH_SHORT).show();
}
}
My first task is to check if the cursor is after the last piece of media and make sure we haven’t run out of stuff to play. When I know I’ve got a valid row from the cursor, I can tell the video view what it should render next. Video views can accept both a path defined as a string as well as the URI for a piece of media declared in the content provider. As I mentioned earlier, the data column of the cursor contains the file path to the media itself. I’ll pull this out of the Cursor
, hand it off to the video view, and then start playback. After playback has started, I will advance the cursor to the next position so that it is ready for checking when the current video finishes.
Tip
You’re not limited to just file paths—you can hand the video view a URL, and it will query and play the media found there.
Recall that earlier I registered my activity as the OnCompletionListener
for the video view so that when a video is finished it will notify me via the OnCompletion
call. In that method, I just need to call back into my playNextVideo
code and we’re playing!
@Override
public void onCompletion(MediaPlayer mp) {
playNextVideo();
}
At this point, the pieces are in place, videos play, and you’re almost done!
You’ve seen me do this at least once before, but it’s always important to close any cursors you request from the content provider. In past cases, I’ve requested data with a query, pulled out the relevant information, and immediately closed the cursor. In this case, however, I need to keep the cursor around for when the video change has to occur. This does not get me off the hook; I still need to close it down, so I’ll need to add that code to my activity’s onDestroy
method:
@Override
public void onDestroy() {
if (mMediaCursor != null) {
mMediaCursor.close();
}
}
I’ve shown you the very basics of loading and playing video content. Now it’s time for you to explore it on your own. Think about loading a video from a remote location (hint: encoding a URL as a URI) or building a progress bar (hint: getCurrentProgress
calls on the VideoView
).
Because errors are to media playback as swearing is to sailors, registering for an onErrorListener
is never a bad idea. Android will, if you pass it a class that implements the OnErrorListener
interface, tell you if it has hiccups playing your media files. As always, check the documentation for more information on playback.
Music playback, in general, revolves around the MediaPlayer
class. This is in a sense very similar to what you’ve just done with the video view (except you don’t need a View
object to render into).
Media players, if used to play music, should end up in their own services, with one notable exception: games and application sound effects. Building a sound effect example will make for a very simple way to get into the basics of audio playback.
You do not simply walk into Mordor. Similarly, you do not simply run about playing things willy-nilly. It requires care, attention to detail, and an understanding of the media player’s states. Here they are, in the order you’re most likely to encounter them:
Idle. In this state, the MediaPlayer
doesn’t know anything and, consequently, cannot actually do anything. To move on to the initialized state, you’ll need to tell it which file it’s going to play. This is done through the setDataSource
method.
Initialized. At this point, the media player knows what you’d like it to play, but it hasn’t acquired the data to do so. This is particularly important to understand when dealing with playing remote audio from a URL. Calling prepare
or prepareAsync
will move it into the prepared state. It will also load enough data from either the file system or the Internet to be ready for playback.
Prepared. After calling prepare
or prepareAsync
(and getting a callback), your media player is ready to rock! At this point, you can call seek
(to move the playhead) or start
(to begin playback).
Playing. Audio is pumping, people are dancing (OK, maybe not), and life is good. In this state, you can call pause
to halt the audio or seek
to move the play position. You end the party by calling stop
, which will move the media player back to the initialized state.
Just because you’ve run out of media to play doesn’t mean your player drops into the idle state. It will keep the current file loaded if you want to call start
(which will restart the audio from the beginning) or seek
(to move the playhead to a particular place). Only when you call stop
or reset
does the MediaPlayer
clear its buffers and return to the initialized state, ready for you to call prepare
again.
At its most straightforward, media playback is actually quite easy. Android gives you helper methods to shepherd your media player from the idle state to the prepared state if you can specify a file or resource id
right away. In this example case, you can record your own WAV file or use the beeeep
file that I included in the example project. When you have the sound file, add the file to a new resource folder called raw/
, which you should create at /res/raw/
. Any asset that you want to be placed in your application, such as text files, sound files, or anything else that doesn’t make sense for the normal resource hierarchy, should exist here so the application can reference it and load it directly.
Further, I’ve added a button (which you should be a pro at by now) that, when pressed, will play the recorded audio. Once the button is defined (R.id.beep_button
) in the main.xml
layout file and the audio beeeep.wav
file is placed in the raw/
folder, the following code should work like a charm:
MediaPlayer mBeeper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button beep = (Button) findViewById(R.id.beep_button);
beep.setOnClickListener(this);
mBeeper = MediaPlayer.create(this, R.raw.beeeep); }
As you can see, I’m retrieving the beep_button
from the activity_main.xml
layout (which I told the activity would be my screen’s layout) and setting my activity as the click listener for the button. Last, I use the media player’s create
helper method to initialize and prepare the media player with the beeeep.wav
file from the raw/
directory.
Remember that loading media, even from the res/
folder, can take some time. With this in mind, I’ve added the media player as a private data member to my Activity
class. This means I can load it once in my onCreate
method and then use it every time the user presses the button. Speaking of button pressing, here’s the code to play the sound effect when the button is pressed:
@Override
public void onClick(View v) {
mBeeper.start();
}
In order to be a good citizen, there’s one more step you need to take: releasing your resources! That’s right, when your activity closes down, you need to tell the media player that you’re finished with it, like so:
@Override
public void onDestroy(){
if(mBeeper != null){
mBeeper.stop();
mBeeper.release();
mBeeper = null;
}
}
Checking for null
before performing the cleanup is a good precaution. If, for whatever reason, there isn’t enough memory to load the resource or it fails for another reason, you won’t have any null
pointer exceptions on your hands.
There’s nothing complex about simple sound effect playback. Once you have a media player in the prepared state, you can call start
on it as many times as you like to produce the desired effect. Just remember to clean it up once you’re finished. Your users will thank you later. Now let’s move on to something a little more tricky.
You didn’t think I’d let you off that easy, did you? Remember two chapters ago when I showed you how to build a service in a separate process by using an AIDL file? I told you you’d need it for longer-running music playback. Here’s a quick recap of that process:
1. Create a service, define a few methods to control music playback, and declare the service in your manifest.
2. Create an Android Interface Definition Language (AIDL) file to define how the service will talk to any of the activities.
3. Bind an activity to the service, and, when the callback is hit, save the binder in order to call the service’s methods.
If most, or any, of those steps don’t make sense, take a gander back at Chapter 6.
In this section, I’ll show you how to turn the empty service into one that actually plays music in the background. The example music service will have methods to pause, play, set a data source, and ask it what the title of the current song is. To show you this service in practice, I’ll have my activity play the most recently added song on my new background music service.
There is a little overlap here with Chapter 6, but it’s worth covering how this works again before I dive into the music service itself. I’ve added the following code to the onCreate
method of our handy MusicExampleActivity
.
public void onCreate(Bundle savedInstanceState) {
//Button code omitted
Intent serviceIntent = new Intent(
getApplicationContext(), MusicService.class);
startService(serviceIntent);
bindService(serviceIntent, this, Service.START_STICKY);
}
You’ll notice that I’m actually starting the service before I attempt to bind to it. Although you can ask the bind service call to start the service for you, this is not a good idea when building a music service. That’s because when you unbind from the service, which you must do whenever your activity is destroyed, it will shut the service down. This, as you might imagine, would be bad if you’d like music to continue playing in the background after your activity has closed.
In the activity’s onResume()
method I’ve added a call to a function named requestMostRecentAudio
, which will query Android’s content provider for the most recent track. Since this is in onResume
, this will be called every time the app returns from the background.
I’ve also added a button to my screen that, when the query returns with some media, will become enabled via a BroadcastReceiver
catching an intent from the MusicService
, allowing you to click and play it (we will cover that in a few pages). Assuming it has both a track to play and a valid service, I can start playing music. Here’s the code that runs when the application hits onResume
:
@Override
protected void onResume() {
super.onResume();
// Register IntentFilters to listen for Broadcasts from the
// MusicService
IntentFilter filter = new IntentFilter();
filter.addAction(MusicService.PLAYING);
filter.addAction(MusicService.PLAYBACK_PREPARED);
registerReceiver(mPlayPauseReceiver, filter);
// Disable the button until media is prepared
mPlayPauseButton.setEnabled(false);
mPlayPauseButton.setText("Preparing...");
requestMostRecentAudio();
}
/**
* Request the most recently added audio file from the system
*/
private void requestMostRecentAudio() {
// The columns which to return
String[] projection = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DATE_ADDED };
// The order in which to return the results
String sortOrder =
MediaStore.Audio.Media.DATE_ADDED + " Desc Limit 1";
CursorLoader cursorLoader = new CursorLoader(this,
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder);
cursorLoader.registerListener(R.id.id_music_loader, this);
cursorLoader.startLoading();
}
In this code, I’m using a cursor loader to fetch my rather bizarre query. I’m asking the content provider for all possible audio tracks, but I’m sorting the results in descending order of their addition date (that is, when they were added to the phone) and limiting it to one result. This will, when the loader finishes, return a cursor with one record (the most recent song added to the library).
To make sure our UI properly reflects when media is ready to be played (Figure 8.1), we are going to listen for a broadcast intent from the service. There are a few ways to accomplish this task, but I find that the broadcast intent communication paradigm is an effective way to get messages from one part of your app to another.
/**
* Catch Broadcasts from the MediaService indicating what state the button
should reflect
*/
private BroadcastReceiver mPlayPauseReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (MusicService.PLAYING.equals(action)) {
try {
mPlayPauseButton.setText(
"Pause " + mService.getSongTitle());
} catch (RemoteException e) {
e.printStackTrace();
}
} else if (MusicService.PLAYBACK_PREPARED.equals(action)) {
mPlayPauseButton.setText("Play");
mPlayPauseButton.setEnabled(true);
}
}
};
The intent communication is pretty straightforward, and in this instance we are using the action of the broadcast intent to determine what behavior we should be applying to the button. If the action is MusicService.PLAYING
, indicating that the music is playing, we want to show the pause button with the name of the song that is playing. Otherwise, we want to show the user the play button and make sure it’s enabled so they can click it.
When the cursor with my data is ready, my activity’s onLoadComplete
will be called, at which point I can tell my music service what to play:
@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId() == R.id.id_music_loader) {
if (cursor == null || !cursor.moveToFirst()) {
Toast.makeText(getBaseContext(),
"No Music to Play",
Toast.LENGTH_LONG).show();
return;
} else if (mService == null) {
Toast.makeText(getBaseContext(),
"No Service to play Music!", Toast.LENGTH_LONG).show();
return;
}
try {
long id = cursor.getLong(
cursor.getColumnIndexOrThrow(
MediaStore.Audio.Media._ID));
mService.setDataSource(id);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set data source", e);
} finally {
cursor.close();
}
}
}
When the loader hits my callback, I’ll first need to check if it actually found any data. By checking if(!cursor.moveToFirst())
, I’m moving to the first and only record in the cursor, but I’m also making sure there actually is a record for me to look at. (If the cursor is empty, moveToFirst
will return false
.)
Next, I’ll need to make sure that my service bind in the onCreate
method was successful. Once I know that the service is valid, I’ll finally get the media ID by calling getLong
on the cursor to acquire the media’s unique ID. It is with this ID that I’ll tell the music service via setDataSource
what it should play.
Now that you can see how the ID is acquired, I’ll switch over to the music service and show you how the handoff occurs over there. Here’s what setDataSource
looks like from the service’s perspective (which we defined the skeleton for earlier):
private MediaPlayer mPlayer;
private String mCurrentTitle;
private String mDataSource;
private void setDataSource(long id) {
// We only want these columns
String[] projection = {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DATA,
MediaStore.Audio.Media.TITLE };
// The "WHERE" clause, excluding there WHERE
String selection = MediaStore.Audio.Media._ID + "=?";
// The arguments for the selection
String[] selectionArgs = new String[] { String.valueOf(id) };
final Cursor c = getContentResolver().query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
null);
if (c != null && c.moveToFirst()) {
try {
mDataSource = c.getString(
c.getColumnIndexOrThrow(
MediaStore.Audio.Media.DATA));
mCurrentTitle = c.getString(
c.getColumnIndexOrThrow(
MediaStore.Audio.Media.TITLE));
prepareMedia();
} finally {
c.close();
}
}
}
/**
* Prepares Media for playback
*/
private void prepareMedia() {
mPlayer.reset();
try {
mPlayer.setDataSource(mDataSource);
} catch (IllegalStateException e) {
// In a bad state!
e.printStackTrace();
} catch (IOException e) {
// Couldn't find the file!
e.printStackTrace();
}
// Reset the player back to the beginning
mPlayer.prepareAsync();
mPlayer.setOnPreparedListener(
new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// Send broadcast so the activity can update its
// button text
sendBroadcast(new Intent(PLAYBACK_PREPARED));
}
});
}
While this code is a little bit long, most of it should look similar to tasks you’ve already done.
1. I’m querying the content provider for the id
passed into the method.
2. I’m making sure that the music is actually there first by checking if the cursor came back null
(which can happen if the SD card has been removed). I’m also checking that there’s a valid row in the cursor.
3. When I’m sure the cursor is valid and contains the data for a song to play, I call prepareMedia
, which is a method I created to prepare the media player for playback with the new data source. This method will reset the player (in case it was already playing something else), set the data source for it, and tell the media player to prepare. Once these methods are finished, the media player is ready to start playback.
With that, your service is ready to go when the activity calls play
.
Now that the service has a data source and is prepared, the activity can call play
, which will trigger the following code to run and post a notification to the status bar (Figure 8.2):
/**
* Begin or resume playback
*/
private void play() {
if (mPlayer != null) {
mPlayer.start();
}
// Place a notification in the bar so we can run without being
// killed by the system
Notification notification =
buildSimpleNotification("Now Playing ", getSongTitle());
startForeground(1, notification);
// Send broadcast so the activity can update its button text
sendBroadcast(new Intent(PLAYING));
}
You’ll need to start media playback and make sure the service switches to running in the foreground. buildSimpleNotification
is a method I defined back in Chapter 6 that builds an icon for the status bar to keep your service alive. If you need a refresher on how to put services into foreground mode, review Chapter 6 or look at the sample code for this chapter.
Lastly, I send a broadcast intent letting anyone who is listening know that we have started playing. This is how we will adjust the state of our play button back in our main activity.
At some point, the music has to stop—either because it’s run out of songs to play or because the user has killed it off. Because I want the service to last beyond the run of my activity, I’ll need to have the service close itself down after it has finished playing its media. You can find the appropriate time to shut down the service by registering it as an onCompletionListener
with the media player. The line of code looks like this:
mPlayer.setOnCompletionListener(this);
You can call it at any point after the player is created. Of course, your service will need to implement OnCompletionListener
and have the correct onCompletion
method.
@Override
public void onCompletion(MediaPlayer mp) {
performStop();
}
/**
* Stop and reset the playback
*/
private void performStop() {
if (mPlayer.isPlaying()) {
mPlayer.stop();
}
// Prepare media for the next playback
prepareMedia();
// Remove our notification since we aren't playing
stopForeground(true);
stopSelf();
}
This means that once the media is finished, the service will call stop
on itself, which, because of the lifecycle of the service, will trigger Android to call the service’s onDestroy
method—the perfect place to clean up. Once the cleanup is finished, the service will be deallocated and cease running.
Cleanup is essential when dealing with media players. If you don’t handle this section correctly, a lot of the device’s memory can get lost in the shuffle. Here’s the onDestroy
method where I clean up the media player:
@Override
public void onDestroy(){
super.onDestroy();
if(mPlayer != null) {
mPlayer.stop();
mPlayer.release();
}
}
I must be careful, because an incorrect data source ID or bad media file could leave either of these references null
, which would crash the service quite handily when I try to shut them down.
When you’re writing music software for Android devices, it’s vitally important that you remember that the hardware on which your software is running is, in fact, a phone. This means you’ll need to watch out for several things.
Audio focus. You’ll need to use the AudioManager
class (introduced in Android 2.2) to register an audio focus listener, because other applications may want to play alerts, navigational directions, or their own horrible music. This is vital to making an Android music playback application play nice with the rest of the system.
Controls built into headphones. You’ll want your service to register to receive headset button intents through your manifest (or at runtime when your service is started). At the very least, set up your service to pause when the headset control is clicked.
Phone calls. By watching the phone’s call state either through the Telephony Manager
or with the audio focus tools, you absolutely must watch for incoming phone calls. You must stop all audio when the phone rings. Nothing will enrage your users (and hurt your ratings) more than not accommodating phone calls.
Missing SD card. You’ll want to make sure your app handles a missing or removed SD card correctly. Users can mount their external cards as removable drives with the USB cable at any point. Android will alert you if you listen for the ACTION_MEDIA_REMOVED
intent.
This might seem like a lot of things to look out for (and it is), but never fear, the developers at Google have released an open-source media player (which they ship with the Android source code) that can be a great guide for dealing with this stuff. As always, the documentation will have a lot on the subject as well.
In this chapter, I showed you how to
Play a simple video
Play a sound effect when a button is pressed
Take a previously created service interface and create a functional media player from it
You should now be comfortable with the essentials of media playback. If you’re looking to go further with videos (which I hope you are), you’ll want to look into using a controller to modify the state of the video view.
Your next step to expand the media playback service is to think about how you’d pass arrays of IDs (playlists) and how you’d deal with updating those playlists on the fly (as users change them).
Android can be a very powerful media platform if you’re careful and treat it with care. Go forth and make a crop of better music players—if for no other reason than so I can use them myself.