Get Stream Metadata with Plain Java for Your Android App

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.