package com.limegroup.gnutella.gui.mp3;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import javax.swing.Box;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.gui.BoxPanel;
import com.limegroup.gnutella.gui.GUIConstants;
import com.limegroup.gnutella.gui.GUIMediator;
import com.limegroup.gnutella.gui.MediaButton;
import com.limegroup.gnutella.gui.RefreshListener;
import com.limegroup.gnutella.gui.playlist.PlaylistMediator;
import com.limegroup.gnutella.gui.themes.ThemeMediator;
import com.limegroup.gnutella.gui.themes.ThemeObserver;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.ProcessingQueue;
/** This class sets up JPanel with MediaPlayer on it, and takes care of
* GUI MediaPlayer events.
*/
public final class MediaPlayerComponent
implements AudioPlayerListener, RefreshListener, ThemeObserver {
private static final Log LOG = LogFactory.getLog(MediaPlayerComponent.class);
/**
* The sole instance.
*/
private static MediaPlayerComponent INSTANCE = null;
/**
* The MP3 player.
*/
private final AudioPlayer PLAYER;
/**
* The ProgressBar for showing the name & play progress.
*/
private /* final */ JProgressBar PROGRESS;
private Dimension _progressBarDimension = new Dimension(110,20);
/**
* The ProcessingQueue that plays songs.
*
* A processingQueue is used so that we don't keep the thread
* around when we're not handling songs.
*/
private static final ProcessingQueue SONG_QUEUE =
new ProcessingQueue("SongProcessor");
/**
* The currently playing song.
*/
private volatile File myCurrentPlayingFile = null;
/**
* The next song that we want to play.
*/
private volatile File nextSongToPlay = null;
/**
* Whether or not the user pressed 'stop' last.
*/
private volatile static boolean stopWasLast = false;
/**
* Index of where to display the name in the progress bar.
*/
private volatile int currBeginIndex = -1;
/**
* The maximum characters to show in the progress bar.
*/
private static final int STRING_SIZE_TO_SHOW = 24;
/**
* Constant for the play button.
*/
private static final MediaButton PLAY_BUTTON =
new MediaButton("MEDIA_PLAY_BUTTON_TIP",
"play_small_up", "play_small_dn");
/**
* Constant for the pause button.
*/
private static final MediaButton PAUSE_BUTTON =
new MediaButton("MEDIA_PAUSE_BUTTON_TIP",
"pause_small_up", "pause_small_dn");
/** Constant for the stop button.
*/
private static final MediaButton STOP_BUTTON =
new MediaButton("MEDIA_STOP_BUTTON_TIP",
"stop_small_up", "stop_small_dn");
/** Constant for the forward button.
*/
private static final MediaButton FORWARD_BUTTON =
new MediaButton("MEDIA_FORWARD_BUTTON_TIP",
"forward_small_up", "forward_small_dn");
/** Constant for the rewind button.
*/
private static final MediaButton REWIND_BUTTON =
new MediaButton("MEDIA_REWIND_BUTTON_TIP",
"rewind_small_up", "rewind_small_dn");
/**
* The lazily constructed media panel.
*/
private JPanel myMediaPanel = null;
/**
* Variable for the name of the current file being played.
*/
private String currentFileName;
/**
* Lock for access to the above String.
*/
private final Object cfnLock = new Object();
/**
* Gets the sole instance.
*/
public static MediaPlayerComponent instance() {
INSTANCE = new MediaPlayerComponent();
return INSTANCE;
}
/**
* Constructs a new <tt>MediaPlayerComponent</tt>.
*/
private MediaPlayerComponent() {
PLAYER = new BasicPlayer();
PLAYER.addAudioPlayerListener(this);
GUIMediator.addRefreshListener(this);
ThemeMediator.addThemeObserver(this);
}
// inherit doc comment
public void updateTheme() {
PLAY_BUTTON.updateTheme();
PAUSE_BUTTON.updateTheme();
STOP_BUTTON.updateTheme();
FORWARD_BUTTON.updateTheme();
REWIND_BUTTON.updateTheme();
PROGRESS.setString(GUIMediator.getStringResource("MEDIA_PLAYER_DEFAULT_STRING"));
}
/**
* Listens for the play button being pressed.
*/
private class PlayListener implements ActionListener {
public void actionPerformed(ActionEvent ae) {
stopWasLast = false;
switch(PLAYER.getStatus()) {
case AudioPlayer.STATUS_PAUSED:
unpause();
break;
case AudioPlayer.STATUS_STOPPED:
setNextSong(GUIMediator.getPlayList().getFileToPlay());
break;
}
}
}
/**
* Listens for the stopped button being pressed.
*/
private class StopListener implements ActionListener {
public void actionPerformed(ActionEvent ae) {
stopWasLast = true;
stopSong();
}
}
/**
* Listens for the next button being pressed.
*/
private class NextListener implements ActionListener {
public void actionPerformed(ActionEvent ae) {
if (PLAYER.getStatus() != AudioPlayer.STATUS_STOPPED) {
// must unpause first if paused.
if (PLAYER.getStatus() == AudioPlayer.STATUS_PAUSED)
PLAYER.unpause();
stopSong(); // will automatically go to the next song.
}
}
}
/**
* Listens for the back button being pressed.
*/
private class BackListener implements ActionListener {
public void actionPerformed(ActionEvent ae) {
if (PLAYER.getStatus() != AudioPlayer.STATUS_STOPPED) {
GUIMediator.getPlayList().setBackwardsMode();
if (PLAYER.getStatus() == AudioPlayer.STATUS_PAUSED)
PLAYER.unpause();
stopSong();
}
}
}
/**
* Listens for the pause button being pressed.
*/
private class PauseListener implements ActionListener {
public void actionPerformed(ActionEvent ae) {
pauseSong();
}
}
/**
* Runnable that waits a little bit before playing songs, to give the
* user time to click 'next' a bunch.
*/
private class SongBuffer implements Runnable {
public void run() {
if(getNextSong() == null) {
LOG.trace("no next song, leaving.");
return;
}
// if something's already playing, sleep a bit until someone
// stops it -- hopefully shouldn't take too long.
while(PLAYER.getStatus() != AudioPlayer.STATUS_STOPPED) {
LOG.trace("player isn't stopped, sleeping.");
try {
Thread.sleep(100);
} catch(InterruptedException ignored) {}
}
// if we're already playing the song, nothing to do.
if(isPlaying(getNextSong())) {
LOG.trace("already playing requested song, leaving.");
return;
}
// if we're already playing something, wait until it finishes...
while(myCurrentPlayingFile != null) {
LOG.trace("player is still completing something, sleeping...");
try {
Thread.sleep(100);
} catch(InterruptedException ignored) {}
}
// get the latest file
File playFile = null;
// While the song the user wants is changing,
// sleep a bit, so we don't play the first second
// of each.
while(playFile != getNextSong()) {
if(LOG.isDebugEnabled())
LOG.debug("new song, setting as: " + getNextSong() +
" and waiting for changes...");
playFile = getNextSong();
try {
Thread.sleep(100);
} catch(InterruptedException ignored) {}
}
// nothing to play?
if(playFile == null) {
LOG.debug("song selection cancelled, leaving.");
return;
}
if(LOG.isTraceEnabled())
LOG.trace("starting song: " + playFile);
myCurrentPlayingFile = playFile;
setNextSong(null);
try {
PlaylistMediator playlist = GUIMediator.getPlayList();
if (playlist != null) {
if(!PLAYER.play(playFile)) {
myCurrentPlayingFile = null;
return;
}
GUIMediator.getPlayList().playStarted();
}
} catch (IOException ioe) {
myCurrentPlayingFile = null;
ErrorService.error(ioe);
}
}
}
/**
* Constructs the media panel.
*/
private JPanel constructMediaPanel() {
int tempWidth = 0, tempHeight = 0;
tempHeight += PLAY_BUTTON.getIcon().getIconHeight() + 2;
tempWidth += PLAY_BUTTON.getIcon().getIconWidth() + 2
+ PAUSE_BUTTON.getIcon().getIconWidth() + 2
+ STOP_BUTTON.getIcon().getIconWidth() + 2
+ FORWARD_BUTTON.getIcon().getIconWidth() + 2
+ REWIND_BUTTON.getIcon().getIconWidth() + 2;
// create sliders
PROGRESS = new SongProgressBar();
PROGRESS.setMaximumSize(_progressBarDimension);
PROGRESS.setPreferredSize(_progressBarDimension);
PROGRESS.setString(GUIMediator.getStringResource("MEDIA_PLAYER_DEFAULT_STRING"));
// setup buttons
PLAY_BUTTON.addActionListener(new PlayListener());
PAUSE_BUTTON.addActionListener(new PauseListener());
STOP_BUTTON.addActionListener(new StopListener());
FORWARD_BUTTON.addActionListener(new NextListener());
REWIND_BUTTON.addActionListener(new BackListener());
// setup sliders
updatePBValue(0);
// add everything
JPanel buttonPanel = new BoxPanel(BoxPanel.X_AXIS);
buttonPanel.setMaximumSize(new Dimension(tempWidth, tempHeight));
buttonPanel.setMinimumSize(new Dimension(tempWidth, tempHeight));
buttonPanel.add(Box.createHorizontalGlue());
buttonPanel.add(REWIND_BUTTON);
buttonPanel.add(PLAY_BUTTON);
buttonPanel.add(PAUSE_BUTTON);
buttonPanel.add(STOP_BUTTON);
buttonPanel.add(FORWARD_BUTTON);
buttonPanel.add(Box.createHorizontalStrut(GUIConstants.SEPARATOR));
buttonPanel.add(PROGRESS);
if (CommonUtils.isMacOSX())
buttonPanel.add(Box.createHorizontalStrut(16));
buttonPanel.add(Box.createHorizontalGlue());
return buttonPanel;
}
/**
* Gets the media panel, constructing it if necessary.
*/
public JPanel getMediaPanel() {
if (myMediaPanel == null)
myMediaPanel = constructMediaPanel();
return myMediaPanel;
}
/**
* Plays the file immediately, marking the player as 'playing'.
*/
public static void playSongImmediately(File f) {
if(INSTANCE == null || f == null)
return;
MediaPlayerComponent.stopWasLast = false;
// only do stuff if we're not already playing it.
if (!MediaPlayerComponent.isPlaying(f))
INSTANCE.setNextSong(f);
}
/**
* Launches the specified song.
*/
public static void launchAudio(File toPlay) {
if (INSTANCE == null || toPlay == null)
return;
// already playing this audio file?
if (MediaPlayerComponent.isPlaying(toPlay))
return;
switch(INSTANCE.PLAYER.getStatus()) {
case AudioPlayer.STATUS_STOPPED:
INSTANCE.setNextSong(toPlay);
break;
case AudioPlayer.STATUS_PLAYING:
if(GUIMediator.getPlayList().isSongPlaying())
GUIMediator.getPlayList().playSongNext(toPlay);
else
INSTANCE.setNextSong(toPlay);
break;
case AudioPlayer.STATUS_PAUSED:
INSTANCE.setNextSong(toPlay);
break;
}
}
/**
* Determines if the given file is playing right now.
*/
public static boolean isPlaying(File f) {
return f != null && f.equals(INSTANCE.myCurrentPlayingFile);
}
/**
* Pauses the currently playing audio file.
*/
private void pauseSong() {
if (PLAYER.getStatus() == AudioPlayer.STATUS_PAUSED) {
if (PLAYER.getFrameSeek() != 0)
PLAYER.stop();
else
PLAYER.unpause();
} else {
PLAYER.pause();
}
}
/**
* Sets the next song to be played.
*/
private void setNextSong(File f) {
if(LOG.isDebugEnabled())
LOG.debug("setting next song to be: " + f);
nextSongToPlay = f;
stopSong();
if(f != null)
SONG_QUEUE.add(new SongBuffer());
}
/**
* Gets the next song to be played.
*/
private File getNextSong() {
return nextSongToPlay;
}
/**
* Stops the currently playing audio file.
*/
private void stopSong() {
if(PLAYER.getStatus() != AudioPlayer.STATUS_STOPPED)
PLAYER.stop();
}
/**
* Unpauses the song, if it was paused.
*/
private void unpause() {
if (PLAYER.getStatus() == AudioPlayer.STATUS_PAUSED) {
// player was paused, play from current audio file location
if (PLAYER.getFrameSeek() == 0)
PLAYER.unpause();
}
}
/**
* Notification that a song has finished playing.
* If play is continuous & the user didn't manually stop,
* queues up the next song to play.
*
* Implements one method of BasicPlayerListener Interface.
*/
public void playComplete() {
if(LOG.isDebugEnabled())
LOG.debug("play completed for: " + myCurrentPlayingFile);
myCurrentPlayingFile = null;
// complete progress bar
updatePBValue(PROGRESS.getMaximum());
updatePBString("");
PlaylistMediator playlist = GUIMediator.getPlayList();
if (playlist == null)
return;
// inform the GUI on whether or not we're going to continue playing.
if (stopWasLast || !playlist.isContinuous())
playlist.playComplete(true);
else {
playlist.playComplete(false);
// if we don't already have another song to play,
// get one.
if (getNextSong() == null)
setNextSong(playlist.getFileToPlay());
}
}
/**
* Notification of the number of frames that will be played in this song.
* Updates the progress bar with the appropriate values.
*
* - called before playing a audio file
* - gets the size of the audio file in frames
* - sets the maximum slider value
* Implements one method of AudioPlayerListener Interface.
*
*/
public void setUpSeek(int lengthInFrames) {
// set slider max to audio file length
updatePBValue(0);
updatePBMaximum(lengthInFrames);
// others may need access to this badboy....
synchronized (cfnLock) {
currentFileName = myCurrentPlayingFile.getName();
if (currentFileName.length() > STRING_SIZE_TO_SHOW) {
currentFileName = currentFileName + " *** " +
currentFileName + " *** ";
}
updatePBString(currentFileName);
currBeginIndex = -5;
}
}
/**
* Updates the progress bar to have the correct progress & name.
*/
public void updateAudioPosition(int value) {
updatePBValue(value);
synchronized (cfnLock) {
if (currentFileName == null) return;
if (currentFileName.length() <= STRING_SIZE_TO_SHOW) return;
Assert.that(currentFileName.length() > (STRING_SIZE_TO_SHOW * 2));
currBeginIndex = currBeginIndex + 5;
if ((currBeginIndex + STRING_SIZE_TO_SHOW) >=
currentFileName.length()) {
currBeginIndex = currBeginIndex - (currentFileName.length()/2);
}
updatePBString(currentFileName.substring(
currBeginIndex, currBeginIndex + STRING_SIZE_TO_SHOW));
}
}
/**
* Updates the audio player.
*/
public void refresh() {
PLAYER.refresh();
}
/**
* Updates the maximum value of the progress bar, on the Swing thread.
*/
private void updatePBMaximum(final int update) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
PROGRESS.setMaximum(update);
}
});
}
/**
* Updates the displayed value of the progress bar, on the Swing thread.
*/
private void updatePBString(final String update) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
PROGRESS.setString(update);
}
});
}
/**
* Updates the current progress of the progress bar, on the Swing thread.
*/
private void updatePBValue(final int update) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
PROGRESS.setValue(update);
} catch(ClassCastException ignored) {
//see: http://bugs.limewire.com:8080/bugs/searching.jsp?disp1=l&disp2=c&disp3=o&disp4=j&l=152&c=188&m=315_1102
//who knows why it happens, but who cares about it.
}
}
});
}
}