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.

16 Sep, 2010

My New Resume

Posted by: Sergei Izvorean In: Design

Designed myself a new resume.

Sergei Izvorean Resume

31 Aug, 2010

On-campus Radio iPhone Application

Posted by: Sergei Izvorean In: Design

Just to expand my skills portfolio I decided to design and develop and iPhone application for Penn State Greater Allegheny on-campus radio. Bellow is a initial design of the app:

WMKP Radio iPhone App

03 Jun, 2009

How to serve Excel files on Google App Engine

Posted by: Sergei Izvorean In: Coding

Google App Engine is a hosting service for your Python and, lately, Java applications. In this case I’m going to talk about Python. Since an application cannot save files it might seem hard to serve an Excel file. Thankfully a great library pyExcelerator is very flexibile and instead of saving a file you can offer a user to download the dynamically generated Excel file. Here is what you do:

from pyExcelerator import *
 
class MainPage(webapp.RequestHandler):
    def get(self):
        wb = Workbook()
        ws0 = wb.add_sheet('Sheet 1')
        # Rows and columns count starts from 0
        for x in range(10):
            for y in range(10):
                # writing to a specific x,y
                ws0.write(x, y, "this is cell %s, %s" % (x,y))
 
        # HTTP headers to force file download
        self.response.headers['Content-Type'] = 'application/ms-excel'
        self.response.headers['Content-Transfer-Encoding'] = 'Binary'
        self.response.headers['Content-disposition'] = 'attachment; filename="workbook.xls"'
 
        # output to user
        wb.save(self.response.out)

Happy coding!


  • 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

  • Customizable
  • . @Regrann del día para @seecolombia - San Luis de Palenque, Casanare, Colombia - The llanero culture of the Eastern Plains is unique within Colombia - this was taken at the Museo Llanerazo near San Luis de Palenque in Casanare department #Colombia #colom
  • La Marta
  • maison de pêcheur à la cascade Datanla

About

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