Networked Audio

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.

HTTP Audio Playback

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.

Image

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 is 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"Image
 android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
    <TextView android:text="Unknown" android:id="@+id/StatusDisplayTextView"Image
 android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
    <TextView android:text="0%" android:id="@+id/BufferValueTextView"Image
 android:layout_width="wrap_content" android:layout_height="wrap_content"></TextView>
    <Button android:layout_width="wrap_content" android:layout_height="wrap_content"Image
 android:text="Start" android:id="@+id/StartButton"></Button>
    <Button android:layout_width="wrap_content" android:layout_height="wrap_content"Image
 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.

Streaming Audio 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.

  • A PLS file is a multimedia playlist file and has the MIME-type “audio/x-scpls”.
  • An M3U file is also a file that stores multimedia playlists but in a more basic format. Its MIME-type is “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.

                    if (line.startsWith("#")) {
                        // Metadata
                        // Could do more with this but not fo now

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"Image
     android:text="Enter URL" android:id="@+id/EnterURLTextView"></TextView>
    <EditText android:layout_width="wrap_content" android:layout_height="wrap_content"Image
     android:id="@+id/EditTextURL" android:text="http://www.mobvcasting.com/android/Image
    audio/test.m3u"></EditText>
    <Button android:layout_width="wrap_content" android:layout_height="wrap_content"Image
     android:id="@+id/ButtonParse" android:text="Parse"></Button>
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"Image
     android:id="@+id/PlaylistTextView"></TextView>
    <Button android:layout_width="wrap_content" android:layout_height="wrap_content"Image
     android:id="@+id/PlayButton" android:text="Play"></Button>
    <Button android:layout_width="wrap_content" android:layout_height="wrap_content"Image
     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.

Image

Figure 6–2. HTTP Audio Playlist Player example shown after audio started playback

RTSP Audio Streaming

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.136.142