/**
* Copyright 2009 Marc Stogaitis and Mimi Sun
*
* 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 org.gmote.client.android;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import org.gmote.common.Protocol.Command;
import org.gmote.common.media.MediaMetaInfo;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* Handles media player events.
*/
public class GmoteMediaPlayer {
private static final int MAX_RECONNECT_ATTEMPTS = 4;
private static final int REWIND_FF_JUMP = 15000;
// Messages that are sent out by this class.
public static final int BUFFERING_UPDATE = 1;
public static final int MEDIA_PLAYER_ERROR = 2;
public static final int MEDIA_INFO_UPDATE = 3;
public static final int SESSION_ERROR = 4;
public static final int MEDIA_DURATION_UPDATE = 5;
public static final int PREPARING_MEDIA = 6;
private static final int VOLUME_UNIT = 1;
private static final int MAX_VOLUME = 15;
private int volume = 10;
private int errorRetryCount = 0;
private boolean inErrorState = false;
// Use two media players, one for playing, the other for buffering the next
// song. (note: advanced buffering not implemented yet. We should do it as
// soon as we can since it will improve playlist performance when over 3G).
List<MediaPlayer> mediaPlayers = new ArrayList<MediaPlayer>();
private List<String> songs;
private int songIndex;
private Handler updateListener;
private MediaPlayer activePlayer;
boolean muted = false;
public GmoteMediaPlayer(Handler updateListener) {
mediaPlayers.add(createMediaPlayer());
this.updateListener = updateListener;
}
private MediaPlayer createMediaPlayer() {
MediaPlayer mp = new MediaPlayer();
mp.setOnBufferingUpdateListener(new BufferingListener());
mp.setOnCompletionListener(new CompletionListener());
mp.setOnErrorListener(new ErrorListener());
mp.setOnPreparedListener(new PreparedListener());
mp.setOnSeekCompleteListener(new SeekCompleteListener());
mp.setVolume(volume, volume);
return mp;
}
public synchronized void handleCommand(Command command) {
if (inErrorState) {
Log.i(ActivityUtil.DEBUG_TAG, "Ignoring gmote media player command due to in error state");
return;
}
MediaPlayer mediaPlayer = getActivePlayer();
if (mediaPlayer == null) {
return;
}
if (command == Command.FAST_FORWARD) {
fastForward();
} else if (command == Command.REWIND) {
rewind();
} else if (command == Command.FAST_FORWARD_LONG) {
nextSong();
} else if (command == Command.REWIND_LONG) {
previousSong();
} else if (command == Command.PAUSE) {
pause();
} else if (command == Command.PLAY) {
play();
} else if (command == Command.STOP) {
stop();
} else if (command == Command.VOLUME_DOWN) {
lowerVolume();
} else if (command == Command.VOLUME_UP) {
increaseVolume();
} else if (command == Command.MUTE) {
mute();
}
}
private void increaseVolume() {
volume = Math.min(MAX_VOLUME, volume + VOLUME_UNIT);
activePlayer.setVolume(volume, volume);
muted = false;
Log.i(ActivityUtil.DEBUG_TAG, "Volume set: " + volume);
}
private void lowerVolume() {
volume = Math.max(0, volume - VOLUME_UNIT);
activePlayer.setVolume(volume, volume);
muted = false;
Log.i(ActivityUtil.DEBUG_TAG, "Volume set: " + volume);
}
private void mute() {
if (muted) {
activePlayer.setVolume(volume, volume);
} else {
activePlayer.setVolume(0, 0);
}
muted = (muted == false);
}
/**
*
* @param songs
*/
public synchronized void playSongs(List<String> songs, int startingSongIndex) {
inErrorState = false;
this.songs = songs;
this.songIndex = startingSongIndex;
if (activePlayer != null) {
activePlayer.reset();
}
fetchSong();
}
private void nextSong() {
songIndex = (songIndex + 1) % songs.size();
activePlayer.reset();
fetchSong();
}
private void previousSong() {
songIndex = (songIndex - 1) % songs.size();
activePlayer.reset();
fetchSong();
}
private void fastForward() {
activePlayer.seekTo(Math.min(activePlayer.getCurrentPosition() + REWIND_FF_JUMP, activePlayer.getDuration()));
}
private void rewind() {
activePlayer.seekTo(Math.max(activePlayer.getCurrentPosition() - REWIND_FF_JUMP, 0));
}
private void pause() {
activePlayer.pause();
}
private synchronized void play() {
activePlayer.start();
}
private void stop() {
activePlayer.seekTo(0);
activePlayer.pause();
}
public synchronized int getSongPosition() {
if (activePlayer != null) {
return activePlayer.getCurrentPosition();
} else {
return 0;
}
}
private synchronized String getSongName() {
String strUrl = URLDecoder.decode(songs.get(songIndex));
URL url;
try {
url = new URL(strUrl);
} catch (MalformedURLException e) {
Log.e(ActivityUtil.DEBUG_TAG, e.getMessage(), e);
return null;
}
String nameAndPath = url.getFile();
nameAndPath = nameAndPath.substring("/files/".length());
return nameAndPath;
}
private synchronized void fetchSong() {
inErrorState = false;
setActivePlayer(null);
MediaPlayer player = mediaPlayers.get(0);
player.reset();
try {
String sessionId = getSessionId();
if (sessionId == null) {
String message = "FetchSong error. No session id. (not connected to server?)";
Log.i(ActivityUtil.DEBUG_TAG, message);
updateListener.sendMessage(Message.obtain(updateListener, SESSION_ERROR, "Error: Can't request a file without an active session. Please try to first connect to the server by hitting browse."));
return;
}
player.setDataSource(songs.get(songIndex) + "?sessionId=" + sessionId);
} catch (IllegalArgumentException e) {
Log.e(ActivityUtil.DEBUG_TAG, e.getMessage(), e);
} catch (IllegalStateException e) {
Log.e(ActivityUtil.DEBUG_TAG, e.getMessage(), e);
} catch (IOException e) {
Log.e(ActivityUtil.DEBUG_TAG, e.getMessage(), e);
}
updateListener.sendEmptyMessage(PREPARING_MEDIA);
player.prepareAsync();
}
private String getSessionId() {
Remote remote = Remote.getInstance();
return remote.getSessionId();
}
private synchronized MediaPlayer getActivePlayer() {
return activePlayer;
}
private synchronized void setActivePlayer(MediaPlayer mp) {
activePlayer = mp;
}
/**
* Utility function to format a duration into an easy to diaplay min:sec.
*/
public static String formatTime(int duration) {
int durationInSec = duration / 1000;
int minutes = durationInSec / (60 );
int seconds = durationInSec - (minutes * 60);
Formatter formatter = new Formatter();
return minutes + ":" + formatter.format("%02d", seconds);
}
private class BufferingListener implements MediaPlayer.OnBufferingUpdateListener {
public void onBufferingUpdate(MediaPlayer mp, int percent) {
Log.i(ActivityUtil.DEBUG_TAG, "BufferUpdate: " + percent);
updateListener.sendMessage(Message.obtain(updateListener, BUFFERING_UPDATE, new Integer(percent)));
}
}
// When media file is ready for playback.
private class PreparedListener implements MediaPlayer.OnPreparedListener {
public void onPrepared(MediaPlayer mp) {
Log.i(ActivityUtil.DEBUG_TAG, "On Prepared()");
synchronized (GmoteMediaPlayer.this) {
setActivePlayer(mp);
MediaMetaInfo metaInfo = new MediaMetaInfo(getSongName(),null, null, null, false);
updateListener.sendMessage(Message.obtain(updateListener, MEDIA_INFO_UPDATE, metaInfo));
updateListener.sendMessage(Message.obtain(updateListener, MEDIA_DURATION_UPDATE, mp.getDuration()));
play();
errorRetryCount = 0;
inErrorState = false;
}
}
}
// Song done playing.
private class CompletionListener implements MediaPlayer.OnCompletionListener {
public synchronized void onCompletion(MediaPlayer mp) {
Log.i(ActivityUtil.DEBUG_TAG, "On Completion()");
synchronized (GmoteMediaPlayer.this) {
nextSong();
}
}
}
private class ErrorListener implements MediaPlayer.OnErrorListener {
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.i(ActivityUtil.DEBUG_TAG, "MediaListener: Error: " + what + " " + extra);
synchronized (GmoteMediaPlayer.this) {
inErrorState = true;
}
new Thread(new ErrorRecoveryRunnable(mp, what, extra)).start();
return true;
}
}
private class SeekCompleteListener implements MediaPlayer.OnSeekCompleteListener {
public void onSeekComplete(MediaPlayer mp) {
Log.i(ActivityUtil.DEBUG_TAG, "SeekComplete");
}
}
public void setMediaPlayerListener(Handler localMediaPlayerListener) {
this.updateListener = localMediaPlayerListener;
}
public class ErrorRecoveryRunnable implements Runnable {
MediaPlayer mp;
int what;
int extra;
public ErrorRecoveryRunnable(MediaPlayer mp, int what, int extra) {
this.mp = mp;
this.what = what;
this.extra = extra;
}
public void run() {
recoverFromError(mp, what, extra);
}
}
private synchronized void recoverFromError(MediaPlayer mp, int what, int extra) {
if (errorRetryCount < 1) {
updateListener.sendMessage(Message.obtain(updateListener, MEDIA_PLAYER_ERROR,
"Trying to reconnect..."));
boolean isConnected = Remote.getInstance().isConnected();
for (int i = 0; i < MAX_RECONNECT_ATTEMPTS && !isConnected; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Log.e(ActivityUtil.DEBUG_TAG, e.getMessage(), e);
}
isConnected = Remote.getInstance().connect(true);
if (!isConnected) {
updateListener.sendMessage(Message.obtain(updateListener, MEDIA_PLAYER_ERROR,
"Reconnect attempt " + (i + 2) + " of " + MAX_RECONNECT_ATTEMPTS));
}
}
synchronized (GmoteMediaPlayer.this) {
mp.reset();
if (isConnected) {
fetchSong();
}
}
if (!isConnected) {
updateListener.sendMessage(Message.obtain(updateListener, MEDIA_PLAYER_ERROR,
"An error occurred: " + what + " " + extra));
}
errorRetryCount++;
} else {
updateListener.sendMessage(Message.obtain(updateListener, MEDIA_PLAYER_ERROR,
"An error occurred: " + what + " " + extra));
}
}
}