"Java Media Framework can handle MP3's and streaming", I thought to myself, "so why can't it handle an MP3 stream?"

Yet there was the JMStudio app, cluelessly gobbling bytes from a Shoutcast stream that it would never start playing. So, I set out to figure out what the problem was... figuring as always that it would be much easier than it ended up being.

But after much experimentation and hackery, the result is jicyshout, an open-source library on which this article is based. To follow along, grab the latest release and look at the SimpleMP3DataSource and SeekableInputStream classes.

Despite the unfortunately widespread belief that Java cannot handle MP3 audio — a request to add MP3 support to javax.sound is the number-12 Request For Enhancement at Javasoft's site as of this writing — the add-on Java Media Framework can easily handle flat MP3 files.

In fact, if you've installed JMF and have a suitable URL, such as a file://-style reference to an MP3 on your local filesystem, or a web URL to a flat file (for example, this snippet on jazz singer Holly Cole's website: http://www.hollycole.com/Multimedia/Mpeg/callingyou96.mp3 ), then playing the MP3 can be crunched into an app that has only one meaningful line of code:

import javax.media.*;
import java.net.URL;
public class OneLineMP3 {
    public static void main (String[] args) {
        try{
            Manager.createRealizedPlayer(new URL(args[0])).start();
        } catch(Exception e) { e.printStackTrace(); }
    }
}

Yet try this app with the URL of your favorite Shoutcast, Icecast, or Live 365 stream, and it will typically throw a NoPlayerException.

So what's the problem? It starts with the fact that all three of those server standards use http, not the more streaming-friendly rtsp protocol. It's odd because a typical web server would rather fire off a flat file as quickly as possible and forget about it, but the streaming servers need to keep a connection open and massage the data transfer, matching its transfer rate to the playback rate of the audio data. An interesting ApacheCon 2001 presentation discusses this further.

Then there are the URL's for these streams — look at some typical examples:
URL Title Type
http://216.53.130.150:8000 Dicky's Rockin' Radio Shoutcast
http://65.165.174.100:8000/som CalArts School of Music Icecast
http://www.live365.com/play/95767 Anime Hardcore 2 Nanocaster*
* - we only know the name "Nanocaster" from http response headers received when connecting to Live365 streams; it appears to be a propreitary system.

Despite obvious differences from one another, these URL's don't give the javax.media.Manager much to go on. Not only is there is no "mp3" file type, the http: protocol suggests a flat file, not an endless stream. In practice, most of these servers don't even send useful http response headers, so we can't depend on a helpful "audio/x-mpeg" content-type header either.

Finding MP3 streams

Some streaming MP3 players like Audion and iTunes have integrated lists of stations, and once you've found a stream this way, you can get its URL with a "Get Info" or "Properties" menu item

The websites for Shoutcast and Icecast have search features that will download you a .pls file. This file has a File1 entry with the URL of the stream.

Live365 streams are harder to figure out, perhaps because the site would rather you use their pop-up listener window. Still, it's pretty straightforward - look at the popup's html source and find a stationId entry in the javascript block. The playable URL of this stream is http://www.live365.com/play/stationId

This isn't just JMF's problem — according to Apple's "What's New in QuickTime 6" document (PDF), the QuickTime API needs to either receive Shoutcast URL's inside a .pls playlist file, or to have the protocol changed to icy:// (and yes, "icy" typically implies Shoutcast streams, not Icecast. We'll also see the term "icy" throughout Shoutcast's metadata scheme in Part II)

To fix the problem, consider how JMF tries to find a suitable media player. In JMF, a DataSource object is responsible for connecting to media data, and for reporting what kind of media it provides. In the case of a PullDataSource, it also provides PullSourceStreams for reading the media data. When we ask Manager to find a Player, it iterates through a list of package prefixes, using a standardized naming scheme to find Players that can call setSource() with our DataSource and not throw an Exception. If no suitable Player is found, Manager throws a NoPlayerException.

So, jicyshout needs a DataSource clued into the fact that it provides MPEG audio data, and that it has no fixed duration (in theory, streams never end, at least not until the lawyers get involved). But there's more — the Player class that handles local MP3 files will reject a PullDataSource unless the PullSourceStream it provides implements the Seekable interface, which indicates a stream's ability to read in a random-access fashion.

One way not to do it is to fake a Seekable implementation, by implemeting the isRandomAccess() method as return false and hope that JMF's MP3 parser takes the hint. That producs the following exception:

java.lang.ArrayIndexOutOfBoundsException
        at com.ibm.media.parser.video.MpegParser.detectStreamType(MpegParser.java:580)

Looking at JMF's MpegParser source reveals that it's reading bytes from a stream it thinks has been reset to an earlier point. The out-of-bounds comes from the code's attempt to determine the encoding type, sampling rate, etc. of the stream, which comes in a four-byte MPEG header block. Parsing this requires bit-shifting parts of header bytes 2 and 3 and looking up a sampling frequency from an array — and since the fake Seekable plows ahead instead of going back in the stream, it provides the wrong bytes to the header parser and which in turn produces practically random array indicies.

One fix is to provide a real Seekable implementation via a BufferedInputStream. The way jicyshout's SeekableInputStream does this is to mark() the beginning of the stream, then implement seek() as "reset to 0, then skip the specified number of bytes". We can't buffer the stream forever, of course, but empirical evidence shows JMF's MPEG parser only jumps around the first few kilobytes, and never seek()s again after 100,000 bytes have been read.

The result of this was that a JMF Player that can finally read bytes from a Shoutcast, Icecast, or Live 365 stream and play the music. Here's the main() method from jicyshout's SimpleMP3DataSource, which shows each step of the process of connecting and playing a URL from the command-line:

    public static void main (String[] args) {
        if (args.length != 1) {
            System.out.println ("Usage: SimpleMP3DataSource http-stream-url");
            return;
        }
        try {
            MediaLocator ml = new MediaLocator (new URL (args[0]));
            System.out.println ("Got MediaLocator");
            SimpleMP3DataSource ds = new SimpleMP3DataSource (ml, false);
            // use the following version if you want metadata
            // SimpleMP3DataSource ds = new SimpleMP3DataSource (ml, true);
            System.out.println ("Got SimpleMP3DataSource");
            ds.connect();
            System.out.println ("Connected DataSource");
            Player p = Manager.createPlayer (ds);
            System.out.println ("Got Player");
            p.setSource (ds);
            System.out.println ("Set Player's DataSource");
            p.realize();
            System.out.println ("Called realize()");
            // wait until realized
            while (p.getState() != Controller.Realized) {
                Thread.sleep (100);
            }
            System.out.println ("Realized");
            p.start();
            System.out.println ("Started");
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

    }

One warning: a few streams will seize up instead of playing — it happens most often with Mac OS X and Live 365 streams, and isn't all that common at any rate.

The current codebase also includes a simple GUI for developers to play with, launched with java net.sourceforge.jicyshout.gui.SimpleDevGUI:
jicyshout devgui screenshot

You'll notice the GUI shows metadata — information about the stream like a name, home page, and current track information. In part II, we'll look at the many standards for storing and retrieving this kind of information.