package org.limewire.ui.swing.player;
import java.awt.Component;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.media.ClockStoppedException;
import javax.media.Control;
import javax.media.Controller;
import javax.media.ControllerListener;
import javax.media.EndOfMediaEvent;
import javax.media.GainControl;
import javax.media.IncompatibleSourceException;
import javax.media.IncompatibleTimeBaseException;
import javax.media.Player;
import javax.media.StartEvent;
import javax.media.StopEvent;
import javax.media.Time;
import javax.media.TimeBase;
import javax.media.protocol.DataSource;
import net.sf.fmj.utility.URLUtils;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.player.api.AudioPlayer;
import org.limewire.player.api.AudioPlayerEvent;
import org.limewire.player.api.AudioPlayerListener;
import org.limewire.player.api.PlayerState;
import com.google.inject.Provider;
/**
* Wraps the LimeWirePlayer in a javax.media.Player. This allows us to reuse
* the LWPlayer using the FMJ Player.
*/
public class JavaSoundPlayer implements Player {
private static final Log LOG = LogFactory.getLog(JavaSoundPlayer.class);
private static final String AUDIO_LENGTH_BYTES = "audio.length.bytes";
private static final String AUDIO_TYPE = "audio.type";
private static final String MP3 = "mp3";
private static final String WAVE = "wave";
private final Provider<AudioPlayer> audioPlayerProvider;
private List<ControllerListener> listenerList = new CopyOnWriteArrayList<ControllerListener>();
private PlayerListener playerListener;
private volatile int state = Unrealized;
/** Map containing properties for the last opened song. */
private volatile Map audioProperties = null;
private volatile long bytesRead = 0;
private volatile Time duration = null;
/** Audio player component. */
private AudioPlayer audioPlayer;
public JavaSoundPlayer(Provider<AudioPlayer> audioPlayerProvider) {
this.audioPlayerProvider = audioPlayerProvider;
}
/**
* Returns the audio player component. When first called, this method
* creates the component and registers this mediator as a listener.
*/
private AudioPlayer getPlayer() {
if (audioPlayer == null) {
audioPlayer = audioPlayerProvider.get();
playerListener = new PlayerListener();
audioPlayer.addAudioPlayerListener(playerListener);
}
return audioPlayer;
}
@Override
public void setSource(DataSource source) throws IncompatibleSourceException {
if (!source.getLocator().getProtocol().equals("file")) {
LOG.debug("attempted to load non-file");
throw new IncompatibleSourceException("Only file URLs supported: " + source);
}
String path = URLUtils.extractValidPathFromFileUrl(source.getLocator().toExternalForm());
if(path == null) {
LOG.debug("could not locate path of file");
throw new IncompatibleSourceException("Cannot find file path");
}
File file = new File(path);
if(!isPlayable(file)) {
LOG.debug("unplayable file");
throw new IncompatibleSourceException("Unplayable file type");
}
try {
getPlayer().loadSong(file);
} catch(Exception e) {
if(LOG.isDebugEnabled())
LOG.debug("failed to load file " + e);
throw new IncompatibleSourceException("failed to load javasoundplayer");
}
bytesRead = 0;
}
@Override
public Component getControlPanelComponent() {
return null;
}
@Override
public GainControl getGainControl() {
if(audioPlayer != null)
return audioPlayer.getGainControl();
return null;
}
@Override
public Component getVisualComponent() {
return null;
}
private boolean isPlayable(File file) {
String name = file.getName().toLowerCase(Locale.US);
return name.endsWith(".mp3") || name.endsWith(".ogg") || name.endsWith(".wav");
}
@Override
public void start() {
state = Started;
PlayerState status = getPlayer().getStatus();
if (status != PlayerState.PAUSED) {
audioPlayer.stop();
audioPlayer.playSong();
} else {
audioPlayer.unpause();
}
}
@Override
public void close() {
stop();
}
@Override
public void deallocate() {
audioProperties = null;
if(playerListener != null && audioPlayer != null)
audioPlayer.removeAudioPlayerListener(playerListener);
audioPlayer = null;
}
@Override
public Control getControl(String forName) {
return null;
}
@Override
public Control[] getControls() {
if(audioPlayer != null)
return new Control[]{audioPlayer.getGainControl()};
else
return new Control[]{};
}
@Override
public Time getStartLatency() {
return new Time(0);
}
@Override
public int getState() {
return state;
}
@Override
public Time getMediaTime() {
if ((audioProperties != null) && audioProperties.containsKey(AUDIO_LENGTH_BYTES) && audioProperties.containsKey("duration")) {
return new Time(bytesRead);
}
return new Time(0);
}
@Override
public void setMediaTime(Time now) {
if(audioPlayer != null && audioProperties != null && audioProperties.containsKey(AUDIO_LENGTH_BYTES)) {
// currently, only mp3 and wav files can be seeked upon
if (isSeekable((String) audioProperties.get(AUDIO_TYPE))) {
float percent = now.getNanoseconds()/((float)getDuration().getNanoseconds());
final long skipBytes = Math.round((Integer) audioProperties.get(AUDIO_LENGTH_BYTES)* percent);
getPlayer().seekLocation(skipBytes);
}
}
}
private static boolean isSeekable(String songType) {
if (songType == null) {
return false;
}
return songType.equalsIgnoreCase(MP3) || songType.equalsIgnoreCase(WAVE);
}
public void pause() {
if(audioPlayer != null)
audioPlayer.pause();
}
@Override
public void stop() {
if(audioPlayer != null)
audioPlayer.stop();
}
@Override
public Time getDuration() {
if(duration == null)
return Time.TIME_UNKNOWN;
else
return duration;
}
@Override
public void addControllerListener(ControllerListener listener) {
listenerList.add(listener);
}
@Override
public void removeControllerListener(ControllerListener listener) {
listenerList.remove(listener);
}
@Override
public void removeController(Controller oldController) {}
@Override
public void addController(Controller newController) throws IncompatibleTimeBaseException {}
@Override
public int getTargetState() { return 0; }
@Override
public void prefetch() {}
@Override
public void realize() {}
@Override
public long getMediaNanoseconds() { return 0; }
@Override
public float getRate() { return 0; }
@Override
public Time getStopTime() { return null; }
@Override
public Time getSyncTime() { return null; }
@Override
public TimeBase getTimeBase() { return null; }
@Override
public Time mapToTimeBase(Time t) throws ClockStoppedException { return null; }
@Override
public float setRate(float factor) { return 0; }
@Override
public void setStopTime(Time stopTime) {}
@Override
public void setTimeBase(TimeBase master) throws IncompatibleTimeBaseException {}
@Override
public void syncStart(Time at) {}
private class PlayerListener implements AudioPlayerListener {
@Override
public void progressChange(int read) {
bytesRead = read;
}
@Override
public void songOpened(Map<String, Object> properties) {
audioProperties = properties;
if(audioProperties != null && audioProperties.containsKey(AUDIO_LENGTH_BYTES)) {
duration = new Time((Integer)audioProperties.get(AUDIO_LENGTH_BYTES));
} else {
duration = Time.TIME_UNKNOWN;
}
for(ControllerListener listener : listenerList) {
listener.controllerUpdate(new StartEvent(JavaSoundPlayer.this, 0, 0, 0, null, null));
}
}
@Override
public void stateChange(AudioPlayerEvent event) {
// Go to next song when finished.
if (event.getState() == PlayerState.EOM) {
state = Unrealized;
for(ControllerListener listener : listenerList) {
listener.controllerUpdate(new EndOfMediaEvent(JavaSoundPlayer.this, 0, 0, 0, null));
}
} else if(event.getState() == PlayerState.STOPPED || event.getState() == PlayerState.PAUSED) {
state = Realized;
for(ControllerListener listener : listenerList) {
listener.controllerUpdate(new StopEvent(JavaSoundPlayer.this, 0, 0, 0, null));
}
} else if(event.getState() == PlayerState.PLAYING) {
state = Started;
for(ControllerListener listener : listenerList) {
listener.controllerUpdate(new StartEvent(JavaSoundPlayer.this, 0, 0, 0, null, null));
}
} else {
state = Realized;
}
}
}
}