Moving our attention forward, let's look at how we can further leverage Android's audio playback capabilities to harness media that lives elsewhere, in particular audio that lives online. With posting MP3 files, podcasting, and streaming all becoming more and more popular, it only makes sense that we would want to build audio playback applications that can leverage those services.
Fortunately, Android has rich capabilities for dealing with various types of audio available on the network.
Let's start with examining how to leverage web-based audio or audio delivered via HTTP.
The simplest case to explore would simply be to play an audio file that lives online and is accessible via HTTP.
One such file would be this, which is available on my server: http://www.mobvcasting.com/android/audio/goodmorningandroid.mp3
Here is an example activity that uses the MediaPlayer
to illustrate how to play audio available via HTTP.
package com.apress.proandroidmedia.ch06.audiohttp;
import java.io.IOException;
import android.app.Activity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.util.Log;
public class AudioHTTPPlayer extends Activity {
MediaPlayer mediaPlayer;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
When our activity is created, we do a generic instantiation of a MediaPlayer
object by calling the MediaPlayer
constructor with no arguments. This is a different way of using the MediaPlayer
than we have previously seen and requires us to take some additional steps before we can play the audio.
mediaPlayer = new MediaPlayer();
Specifically, we need to call the setDataSource
method, passing in the HTTP location of the audio file we would like to play. This method can throw an IOException
, so we have to catch and deal with that as well.
try {
mediaPlayer.setDataSource(
"http://www.mobvcasting.com/android/audio/goodmorningandroid.mp3");
Following that we call the prepare method and then the start
method, after which the audio should start playing.
mediaPlayer.prepare();
mediaPlayer.start();
} catch (IOException e) {
Log.v("AUDIOHTTPPLAYER",e.getMessage());
}
}
}
Running this example, you will probably notice a significant lag time from when the application loads to when the audio plays. The length of the delay is due to the speed of the data network that the phone is using for its Internet connection (among other variables).
If we add Log or Toast messages throughout the code, we would see that this delay happens between the call to the prepare method and the start method. During the running of the prepare
method, the MediaPlayer
is filling up a buffer so that the audio playback can run smoothly even if the network is slow.
The prepare
method actually blocks while it is doing this. This means that applications that use this method will likely become unresponsive until the prepare
method is complete. Fortunately, there is a way around this, and that is to use the prepareAsync
method. This method returns immediately and does the buffering and other work in the background, allowing the application to continue.
The issue then becomes one of paying attention to the state of the MediaPlayer
object and implementing various callbacks that help us keep track of its state.
To get a handle on the various states that a MediaPlayer
object may be in, it is helpful to look over the diagram from the MediaPlayer
page on the Android API Reference, shown in Figure 6–1.
Figure 6–1. MediaPlayer state diagram from Android API Reference
Here is a full MediaPlayer
example that uses prepareAsync
and implements several listeners to keep track of its state.
package com.apress.proandroidmedia.ch06.audiohttpasync;
import java.io.IOException;
import android.app.Activity;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnBufferingUpdateListener;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnInfoListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
In this version of our HTTP audio player, we are implementing several interfaces. Two of them, OnPreparedListener
and OnCompletionListener
, will help us keep track of the state of the MediaPlayer
so that we don't attempt to play or stop audio when we shouldn't.
public class AudioHTTPPlayer extends Activity
implements OnClickListener, OnErrorListener, OnCompletionListener,
OnBufferingUpdateListener, OnPreparedListener {
MediaPlayer mediaPlayer;
The interface for this activity has start and stop Buttons, a TextView for displaying the status, and a TextView for displaying the percentage of the buffer that has been filled.
Button stopButton, startButton;
TextView statusTextView, bufferValueTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
In the onCreate
method, we set the stopButton
and startButton
to be disabled. They'll be enabled or disabled throughout the running of the application. This is to illustrate when the methods they trigger are and aren't available.
stopButton = (Button) this.findViewById(R.id.EndButton);
startButton = (Button) this.findViewById(R.id.StartButton);
stopButton.setOnClickListener(this);
startButton.setOnClickListener(this);
stopButton.setEnabled(false);
startButton.setEnabled(false);
bufferValueTextView = (TextView) this.findViewById(R.id.BufferValueTextView);
statusTextView = (TextView) this.findViewById(R.id.StatusDisplayTextView);
statusTextView.setText("onCreate");
After we instantiate the MediaPlayer
object, we register the activity to be the OnCompletionListener
, the OnErrorListener
, the OnBufferingUpdateListener
, and the OnPreparedListener
.
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnErrorListener(this);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnPreparedListener(this);
statusTextView.setText("MediaPlayer created");
Next we call setDataSource
with the URL to the audio file.
try {
mediaPlayer.setDataSource(
"http://www.mobvcasting.com/android/audio/goodmorningandroid.mp3");
statusTextView.setText("setDataSource done");
statusTextView.setText("calling prepareAsync");
Last, we'll call prepareAsync
, which will start the buffering of the audio file in the background and return. When the preparation is complete, our activity's onCompletion
method will be called due to our activity being registered as the OnCompletionListener
for the MediaPlayer
.
mediaPlayer.prepareAsync();
} catch (IOException e) {
Log.v("AUDIOHTTPPLAYER",e.getMessage());
}
}
What follows is the implementation of the onClick
method for the two Buttons. When the stopButton
is pressed, the MediaPlayer
's pause
method will be called. When the startButton i
s pressed, the MediaPlayer
's start
method is called.
public void onClick(View v) {
if (v == stopButton) {
mediaPlayer.pause();
statusTextView.setText("pause called");
startButton.setEnabled(true);
} else if (v == startButton) {
mediaPlayer.start();
statusTextView.setText("start called");
startButton.setEnabled(false);
stopButton.setEnabled(true);
}
}
If the MediaPlayer
enters into an error state, the onError
method will be called on the object that is registered as the MediaPlayer
's OnErrorListener
. The following onError
method shows the various constants that are specified in the MediaPlayer
class.
public boolean onError(MediaPlayer mp, int what, int extra) {
statusTextView.setText("onError called");
switch (what) {
case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK:
statusTextView.setText(
"MEDIA ERROR NOT VALID FOR PROGRESSIVE PLAYBACK " + extra);
Log.v(
"ERROR","MEDIA ERROR NOT VALID FOR PROGRESSIVE PLAYBACK " + extra);
break;
case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
statusTextView.setText("MEDIA ERROR SERVER DIED " + extra);
Log.v("ERROR","MEDIA ERROR SERVER DIED " + extra);
break;
case MediaPlayer.MEDIA_ERROR_UNKNOWN:
statusTextView.setText("MEDIA ERROR UNKNOWN " + extra);
Log.v("ERROR","MEDIA ERROR UNKNOWN " + extra);
break;
}
return false;
}
When the MediaPlayer
completes playback of an audio file, the onCompletion
method of the object registered as the OnCompletionListener
will be called. This indicates that we could start playback again.
public void onCompletion(MediaPlayer mp) {
statusTextView.setText("onCompletion called");
stopButton.setEnabled(false);
startButton.setEnabled(true);
}
While the MediaPlayer
is buffering, the onBufferingUpdate
method of the object registered as the MediaPlayer
's onBufferingUpdateListener
is called. The percentage of the buffer that is filled is passed in.
public void onBufferingUpdate(MediaPlayer mp, int percent) {
bufferValueTextView.setText("" + percent + "%");
}
When the prepareAsync
method finishes, the onPrepared
method of the object registered as the OnPreparedListener
will be called. This indicates that the audio is ready for playback, and therefore, in the following method, we are setting the startButton
to be enabled.
public void onPrepared(MediaPlayer mp) {
statusTextView.setText("onPrepared called");
startButton.setEnabled(true);
}
}
Here is the Layout XML associated with the foregoing activity:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView android:text="Status" android:id="@+id/TextView01"
android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
<TextView android:text="Unknown" android:id="@+id/StatusDisplayTextView"
android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
<TextView android:text="0%" android:id="@+id/BufferValueTextView"
android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Start" android:id="@+id/StartButton"></Button>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/EndButton" android:text="Stop"></Button>
</LinearLayout>
As just shown, the MediaPlayer
has a nice set of capabilities for handling audio files that are available online via HTTP.
One way that live audio is commonly delivered online is via HTTP streaming. There are a variety of streaming methods that fall under the umbrella of HTTP streaming from server push, which has historically been used for displaying continually refreshing webcam images in browsers to a series of new methods being put forth by Apple, Adobe, and Microsoft for use by their respective media playback applications.
The main method for streaming live audio over HTTP is one developed in 1999 by a company called Nullsoft, which was subsequently purchased by AOL. Nullsoft was the creator of WinAMP, a popular MP3 player, and they developed a live audio streaming server that used HTTP, called SHOUTcast. SHOUTcast uses the ICY protocol, which extends HTTP. Currently, a large number of servers and playback software products support this protocol, so many, in fact, that it may be considered the de facto standard for online radio.
Fortunately, the MediaPlayer
class on Android is capable of playing ICY streams without requiring us developers to jump through hoops.
Unfortunately for us, Internet radio stations don't typically advertise the direct URL to their streams. This is for good reason; unfortunately, browsers don't support ICY streams directly and require a helper application or plug-in to play the stream. In order to know to open a helper application, an intermediary file with a specific MIME-type is delivered by the Internet radio station, which contains a pointer to the actual live stream. In the case of ICY streams, this is typically either a PLS file or an M3U file.
audio/x-scpls
”.audio/x-mpegurl
”.The following illustrates the contents of an M3U file that points to a fake live stream.
#EXTM3U
#EXTINF:0,Live Stream Name
http://www.nostreamhere.org:8000/
The first line, #EXTM3U
, is required and specifies that what follows is an Extended M3U file that can contain extra information. Extra information about a playlist entry is specified on the line above the entry and starts with #EXTINF:
, followed by the duration in seconds, a comma, and then the name of the media.
An M3U file can have multiple entries as well, specifying one file or stream after another.
#EXTM3U
#EXTINF:0,Live Stream Name
http://www.nostreamhere.org:8000/
#EXTINF:0,Other Live Stream Name
http://www.nostreamthere.org/
Unfortunately, the MediaPlayer
on Android doesn't handle the parsing of M3U files for us. Therefore, to create an HTTP streaming audio player on Android, we have to handle the parsing ourselves and use the MediaPlayer
for the actual media playback.
Here is an example activity that parses and plays an M3U file delivered from an online radio station or any M3U file as entered in the URL field.
package com.apress.proandroidmedia.ch06.httpaudioplaylistplayer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Vector;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import android.app.Activity;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
As in our previous example, this activity will extend OnCompletionListener
and OnPreparedListener
to track the state of the MediaPlayer
.
public class HTTPAudioPlaylistPlayer extends Activity
implements OnClickListener, OnCompletionListener, OnPreparedListener {
We'll use a Vector
to hold the list of items in the playlist. Each item will be a PlaylistFile
object that is defined in an inner class at the end of this class.
Vector playlistItems;
We'll have a few Buttons on the interface as well as an EditText
object, which will contain the URL to the M3U file.
Button parseButton;
Button playButton;
Button stopButton;
EditText editTextUrl;
String baseURL = "";
MediaPlayer mediaPlayer;
The following integer is used keep track of which item from the playlistItems Vector
we are currently on.
int currentPlaylistItemNumber = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
parseButton = (Button) this.findViewById(R.id.ButtonParse);
playButton = (Button) this.findViewById(R.id.PlayButton);
stopButton = (Button) this.findViewById(R.id.StopButton);
We are setting the text of the editTextUrl
object to be the URL of an M3U file from an online radio station. The first one, which is commented out, is the URL for KBOO, a community radio station in Portland, Oregon (www.kboo.fm/
). The second, which is not commented out, is for KMFA, a classical station in Austin, Texas (www.kmfa.org/
).
The user can edit this to be the URL to any M3U file available on the Internet.
editTextUrl = (EditText) this.findViewById(R.id.EditTextURL);
//editTextUrl.setText("http://live.kboo.fm:8000/high.m3u");
editTextUrl.setText("http://pubint.ic.llnwd.net/stream/pubint_kmfa.m3u");
parseButton.setOnClickListener(this);
playButton.setOnClickListener(this);
stopButton.setOnClickListener(this);
Initially the playButton
and stopButton
will not be enabled; the user will not be able to press them. The parseButton
, on the other hand, will be enabled. After the M3U file is retrieved and parsed, the playButton
will be enabled, and after the audio is playing, the stopButton
will be enabled. This is how we'll guide the user through the flow of the application.
playButton.setEnabled(false);
stopButton.setEnabled(false);
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setOnPreparedListener(this);
}
Each of the Buttons has their OnClickListener
set to be this activity. Therefore the following onClick
method will be called when any of these are clicked. This drives most of the application's flow.
When the parseButton
is pressed, the parsePlaylistFile
method is called. When the playButton
is pressed, the playPlaylistItems
method is called.
public void onClick(View view) {
if (view == parseButton) {
parsePlaylistFile();
} else if (view == playButton) {
playPlaylistItems();
} else if (view == stopButton) {
stop();
}
}
The first method that will be triggered is parsePlaylistFile
. This method downloads the M3U file that is specified by the URL in the editTextUrl
object and parses it. The act of parsing it picks out any lines that represent files to play and creates a PlaylistItem
object, which is added to the playlistItems
Vector
.
private void parsePlaylistFile() {
We'll start out with an empty Vector
. If a new M3U file is parsed, anything previously in here will be thrown away.
playlistItems = new Vector();
To retrieve the M3U file off of the Web, we can use the Apache Software Foundation's HttpClient
library, which is included with Android.
First we create an HttpClient
object, which represents something along the lines of a web browser, and then an HttpGet
object, which represents the specific request for a file. The HttpClient
will execute the HttpGet
and return an HttpResponse
.
HttpClient httpClient = new DefaultHttpClient();
HttpGet getRequest = new HttpGet(editTextUrl.getText().toString());
Log.v("URI",getRequest.getURI().toString());
try {
HttpResponse httpResponse = httpClient.execute(getRequest);
if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
// ERROR MESSAGE
Log.v("HTTP ERROR",httpResponse.getStatusLine().getReasonPhrase());
} else {
After we make the request, we can retrieve an InputStream
from the HttpResponse
. This InputStream
contains the contents of the file requested.
InputStream inputStream = httpResponse.getEntity().getContent();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream));
With the aid of a BufferedReader
, we can go through the file line by line.
String line;
while ((line = bufferedReader.readLine()) != null) {
Log.v("PLAYLISTLINE","ORIG: " + line);
If the line starts with a “#”, we'll ignore it for now. As described earlier, these lines are metadata.
Otherwise, if it isn't a blank line, it has a length greater than 0, and we'll assume that it is a playlist item.
} else if (line.length() > 0) {
If the line starts with “http://”, we treat it as a full URL to the stream, otherwise we treat it as a relative URL and tack on the URL of the original request for the M3U file.
String filePath = "";
if (line.startsWith("http://")) {
// Assume it's a full URL
filePath = line;
} else {
// Assume it's relative
filePath = getRequest.getURI().resolve(line).toString();
}
We then add it to our Vector
of playlist items.
PlaylistFile playlistFile = new PlaylistFile(filePath);
playlistItems.add(playlistFile);
}
}
inputStream.close();
}
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Last, now that we are done parsing the file, we enable the playButton
.
playButton.setEnabled(true);
}
When the user clicks the playButton
, the playPlaylistItems
method is called. This method takes the first item from the playlistItems
Vector
and hands it to the MediaPlayer
object for preparation.
private void playPlaylistItems() {
playButton.setEnabled(false);
currentPlaylistItemNumber = 0;
if (playlistItems.size() > 0)
{
String path =
((PlaylistFile)playlistItems.get(currentPlaylistItemNumber)).getFilePath();
try {
After extracting the path to the file or stream, we use that in a setDataSource
method call on the MediaPlayer
.
mediaPlayer.setDataSource(path);
Then we call prepareAsync
, which allows the MediaPlayer
to buffer and prepare to play the audio in the background. When the buffering and other preparation is done, the activity's onPrepared
method will be called since the activity is registered as the OnPreparedListener
.
mediaPlayer.prepareAsync();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Once the onPrepared
method has been called, the stopButton
is enabled and the MediaPlayer
object is triggered to start playing the audio.
public void onPrepared(MediaPlayer _mediaPlayer) {
stopButton.setEnabled(true);
Log.v("HTTPAUDIOPLAYLIST","Playing");
mediaPlayer.start();
}
When the audio playback is complete, the onCompletion
method is triggered in this activity since the activity extends and is registered as the MediaPlayer
's OnCompletionListener
.
The onCompletion
method cues up the next item in the playlistItems
Vector
.
public void onCompletion(MediaPlayer mediaPlayer) {
Log.v("ONCOMPLETE","called");
mediaPlayer.stop();
mediaPlayer.reset();
if (playlistItems.size() > currentPlaylistItemNumber + 1) {
currentPlaylistItemNumber++;
String path =
((PlaylistFile)playlistItems.get(currentPlaylistItemNumber)).getFilePath();
try {
mediaPlayer.setDataSource(path);
mediaPlayer.prepareAsync();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
The stop
method is called when the user presses the stopButton
. This method causes the MediaPlayer
to pause rather than stop. The MediaPlayer
has a stop method, but that puts the MediaPlayer
in an unprepared state. The pause
method just pauses playback.
private void stop() {
mediaPlayer.pause();
playButton.setEnabled(true);
stopButton.setEnabled(false);
}
Last, we have an inner class called PlaylistFile
. One PlaylistFile
object is created for each file represented in the M3U file.
class PlaylistFile {
String filePath;
public PlaylistFile(String _filePath) {
filePath = _filePath;
}
public void setFilePath(String _filePath) {
filePath = _filePath;
}
public String getFilePath() {
return filePath;
}
}
}
Here is the layout XML file (main.xml
) for the foregoing activity.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="Enter URL" android:id="@+id/EnterURLTextView"></TextView>
<EditText android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/EditTextURL" android:text="http://www.mobvcasting.com/android/
audio/test.m3u"></EditText>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/ButtonParse" android:text="Parse"></Button>
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/PlaylistTextView"></TextView>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/PlayButton" android:text="Play"></Button>
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:id="@+id/StopButton" android:text="Stop"></Button>
</LinearLayout>
This example requires that the following permission be added to the AndroidManifest.xml
file
<uses-permission android:name="android.permission.INTERNET" />
As you can see in the foregoing example, working with a live audio stream via HTTP is as straightforward as working with a file delivered via HTTP. Figure 6–2 shows the example in action.
Figure 6–2. HTTP Audio Playlist Player example shown after audio started playback
Android supports one more protocol for streaming audio through the MediaPlayer
. This is called the Real Time Streaming Protocol or RTSP. RTSP has been in use for quite some time and was made popular in the mid- to late 1990s by RealNetworks, as it is the protocol they used in their audio and video streaming software.
The same code in use for the preceding HTTP streaming example works with an RTSP audio stream. We'll get into more RTSP specifics in Chapter 10.
18.221.136.142