2011年9月6日 星期二

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 resourcewas 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.

沒有留言:

張貼留言