package com.piterwilson.audio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Timer;
import java.util.TimerTask;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* Plays a MP3 Radio stream using MediaExtractor, MediaCodec and AudioTrack
*
* @author Juan Carlos Ospina Gonzalez / juan@supersteil.com
*/
public class MP3RadioStreamPlayer {
public final String LOG_TAG = "MP3RadioStreamPlayer";
protected MediaExtractor extractor;
protected MediaCodec codec;
protected AudioTrack audioTrack;
protected int inputBufIndex;
protected int bufIndexCheck;
protected int lastInputBufIndex;
protected Boolean doStop = false;
/*
* Delegate to receive notifications
*/
protected MP3RadioStreamDelegate mDelegate;
/**
* Set the delegate for this instance. The delegate will receive notifications about the player's status
*
* @param mDelegate
*/
public void setDelegate(MP3RadioStreamDelegate mDelegate)
{
this.mDelegate = mDelegate;
}
public MP3RadioStreamDelegate getDelegate()
{
return this.mDelegate;
}
// indicates the state our service:
public enum State {
Retrieving, // retrieving music (filling buffer)
Stopped, // player is stopped and not prepared to play
Playing, // playback active
};
/**
* Current player state
*/
State mState = State.Retrieving;
/**
* Getter for mState
*/
public State getState()
{
return mState;
}
/**
* String The url of the radio stream
*/
private String mUrlString;
public void setUrlString(String mUrlString) {
this.mUrlString = mUrlString;
}
/**
* mUrlString getter
*/
public String getUrlString() {
return mUrlString;
}
/**
* Constructor
*
* @param url String The url of the radio stream
*/
public MP3RadioStreamPlayer(){
mState = State.Stopped;
}
/**
* Attempts to fetch mp3 data from the mUrlString location, decode it and feed it to an AudioTrack instance
*
* @return void
* @throws IOException
*/
public void play() throws IOException
{
mState = State.Retrieving;
mDelegateHandler.onRadioPlayerBuffering(MP3RadioStreamPlayer.this);
doStop = false;
bufIndexCheck = 0;
lastInputBufIndex = -1;
myTimerTask= new CheckProgressTimerTask();
myTimer = new Timer();
myTimer.scheduleAtFixedRate(myTimerTask, 0, 1000); //(timertask,delay,period)
new DecodeOperation().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private DelegateHandler mDelegateHandler = new DelegateHandler();
class DelegateHandler extends Handler {
@Override
public void handleMessage(Message msg) {
}
public void onRadioPlayerPlaybackStarted(MP3RadioStreamPlayer player)
{
if(MP3RadioStreamPlayer.this.mDelegate != null)
{
MP3RadioStreamPlayer.this.mDelegate.onRadioPlayerPlaybackStarted(player);
}
}
public void onRadioPlayerStopped(MP3RadioStreamPlayer player)
{
if(MP3RadioStreamPlayer.this.mDelegate != null)
{
MP3RadioStreamPlayer.this.mDelegate.onRadioPlayerStopped(player);
}
}
public void onRadioPlayerError(MP3RadioStreamPlayer player)
{
if(MP3RadioStreamPlayer.this.mDelegate != null)
{
MP3RadioStreamPlayer.this.mDelegate.onRadioPlayerError(player);
}
}
public void onRadioPlayerBuffering(MP3RadioStreamPlayer player)
{
if(MP3RadioStreamPlayer.this.mDelegate != null)
{
MP3RadioStreamPlayer.this.mDelegate.onRadioPlayerBuffering(player);
}
}
};
Timer myTimer;
CheckProgressTimerTask myTimerTask;
private class CheckProgressTimerTask extends TimerTask {
@Override
public void run() {
if(lastInputBufIndex == bufIndexCheck)
{
Log.d(LOG_TAG, "----lastInputBufIndex "+lastInputBufIndex);
Log.d(LOG_TAG, "----bufIndexCheck "+bufIndexCheck);
// buferring ... (buffer has not progressed)
if(MP3RadioStreamPlayer.this.mState == State.Playing)
{
Log.d(LOG_TAG, "buffering???? onRadioPlayerBuffering");
mDelegateHandler.onRadioPlayerBuffering(MP3RadioStreamPlayer.this);
}
MP3RadioStreamPlayer.this.mState = State.Retrieving;
}
lastInputBufIndex = bufIndexCheck;
Log.d(LOG_TAG, "lastInputBufIndex "+lastInputBufIndex);
if(bufIndexCheck > 9999)
{
bufIndexCheck = 0;
}
}
}
/**
* @throws IOException
*
*
*/
private void decodeLoop()
{
ByteBuffer[] codecInputBuffers;
ByteBuffer[] codecOutputBuffers;
// extractor gets information about the stream
extractor = new MediaExtractor();
try {
extractor.setDataSource(this.mUrlString);
} catch (Exception e) {
mDelegateHandler.onRadioPlayerError(MP3RadioStreamPlayer.this);
return;
}
MediaFormat format = extractor.getTrackFormat(0);
String mime = format.getString(MediaFormat.KEY_MIME);
// the actual decoder
codec = MediaCodec.createDecoderByType(mime);
codec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
codec.start();
codecInputBuffers = codec.getInputBuffers();
codecOutputBuffers = codec.getOutputBuffers();
// get the sample rate to configure AudioTrack
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
Log.i(LOG_TAG,"mime "+mime);
Log.i(LOG_TAG,"sampleRate "+sampleRate);
// create our AudioTrack instance
audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT,
AudioTrack.getMinBufferSize (
sampleRate,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT
),
AudioTrack.MODE_STREAM
);
// start playing, we will feed you later
audioTrack.play();
extractor.selectTrack(0);
// start decoding
final long kTimeOutUs = 10000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int noOutputCounter = 0;
int noOutputCounterLimit = 50;
while (!sawOutputEOS && noOutputCounter < noOutputCounterLimit && !doStop) {
//Log.i(LOG_TAG, "loop ");
noOutputCounter++;
if (!sawInputEOS) {
inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
bufIndexCheck++;
// Log.d(LOG_TAG, " bufIndexCheck " + bufIndexCheck);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
int sampleSize =
extractor.readSampleData(dstBuf, 0 /* offset */);
long presentationTimeUs = 0;
if (sampleSize < 0) {
Log.d(LOG_TAG, "saw input EOS.");
sawInputEOS = true;
sampleSize = 0;
} else {
presentationTimeUs = extractor.getSampleTime();
}
// can throw illegal state exception (???)
codec.queueInputBuffer(
inputBufIndex,
0 /* offset */,
sampleSize,
presentationTimeUs,
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
if (!sawInputEOS) {
extractor.advance();
}
}
else
{
Log.e(LOG_TAG, "inputBufIndex " +inputBufIndex);
}
}
int res = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (res >= 0) {
//Log.d(LOG_TAG, "got frame, size " + info.size + "/" + info.presentationTimeUs);
if (info.size > 0) {
noOutputCounter = 0;
}
int outputBufIndex = res;
ByteBuffer buf = codecOutputBuffers[outputBufIndex];
final byte[] chunk = new byte[info.size];
buf.get(chunk);
buf.clear();
if(chunk.length > 0){
audioTrack.write(chunk,0,chunk.length);
if(this.mState != State.Playing)
{
mDelegateHandler.onRadioPlayerPlaybackStarted(MP3RadioStreamPlayer.this);
}
this.mState = State.Playing;
}
codec.releaseOutputBuffer(outputBufIndex, false /* render */);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(LOG_TAG, "saw output EOS.");
sawOutputEOS = true;
}
} else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
Log.d(LOG_TAG, "output buffers have changed.");
} else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat oformat = codec.getOutputFormat();
Log.d(LOG_TAG, "output format has changed to " + oformat);
} else {
Log.d(LOG_TAG, "dequeueOutputBuffer returned " + res);
}
}
Log.d(LOG_TAG, "stopping...");
relaxResources(true);
this.mState = State.Stopped;
doStop = true;
// attempt reconnect
if(sawOutputEOS)
{
try {
MP3RadioStreamPlayer.this.play();
return;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(noOutputCounter >= noOutputCounterLimit)
{
mDelegateHandler.onRadioPlayerError(MP3RadioStreamPlayer.this);
}
else
{
mDelegateHandler.onRadioPlayerStopped(MP3RadioStreamPlayer.this);
}
}
public void release()
{
stop();
relaxResources(false);
}
private void relaxResources(Boolean release)
{
if(codec != null)
{
if(release)
{
codec.stop();
codec.release();
codec = null;
}
}
if(audioTrack != null)
{
audioTrack.flush();
audioTrack.release();
audioTrack = null;
}
}
/**
* Stops playback
*
* @return void
*/
public void stop()
{
doStop = true;
if(myTimer != null)
{
myTimer.cancel();
myTimer = null;
}
if(myTimerTask != null)
{
myTimerTask.cancel();
myTimerTask = null;
}
}
/**
* AsyncTask that takes care of running the decode/playback loop
*
*/
private class DecodeOperation extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... values) {
MP3RadioStreamPlayer.this.decodeLoop();
return null;
}
@Override
protected void onPreExecute() {
}
@Override
protected void onProgressUpdate(Void... values) {
}
}
}