/**
* MP3Player.java
*
* This program is distributed under the terms of the GNU General Public
* License
* Copyright 2008 NJ Pearman
*
* This file is part of MobScrob.
*
* MobScrob is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MobScrob is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MobScrob. If not, see <http://www.gnu.org/licenses/>.
*/
package mobscrob.player;
import java.io.IOException;
import java.io.InputStream;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.StringItem;
import javax.microedition.media.Manager;
import javax.microedition.media.MediaException;
import javax.microedition.media.Player;
import javax.microedition.media.PlayerListener;
import mobscrob.event.Listener;
import mobscrob.id3.TrackMetadata;
import mobscrob.logging.Log;
import mobscrob.logging.LogFactory;
import mobscrob.midlet.Callback;
import mobscrob.midlet.MobScrobDisplay;
import mobscrob.playlist.Playlist;
/**
* @author neill
*
*/
public class MP3Player implements Runnable, PlayerListener {
private static final Log log = LogFactory.getLogger(MP3Player.class);
private static final String RESOURCE = "resource:";
private static final String FILE = "file:";
private static final String MP3 = ".mp3";
private Callback cb;
private CurrentTrack trackDisplay;
private TrackTimer timer;
private Player mp3Player;
private String queuedFile;
private Playlist playlist;
private TrackMetadata stoppedTrack;
private TrackMetadata currentTrack;
private PlayProcessor prePlayProcessor;
private PlayProcessorSet postProcessors;
public MP3Player(Callback cb) {
this.cb = cb;
this.trackDisplay = new CurrentTrack();
timer = new TrackTimer();
this.postProcessors = new PlayProcessorSet(2);
}
public void addPlaylist(Playlist playlist) {
final String methodName = "1";
if (playlist != null) {
this.playlist = playlist;
} else {
log.warn(methodName, "Already have a playlist");
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run() {
final String methodName = "2";
log.info(methodName, "Started thread...");
try {
// stop player if it is currently playing.
this.stopCurrentTrack();
// start player to play track
this.startTrack(queuedFile);
} catch (IOException e) {
log.error(methodName, "IOException: " + e.getMessage(), e);
} catch (MediaException e) {
log.error(methodName, "MediaException: " + e.getMessage(), e);
} catch (Throwable e) {
log.error(methodName, "Unchecked exception: " + e.getMessage(), e);
}
}
/**
* Sets the post processor for this player. If it has already been set, this
* call is ignored.
*
* @param postProcessor
*/
public void setPrePlayProcessor(PlayProcessor preProcessor) {
if (this.prePlayProcessor == null) {
this.prePlayProcessor = preProcessor;
}
}
public void addPostPlayProcessor(PlayProcessor processor) {
if(processor != null) {
postProcessors.addProcessor(processor);
}
}
/**
* Starts playing a track
*/
public void play(String filename) {
final String methodName = "3";
queuedFile = filename;
// play the track in a new thread
log.info(methodName, "Starting track " + filename);
new Thread(this).start();
}
/**
* Stops and closes the current track. This method will not allow the
* current track to be started up again at the point it was stopped.
*/
public void stopCurrentTrack() {
final String methodName = "4";
log.info(methodName, "Stopping track");
// cache the track that is stopping if there is one
// and note how long its played for
if (currentTrack != null && mp3Player != null && mp3Player.getState() == Player.STARTED) {
stoppedTrack = currentTrack;
stoppedTrack.setCurrentPosition((long) (mp3Player.getMediaTime() / 1000));
if (stoppedTrack.getCurrentPosition() == Player.TIME_UNKNOWN) {
log.warn(methodName, "Unknown track position");
}
}
if (mp3Player != null && mp3Player.getState() != Player.CLOSED) {
try {
mp3Player.stop();
timer.stop();
mp3Player.close();
log.info(methodName, "Stopped and closed player");
} catch (MediaException e) {
log.error(methodName, "Error stopping player: "
+ e.getMessage(), e);
}
} else {
log.info(methodName, "Nothing to stop");
}
}
/**
* Starts a new Player with the specified resource
*
* @param resourceName
* @throws IOException
* @throws MediaException
*/
private void startTrack(String resourceName) throws IOException,
MediaException {
final String methodName = "5";
log.info(methodName, "Playing " + resourceName);
// reset the display time
trackDisplay.resetTime();
// set up the player as MP3 and add this as a listener
mp3Player = Manager.createPlayer(getResourceAsStream(resourceName),
"audio/mpeg");
mp3Player.addPlayerListener(this);
log.info(methodName, "Created player as MP3 and added listener");
// set up the player and start playing
mp3Player.prefetch();
mp3Player.realize();
long length = mp3Player.getDuration() / 1000;
log.info(methodName, "Realized player, track length: " + length);
currentTrack = new TrackMetadata(resourceName, length);
prePlayProcess(currentTrack);
mp3Player.start();
new Thread(timer).start();
}
/**
* Checks that the file has .mp3 extension, and retrieves a Stream for the
* resource from the correct location.
*
* @param resourceName
* @return
* @throws IOException
*/
public InputStream getResourceAsStream(String resourceName)
throws IOException {
final String methodName = "6";
InputStream is = null;
// check we are getting an MP3 file
if (!resourceName.endsWith(MP3)) {
throw new IOException("Incompatible resource: " + resourceName
+ "\r\n Can only play MP3.");
}
if (resourceName == null) {
String msg = "Null resource type";
log.error(methodName, msg);
throw new IOException(msg);
} else if (resourceName.startsWith(RESOURCE)) {
int index = resourceName.indexOf(':');
String trimmed = resourceName.substring(index + 1);
log.debug(methodName, "Getting jar resource " + trimmed);
is = this.getClass().getResourceAsStream(trimmed);
log.info(methodName, "Got resource as stream, "
+ (is == null ? "stream is null" : "stream not null"));
} else if (resourceName.startsWith(FILE)) {
log.info(methodName, "Opening MP3 file stream");
FileConnection fc = (FileConnection) Connector.open(resourceName);
if (!fc.exists()) {
throw new IOException("Resource " + resourceName
+ " cannot be found");
}
is = fc.openInputStream();
log.info(methodName, "Got file as stream, "
+ (is == null ? "stream is null" : "stream not null"));
} else {
String msg = "Unknown resource type: " + resourceName;
// unknown resource type
log.error(methodName, msg);
throw new IOException(msg);
}
return is;
}
/**
* Receives notifications of events in the Player. Primarily for logging.
*
* @param player
* @param event
* @param eventData
*/
public void playerUpdate(Player player, String event, Object eventData) {
final String methodName = "7";
log.debug(methodName, "Update from player: " + event + ", "
+ currentTrack.getFileLocation());
if (PlayerListener.STARTED.equals(event)) {
log.info(methodName, "player started");
} else if (PlayerListener.END_OF_MEDIA.equals(event)) {
// get the track thats finished and start up the next one
log.info(methodName, "End of media");
timer.stop();
// process a track that has finished, assume played time == track
// length
currentTrack.setCurrentPosition(currentTrack.getTrackLength());
log.info(methodName, "Set current track position to "
+ currentTrack.getCurrentPosition());
postPlayProcess(currentTrack);
if (playlist != null) {
if (playlist.hasNext()) {
log.info(methodName, "Playing next track in playlist");
play(playlist.getNext().getLocation());
}
} else {
log.warn(methodName,
"Playlist is null so no more tracks to play :(");
}
} else if (PlayerListener.STOPPED.equals(event)) {
// post process - note if a track is stopped then started again
// it won't currently be processed the second time through
// needs to be looked at to act more intelligently
log.info(methodName, "Stopped");
postPlayProcess(stoppedTrack);
} else if (PlayerListener.CLOSED.equals(event)) {
log.info(methodName, "Closed");
}
}
public void prePlayProcess(TrackMetadata track) {
final String methodName = "8";
if (prePlayProcessor != null) {
track.setListener(trackDisplay);
prePlayProcessor.queueTrack(track);
log.info(methodName, "Pre processing track");
} else {
log.warn(methodName, "No pre-play processor set");
}
}
/**
* Call the post processor
*
* @param track
*/
public void postPlayProcess(TrackMetadata track) {
final String methodName = "9";
// do something with a finished track
if (postProcessors != null && !track.isPostProcessed()) {
postProcessors.processByAll(track);
} else {
log.warn(methodName, "Not post processing track");
}
}
public void displayCurrentTrack(Display parent) {
this.trackDisplay.open(parent);
}
private class PlayProcessorSet {
private int index;
private PlayProcessor[] processors;
PlayProcessorSet(int capacity) {
this.index = 0;
this.processors = new PlayProcessor[capacity];
}
/**
* Add a PlayProcessor to this PlayProcessorSet, provided the capacity
* of the set hasn't been reached. If it has, then this call is
* ignored.
* @param processor
*/
void addProcessor(PlayProcessor processor) {
final String methodName = "10";
if(index < processors.length) {
this.processors[index++] = processor;
} else {
log.error(methodName, "Unable to add more PlayProcessors, capacity reached");
}
}
/**
* Process the track in all PlayProcessors contained in this set
* @param track
*/
void processByAll(TrackMetadata track) {
final String methodName = "11";
for(int i=0; i<processors.length; i++) {
if(processors[i] != null) {
processors[i].queueTrack(track);
track.setPostProcessed(true);
log.info(methodName, "Post processing track");
}
}
}
}
/**
* Runnable class used to time a track and update the display appropriately.
*
* @author Neill
*
*/
public class TrackTimer implements Runnable {
public long MICRO_SECONDS_PER_SECOND = 1000 * 1000;
public long ONE_SECOND = 1000;
boolean playing;
boolean prevStopped;
TrackTimer() {
prevStopped = true;
}
public void run() {
final String methodName = "12";
long sleepToTime, currentTime, sleepTime;
log.info(methodName, "Starting track timer...");
while (!prevStopped) {
try {
Thread.sleep(ONE_SECOND);
} catch (Exception e) {
}
}
// start the track timer
playing = true;
prevStopped = false;
log.info(methodName, "Entering track timer loop");
sleepToTime = System.currentTimeMillis() + ONE_SECOND;
while (playing) {
// sleep until next second to update display
try {
currentTime = System.currentTimeMillis();
sleepTime = currentTime >= sleepToTime ? 1000 : sleepToTime - currentTime;
Thread.sleep(sleepTime);
sleepToTime = System.currentTimeMillis() + ONE_SECOND;
// update display time
trackDisplay.increaseTimeByOneSecond();
} catch (Exception e) {
log.error(methodName, "TrackTimer was interrupted: "
+ e.getMessage(), e);
}
}
log.info(methodName, "Exited track timer loop");
prevStopped = true;
}
void stop() {
final String methodName = "stop";
log.info(methodName, "Stopping track timer");
playing = false;
}
}
/**
* Display for the current track being played.
*
* @author Neill
*
*/
public class CurrentTrack implements MobScrobDisplay, CommandListener, Listener {
private Command exitCmd = new Command("Main", Command.BACK, 1);
private Command stopCmd = new Command("Stop", Command.OK, 1);
private Command startCmd = new Command("Start", Command.OK, 1);
private Form track;
private StringItem timeDisplay;
private StringItem trackDisplay;
private StringItem artistDisplay;
private StringItem albumDisplay;
private long playTime;
CurrentTrack() {
playTime = 0;
init();
}
private void init() {
track = new Form("No track selected");
artistDisplay = new StringItem("", "");
trackDisplay = new StringItem("", "");
albumDisplay = new StringItem("", "");
timeDisplay = new StringItem("", "Time: " +
getDisplayTimeString(this.playTime) + " secs");
artistDisplay.setFont(Font.getFont(Font.FACE_PROPORTIONAL,
Font.STYLE_PLAIN, Font.SIZE_MEDIUM));
trackDisplay.setFont(Font.getFont(Font.FACE_PROPORTIONAL,
Font.STYLE_PLAIN, Font.SIZE_LARGE));
albumDisplay.setFont(Font.getFont(Font.FACE_PROPORTIONAL,
Font.STYLE_PLAIN, Font.SIZE_MEDIUM));
timeDisplay.setFont(Font.getFont(Font.FACE_PROPORTIONAL,
Font.STYLE_PLAIN, Font.SIZE_MEDIUM));
track.addCommand(exitCmd);
track.append(trackDisplay);
track.append(artistDisplay);
track.append(albumDisplay);
track.append(timeDisplay);
track.setCommandListener(this);
}
public void open(Display parent) {
final String methodName = "13";
if (currentTrack == null) {
log.warn(methodName, "No current track to display");
track.setTitle("No track selected");
} else {
log.warn(methodName, "Have a track to display");
track.setTitle("Current track");
displayCurrentTrack();
setTime();
}
setActiveCommand();
parent.setCurrent(track);
}
void setActiveCommand() {
final String methodName = "setActiveCommand";
// check whether track is currently playing or not
if (mp3Player == null) {
log.warn(methodName,
"Can't set current track action - player is null");
} else if (mp3Player.getState() == Player.STARTED) {
track.addCommand(stopCmd);
track.removeCommand(startCmd);
} else {
track.addCommand(startCmd);
track.removeCommand(stopCmd);
}
}
/**
* Increases the display time by one second.
*/
void increaseTimeByOneSecond() {
playTime++;
setTime();
}
/**
* Sets the current display details to the details of the track
* currently being played.
*/
void displayCurrentTrack() {
trackDisplay.setText(currentTrack.getTrackTitle() + "\n");
artistDisplay.setText(currentTrack.getArtist() + "\n");
albumDisplay.setText("#" + currentTrack.getTrackNumber() + " - "
+ currentTrack.getAlbumTitle() + "\n");
}
/**
* Resets the display time to zero.
*/
void resetTime() {
playTime = 0;
setTime();
}
/**
* Updates the display time.
*/
void setTime() {
if (timeDisplay != null) {
timeDisplay.setText("Time: " + getDisplayTimeString(this.playTime));
}
}
/**
* Determines the string to display reflecting the time in seconds
* passed to this method.
* @param timeInSeconds
* @return
*/
String getDisplayTimeString(long timeInSeconds) {
long hours = timeInSeconds / (60*60);
long mins = timeInSeconds / 60;
long secs = timeInSeconds % 60;
StringBuffer buf = new StringBuffer();
if(hours > 0) {
buf.append(hours).append(':');
}
if(mins < 10) buf.append('0');
buf.append(mins).append(':');
if(secs < 10) buf.append('0');
buf.append(secs);
return buf.toString();
}
public void commandAction(Command command, Displayable s) {
final String methodName = "14";
if (command == exitCmd) {
// back to main screen
cb.callback();
} else if (command == stopCmd) {
// stop playing
stopCurrentTrack();
setActiveCommand();
} else if (command == startCmd) {
// start playing
try {
if (currentTrack != null) {
startTrack(currentTrack.getFileLocation());
resetTime();
setActiveCommand();
} else {
log.error(methodName, "No current track selected!");
}
} catch (Exception e) {
log.error(methodName, "Unable to start track "
+ currentTrack.getFileLocation(), e);
}
}
}
public void event() {
this.displayCurrentTrack();
}
}
}