/*
* ServeStream: A HTTP stream browser/player for Android
* Copyright 2014 William Seemann
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sourceforge.servestream.media;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.servestream.provider.Media;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
public class ShoutCastRetrieverTask {
private Context mContext = null;
private long mId;
private MetadataRetrieverListener mListener;
private MetadataTask mTask;
private boolean mNoMetadata;
public ShoutCastRetrieverTask(Context context, long id) {
mContext = context;
mId = id;
mTask = null;
mNoMetadata = false;
// Verify that the host activity implements the callback interface
try {
// Instantiate the MetadataRetrieverListener so we can send events to the host
mListener = (MetadataRetrieverListener) context;
} catch (ClassCastException e) {
// The activity doesn't implement the interface, throw exception
throw new ClassCastException(context.toString()
+ " must implement MetadataRetrieverListener");
}
}
private synchronized String getUri(Context context, long id) {
String uri = null;
// Form an array specifying which columns to return.
String [] projection = new String [] { Media.MediaColumns.URI };
// Get the base URI for the Media Files table in the Media content provider.
Uri mediaFile = Media.MediaColumns.CONTENT_URI;
// Make the query.
Cursor cursor = context.getContentResolver().query(mediaFile,
projection,
Media.MediaColumns._ID + " = " + id,
null,
null);
if (cursor.moveToNext()) {
uri = cursor.getString(cursor.getColumnIndex(Media.MediaColumns.URI));
}
cursor.close();
return uri;
}
public synchronized void start() {
if (mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED) {
mTask.cancel();
}
if (!mNoMetadata) {
mTask = new MetadataTask();
mTask.execute();
}
}
public synchronized void stop() {
if (mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED) {
mTask.cancel();
}
}
private class MetadataTask implements Runnable {
private boolean mIsCancelled;
private AsyncTask.Status mStatus;
private MetadataTask() {
mStatus = AsyncTask.Status.PENDING;
}
@Override
public void run() {
mStatus = AsyncTask.Status.RUNNING;
int retries = 0;
boolean metadataFound;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
if (isCancelled()) {
return;
}
String uri = getUri(mContext, mId);
if (uri != null) {
while (!isCancelled()) {
Metadata metadata = null;
metadataFound = false;
FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
try {
mmr.setDataSource(uri);
String icyMetadata = mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_ICY_METADATA);
if ((metadata = parseMetadata(icyMetadata)) != null) {
metadataFound = true;
}
} catch (IllegalArgumentException ex) {
} finally {
mmr.release();
}
if (metadataFound) {
if (mListener != null) {
mListener.onMetadataParsed(mId, metadata);
}
retries = 0;
} else {
retries++;
if (retries >= 2) {
mNoMetadata = true;
break;
}
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
}
mStatus = AsyncTask.Status.FINISHED;
}
private Metadata parseMetadata(String icyMetadata) {
String title = null;
String artist = null;
Metadata metadata = null;
if (icyMetadata == null) {
return metadata;
}
String streamTitle = null;
Map<String, String> parsedMetadata = new HashMap<String, String>();
String[] metaParts = icyMetadata.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()) {
parsedMetadata.put((String)m.group(1), (String)m.group(2));
}
}
streamTitle = parsedMetadata.get("StreamTitle");
if (streamTitle == null || streamTitle.trim().equals("")) {
streamTitle = parsedMetadata.get("StreamTitleReplay");
if (streamTitle == null || streamTitle.trim().equals("")) {
return metadata;
}
}
// check if the stream title contain a "-" character. This is usually done
// to indicate "artist - title". If not, don't try to parse up the string
// just store it
if (streamTitle.indexOf("-") != -1) {
artist = streamTitle.substring(0, streamTitle.indexOf("-")).trim();
title = streamTitle.substring(streamTitle.indexOf("-") + 1).trim();
} else {
artist = streamTitle.trim();
title = "";
}
// if we didn't obtain at least the title and artist then don't store
// the metadata since it's pretty useless
if (title != null &&
artist != null) {
HashMap<String, Object> meta = new HashMap<String, Object>();
meta.put(Metadata.METADATA_KEY_TITLE, title);
meta.put(Metadata.METADATA_KEY_ARTIST, artist);
// Form an array specifying which columns to return.
metadata = new Metadata();
metadata.parse(meta);
}
return metadata;
}
private synchronized boolean isCancelled() {
return mIsCancelled;
}
public synchronized boolean cancel() {
if (mStatus != AsyncTask.Status.FINISHED) {
mIsCancelled = true;
return true;
}
return false;
}
public synchronized AsyncTask.Status getStatus() {
return mStatus;
}
public void execute() {
new Thread(this, "").start();
}
}
}