Unique Culture

27 Nov, 2010

Get Stream Metadata with Plain Java for Your Android App

Posted by: Sergei Izvorean In: Coding

When developing for Android streaming music isn’t much of a problem using MediaPlayer API, getting stream’s metadata is little bit of a challenge. Since the SHOUTcast standard isn’t open-source there is no clear documentation on how to deal with metadata. One resource was able to explain the protocol. However, many online implementation are using a 3rd party library which isn’t suitable to drop-in into your Android project.

Below is a preliminary Java class written specifically to obtain metadata from a stream URL using only Java API thus suitable for any Android project. The class is based on another great example to retrieve metadata.

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class IcyStreamMeta {
 
	protected URL streamUrl;
	private Map<String, String> metadata;
	private boolean isError;
 
	public IcyStreamMeta(URL streamUrl) {
		setStreamUrl(streamUrl);
 
		isError = false;
	}
 
	/**
	 * Get artist using stream's title
	 *
	 * @return String
	 * @throws IOException
	 */
	public String getArtist() throws IOException {
		Map<String, String> data = getMetadata();
 
		if (!data.containsKey("StreamTitle"))
			return "";
 
		String streamTitle = data.get("StreamTitle");
		String title = streamTitle.substring(0, streamTitle.indexOf("-"));
		return title.trim();
	}
 
	/**
	 * Get title using stream's title
	 *
	 * @return String
	 * @throws IOException
	 */
	public String getTitle() throws IOException {
		Map<String, String> data = getMetadata();
 
		if (!data.containsKey("StreamTitle"))
			return "";
 
		String streamTitle = data.get("StreamTitle");
		String artist = streamTitle.substring(streamTitle.indexOf("-")+1);
		return artist.trim();
	}
 
	public Map<String, String> getMetadata() throws IOException {
		if (metadata == null) {
			refreshMeta();
		}
 
		return metadata;
	}
 
	public void refreshMeta() throws IOException {
		retreiveMetadata();
	}
 
	private void retreiveMetadata() throws IOException {
		URLConnection con = streamUrl.openConnection();
		con.setRequestProperty("Icy-MetaData", "1");
		con.setRequestProperty("Connection", "close");
		con.setRequestProperty("Accept", null);
		con.connect();
 
		int metaDataOffset = 0;
		Map<String, List<String>> headers = con.getHeaderFields();
		InputStream stream = con.getInputStream();
 
		if (headers.containsKey("icy-metaint")) {
			// Headers are sent via HTTP
			metaDataOffset = Integer.parseInt(headers.get("icy-metaint").get(0));
		} else {
			// Headers are sent within a stream
			StringBuilder strHeaders = new StringBuilder();
			char c;
			while ((c = (char)stream.read()) != -1) {
				strHeaders.append(c);
				if (strHeaders.length() > 5 && (strHeaders.substring((strHeaders.length() - 4), strHeaders.length()).equals("\r\n\r\n"))) {
					// end of headers
					break;
				}
			}
 
			// Match headers to get metadata offset within a stream
			Pattern p = Pattern.compile("\\r\\n(icy-metaint):\\s*(.*)\\r\\n");
			Matcher m = p.matcher(strHeaders.toString());
			if (m.find()) {
				metaDataOffset = Integer.parseInt(m.group(2));
			}
		}
 
		// In case no data was sent
		if (metaDataOffset == 0) {
			isError = true;
			return;
		}
 
		// Read metadata
		int b;
		int count = 0;
		int metaDataLength = 4080; // 4080 is the max length
		boolean inData = false;
		StringBuilder metaData = new StringBuilder();
		// Stream position should be either at the beginning or right after headers
		while ((b = stream.read()) != -1) {
			count++;
 
			// Length of the metadata
			if (count == metaDataOffset + 1) {
				metaDataLength = b * 16;
			}
 
			if (count > metaDataOffset + 1 && count < (metaDataOffset + metaDataLength)) { 				
				inData = true;
			} else { 				
				inData = false; 			
			} 	 			
			if (inData) { 				
				if (b != 0) { 					
					metaData.append((char)b); 				
				} 			
			} 	 			
			if (count > (metaDataOffset + metaDataLength)) {
				break;
			}
 
		}
 
		// Set the data
		metadata = IcyStreamMeta.parseMetadata(metaData.toString());
 
		// Close
		stream.close();
	}
 
	public boolean isError() {
		return isError;
	}
 
	public URL getStreamUrl() {
		return streamUrl;
	}
 
	public void setStreamUrl(URL streamUrl) {
		this.metadata = null;
		this.streamUrl = streamUrl;
		this.isError = false;
	}
 
	public static Map<String, String> parseMetadata(String metaString) {
		Map<String, String> metadata = new HashMap();
		String[] metaParts = metaString.split(";");
		Pattern p = Pattern.compile("^([a-zA-Z]+)=\\'([^\\']*)\\'$");
		Matcher m;
		for (int i = 0; i < metaParts.length; i++) {
			m = p.matcher(metaParts[i]);
			if (m.find()) {
				metadata.put((String)m.group(1), (String)m.group(2));
			}
		}
 
		return metadata;
	}
}

The class is suitable for most cases. You’ll be able to retrieve some common information like artist and song title. Other metadata is available in the Map returned by getMetadata().

To effectively use the class in your Android application I would suggest using AsyncTask. This will allow your app to asynchronously get the metadata and properly interact with UI thread. Below is the code I used:

protected class MetadataTask extends AsyncTask {
	protected IcyStreamMeta streamMeta;
 
	@Override
	protected IcyStreamMeta doInBackground(URL... urls) {
		streamMeta = new IcyStreamMeta(urls[0]);
		try {
			streamMeta.refreshMeta();
		} catch (IOException e) {
			// TODO: Handle
			Log.e(MetadataTask.class.toString(), e.getMessage());
		}
		return streamMeta;
	}
 
	@Override
	protected void onPostExecute(IcyStreamMeta result) {
		try {
			txtArtist.setText(streamMeta.getArtist());
			txtTitle.setText(streamMeta.getTitle());
		} catch (IOException e) {
			// TODO: Handle
			Log.e(MetadataTask.class.toString(), e.getMessage());
		}
	}
}

The code isn’t final, questions and comments are welcome.

8 Responses to "Get Stream Metadata with Plain Java for Your Android App"

1 | emulienfou

November 29th, 2010 at 1:16 pm

Avatar

Hi,
I try your code but nothing display when i try to setText getTitle or getArtist.

I try this code under Froyo.

2 | emulienfou

November 29th, 2010 at 1:27 pm

Avatar

Ok it’s work but the display it’s very long normal ?

3 | Parrotlover77

December 30th, 2010 at 12:45 am

Avatar

Thanks for sharing.

I wrote an app that included SHOUTcast support for Windows Mobile a while back and became an official partner. As such, I have the actual API docs and your code is spot-on.

The only problem with this workaround, and frankly, I don’t see any way around it, is that to keep your stream title up-to-date, you have to periodically (perhaps once every second) ping the station’s web server. That’s wasteful for the station and silly since even the first bit of metadata is embedded deep into the stream (whatever the interval is).

This would be less of a big deal if the Froyo update didn’t kill the “NPR method” of creating a proxy stream. Proxies just don’t seem to work on 2.2 and above. I haven’t found a workaround that is successful yet, anyway. If proxies still worked, I could just look into the stream as it went along and updated during the metadata intervals like sane media players do. I really don’t understand why Google is making something that should be so simple, so incredibly difficult.

4 | Parrotlover77

January 2nd, 2011 at 8:44 pm

Avatar

Someone came up with a solution to adapt NPR’s StreamProxy to 2.2+ Android phones. This beats the two-connection workaround stated in this post. http://stackoverflow.com/questions/4209382/streaming-with-android-mediaplayer-in-sdk-8/4560202#4560202

The trick is to make sure all your headers terminate with carriage return + line feed (\r\n) instead of just line feed (\n). Once you are going through a proxy, you can simply extract the data as it comes through, using the code in this post as a template for how to request metadata (Icy-Metadata header) and then every “metaint” interval, suck out the data, but do NOT pass that part to the underlying mediaplayer.

5 | harikj

January 25th, 2011 at 11:09 pm

Avatar

@Parrotlover77

I’m using the NPR method of playing a stream. But am unable to get the cuepoint data that my stream provider is sending with the stream. If I use the method mentioned in the post then I’m getting only the StreamTitle but the metaint is not there. Can someone clarify on this please.

6 | Andras Hatvani

September 2nd, 2011 at 5:12 am

Avatar

It’s working, thanks for sharing it. I’m not sure, whether to use a proxy or recurring polling with an AsyncTask…

7 | vijae manlapaz

December 1st, 2011 at 12:35 am

Avatar

hi, i have use the code in this post i was able to make it work but instead of displaying the song title it displayed the site title

here is the code i used to display the title.
please help! thanks

private void meta(){
URL url;
try{
url = new URL(“http://rakista.com:8000/listen.mp3″);
IcyStreamMeta icy = new IcyStreamMeta(url);
TextView title = (TextView) findViewById(R.id.song_title);
String T = icy.getTitle();
title.setText(T);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

8 | imon

December 13th, 2011 at 10:43 am

Avatar

Getting error in Java

“Exception in thread “main” java.lang.StringIndexOutOfBoundsException: String index out of range: -1
at java.lang.String.substring(Unknown Source)
at IcyStreamMeta.getArtist(IcyStreamMeta.java:36)
at MetaTest.main(MetaTest.java:21)

Comment Form


  • imon: Getting error in Java "Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: -1 at java.lang.String.su
  • vijae manlapaz: hi, i have use the code in this post i was able to make it work but instead of displaying the song title it displayed the site title here is the co
  • Andras Hatvani: It's working, thanks for sharing it. I'm not sure, whether to use a proxy or recurring polling with an AsyncTask...

Flickr PhotoStream

  • oni
  • Chingay Festival 2012 in Singapore
  • Chingay Festival 2012 in Singapore

About

Pittsburgh web and application developer @ Eyeflow and Penn State student.