package org.limewire.ui.swing.player;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import javax.media.ClockStoppedException;
import javax.media.Control;
import javax.media.Controller;
import javax.media.ControllerEvent;
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 javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;
import net.sf.fmj.concurrent.ExecutorServiceManager;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.concurrent.ThreadPoolListeningExecutor;
import org.limewire.core.api.Category;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.player.api.PlayerState;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.NativeLaunchUtils;
import org.limewire.ui.swing.util.SwingUtils;
import ca.odell.glazedlists.EventList;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.lti.utils.OSUtils;
@Singleton
class PlayerMediatorImpl implements PlayerMediator {
private static final Log LOG = LogFactory.getLog(PlayerMediatorImpl.class);
private final VideoDisplayDirector displayDirector;
private final List<PlayerMediatorListener> listenerList;
private final CategoryManager categoryManager;
private final PlayerInitializer playerInitializer;
private final ControllerListener controllerListener;
private final Provider<MediaPlayerFactoryImpl> mediaPlayerFactory;
private final Playlist playlist;
/** A static Player to avoid null checks when no player currently is in use. */
private static final Player EMPTY_PLAYER = new EmptyPlayer();
/** The current instance of the Player that is being used. This will change on a file per file basis.
* This Player should never be null, if no Player exists currently it should be an instanceOf EMPTY_PLAYER.
*/
private Player player;
/** The File that is currently playing. If no File has been set, this is null. */
private File currentMediaFile;
/** If the Player is active, this will return a valid Timer, otherwise return null. */
private volatile Timer updateTimer;
/** Returns true if this Player is currently seeking, false otherwise.*/
private boolean isSeeking;
private long playingWindowStartTime = -1;
private int playingSwitches = -1;
@SuppressWarnings("unused")
private volatile boolean videoPlayedInSessionByLW = false;
@SuppressWarnings("unused")
private volatile boolean audioPlayedInSession = false;
@Inject
PlayerMediatorImpl(VideoDisplayDirector displayDirector, CategoryManager categoryManager,
Provider<MediaPlayerFactoryImpl> mediaPlayerFactory) {
this.displayDirector = displayDirector;
this.categoryManager = categoryManager;
this.mediaPlayerFactory = mediaPlayerFactory;
this.listenerList = new ArrayList<PlayerMediatorListener>();
this.playerInitializer = new PlayerInitializer();
this.controllerListener = new VideoControllerListener();
this.playlist = new Playlist();
this.player = EMPTY_PLAYER;
//NOTE: used in Handler to catch runtime exceptions and report them, don't delete this
ThreadPoolListeningExecutor tpe = new ThreadPoolListeningExecutor(1, 1,
5L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
ExecutorsHelper.daemonThreadFactory("Video ThreadPool"));
ExecutorServiceManager.setExecutorService(ExecutorsHelper.unconfigurableExecutorService(tpe));
}
@Inject
void register(){
// if playing audio/video files within LW is disabled, stop any file
// that may be playing currently.
SwingUiSettings.PLAYER_ENABLED.addSettingListener(new SettingListener(){
@Override
public void settingChanged(final SettingEvent evt) {
SwingUtilities.invokeLater(new Runnable(){
public void run() {
if (!SwingUiSettings.PLAYER_ENABLED.getValue()) {
stop();
}
}
});
}
});
}
@Override
public void addMediatorListener(PlayerMediatorListener listener) {
listenerList.add(listener);
}
@Override
public void removeMediatorListener(PlayerMediatorListener listener) {
listenerList.remove(listener);
}
@Override
public File getCurrentMediaFile() {
return currentMediaFile;
}
@Override
public PlayerState getStatus() {
return convertControllerState(player.getState());
}
private PlayerState convertControllerState(int controllerState) {
if(isSeeking){
return PlayerState.SEEKING;
}
switch (controllerState) {
case Controller.Started:
return PlayerState.PLAYING;
case Controller.Realized:
return PlayerState.PAUSED;
default:
return PlayerState.UNKNOWN;
}
}
@Override
public void setActivePlaylist(EventList<LocalFileItem> fileList) {
playlist.setActivePlaylist(fileList);
}
@Override
public boolean isPaused(File file) {
return player.getState() != Controller.Started && file.equals(currentMediaFile);
}
@Override
public boolean isPlaying(File file) {
return player.getState() == Controller.Started && file.equals(currentMediaFile);
}
@Override
public boolean isSeekable() {
return isDurationMeasurable();
}
@Override
public boolean isShuffle() {
return playlist.isShuffle();
}
@Override
public void pause() {
if(player instanceof JavaSoundPlayer)
((JavaSoundPlayer)player).pause();
else
player.stop();
}
@Override
public void play(LocalFileItem localFileItem) {
playlist.setCurrentItem(localFileItem);
play(localFileItem.getFile(), true);
}
@Override
public void playOrLaunchNatively(File file) {
playlist.setCurrentItem(null);
playlist.setActivePlaylist(null);
play(file, true);
}
/**
* Handles the actual launching of the media file.
* @param isLaunchOnFailure if true will launch the file natively if it fails to
* open within LW. If false and an audio file, will attempt to play the next
* file within a playlist if one exists.
*/
private void play(File file, boolean isLaunchOnFailure) {
Category category = categoryManager.getCategoryForFile(file);
if (SwingUiSettings.PLAYER_ENABLED.getValue() && (OSUtils.isWindows() || OSUtils.isMacOSX()) &&
(category == Category.AUDIO || category == Category.VIDEO)) {
currentMediaFile = file;
initializePlayerOrNativeLaunch(file, null, false, true, isLaunchOnFailure);
} else {
currentMediaFile = null;
NativeLaunchUtils.safeLaunchFile(file, categoryManager);
}
}
/**
* Initializes an FMJ player for the video if possible, launches natively if
* not.
*
* @param file the video file to be played
* @param time the starting time of the video. null to start at the
* beginning.
* @param autoPlay whether or not to start playback
* @return true if the player is successfully initialized, false if it is
* not initialized and the file is natively launched
*/
private void initializePlayerOrNativeLaunch(final File file, Time time, boolean isFullScreen, boolean autoStart, boolean isLaunchOnFailure) {
GuiUtils.getMainFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
if(playerInitializer.isInitializing()) {
playerInitializer.cancel();
}
killPlayer();
playerInitializer.initialize(file, time, isFullScreen, autoStart, isLaunchOnFailure);
}
@Override
public void nextSong() {
stop();
LocalFileItem fileItem = playlist.getNextFileItem();
if (fileItem != null) {
play(fileItem.getFile(), false);
}
}
@Override
public void prevSong() {
stop();
LocalFileItem fileItem = playlist.getPrevFileItem();
if (fileItem != null) {
play(fileItem.getFile(), false);
}
}
@Override
public void resume() {
if (player.getState() != Controller.Started) {
player.start();
}
}
@Override
public void setShuffle(boolean shuffle) {
playlist.setShuffle(shuffle);
}
@Override
public void setVolume(double value) {
if ( player.getState() != Controller.Unrealized && player.getState() != Controller.Realizing &&
player.getGainControl() != null) {
player.getGainControl().setLevel((float) value);
}
}
private boolean isAudioFile(File file) {
return categoryManager.getCategoryForFile(file) == Category.AUDIO;
}
@Override
public boolean hasVolumeControl() {
// On Mac OS X we're using true full screen mode, and you can't use popups in true full screen mode.
// So, since the volume popup won't work, let's disable it.
if (OSUtils.isMacOSX() && isFullScreen())
return false;
int state = player.getState();
if(state == Controller.Unrealized || state == Controller.Realizing)
return false;
return player.getGainControl() != null;
}
@Override
public void seek(double percent) {
isSeeking = true;
player.setMediaTime(new Time(percent * player.getDuration().getSeconds()));
isSeeking = false;
}
@Override
public void stop() {
if(playerInitializer.isInitializing()){
playerInitializer.cancel();
}
displayDirector.close();
killTimer();
killPlayer();
currentMediaFile = null;
}
/**
* Notifies the listeners of the change in state of the player.
*/
private void firePlayerStateChanged(PlayerState state) {
for (PlayerMediatorListener listener : listenerList) {
listener.stateChanged(state);
}
}
/**
* Notifies the listeners of the current progress on the currently playing
* media file.
*/
private void fireProgressUpdated() {
fireProgressUpdated((float) (player.getMediaTime().getSeconds() / player.getDuration().getSeconds()));
}
/**
* Notifies the listeners of the current progress on the currently playing
* media file.
*
* @param progress - the current progress [0.0f,100.0f]
*/
private void fireProgressUpdated(float progress) {
for (PlayerMediatorListener listener : listenerList) {
listener.progressUpdated(progress);
}
}
/**
* Notifies the listeners when the media file that is currently playing changes.
*/
private void fireMediaChanged(String name) {
for (PlayerMediatorListener listener : listenerList) {
listener.mediaChanged(name);
}
}
private void killTimer() {
if (updateTimer != null) {
if (updateTimer.isRunning()) {
updateTimer.stop();
}
updateTimer = null;
}
}
/**
* Stops the Player and cleans up any resources used by the Player.
* Player is reset to EMPTY_PLAYER.
*/
private void killPlayer() {
player.stop();
player.removeControllerListener(controllerListener);
player.close();
player.deallocate();
player = EMPTY_PLAYER;
}
public void setFullScreen(boolean isFullScreen) {
if(player == EMPTY_PLAYER) {
return;
}
if(isAudioFile(currentMediaFile))
return;
if (displayDirector.isFullScreen() == isFullScreen) {
return;
}
//task is already running. user probably hit crtl-f twice quickly.
if (playerInitializer.isInitializing()){
return;
}
boolean isPlaying = player.getState() == Controller.Started;
Time time = isDurationMeasurable() ? player.getMediaTime() : null;
killTimer();
killPlayer();
initializePlayerOrNativeLaunch(currentMediaFile, time, isFullScreen, isPlaying, false);
}
@Override
public boolean isPlayable(File file) {
if(!SwingUiSettings.PLAYER_ENABLED.getValue())
return false;
Category category = categoryManager.getCategoryForFile(file);
return category == Category.AUDIO || category == Category.VIDEO;
}
public boolean isFullScreen() {
return displayDirector.isFullScreen();
}
private boolean isDurationMeasurable() {
if(player == EMPTY_PLAYER) {
return false;
}
long time = player.getDuration().getNanoseconds();
return Player.DURATION_UNBOUNDED.getNanoseconds() != time && Player.DURATION_UNKNOWN.getNanoseconds() != time && Time.TIME_UNKNOWN.getNanoseconds() != time;
}
private class VideoControllerListener implements ControllerListener {
@Override
public void controllerUpdate(final ControllerEvent controllerEvent) {
SwingUtils.invokeNowOrLater(new Runnable() {
@Override
public void run() {
if (controllerEvent instanceof EndOfMediaEvent) {
setEndOfMedia();
} else if (controllerEvent instanceof StartEvent || controllerEvent.getSourceController().getState() == Controller.Started) {
firePlayerStateChanged(PlayerState.OPENED);
firePlayerStateChanged(PlayerState.PLAYING);
if (updateTimer == null) {
updateTimer = new Timer(100, new TimerAction());
}
if (!updateTimer.isRunning()) {
updateTimer.start();
}
} else if (controllerEvent instanceof StopEvent) {
firePlayerStateChanged(PlayerState.STOPPED);
if (updateTimer != null) {
updateTimer.stop();
}
}
}
});
}
}
private class TimerAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (!isDurationMeasurable()) {
if(updateTimer != null)
updateTimer.stop();
return;
}
if (player.getMediaTime().getSeconds() >= player.getDuration().getSeconds()) {
// some FMJ players don't seem to fire EndOfMediaEvents so we need to do
// this manually
setEndOfMedia();
} else {
fireProgressUpdated();
}
}
}
private void setEndOfMedia() {
player.stop();
player.setMediaTime(new Time(0));
if(updateTimer != null)
updateTimer.stop();
firePlayerStateChanged(PlayerState.EOM);
fireProgressUpdated(100f);
// Sanity check before switching to the next song,
// the last 10 songs that switched must have taken over
// 5 seconds to play.
playingSwitches = (playingSwitches+1);
if (playingSwitches % 10 == 0) {
if(playingSwitches == 0) {
playingWindowStartTime = System.currentTimeMillis();
nextSong();
} else {
long currentTime = System.currentTimeMillis();
if(currentTime - playingWindowStartTime < 5000) {
playingSwitches = -1;
playingWindowStartTime = -1;
} else {
playingSwitches = -1;
playingWindowStartTime = currentTime;
nextSong();
}
}
} else {
nextSong();
}
}
interface PlayerCompleteCallback {
public void complete(Player newPlayer, Time time, boolean autoStart);
}
/**
* Asynchronously initializes new media players.
*/
private class PlayerInitializer {
private PlayerInitalizationWorker initializationWorker;
public PlayerInitializer(){}
public boolean isInitializing() {
return initializationWorker != null;
}
public void initialize(final File file, Time time, boolean isFullScreen, boolean autoStart, boolean isLaunchOnFailure) {
if (isInitializing()) {
cancel();
}
if(LOG.isDebugEnabled())
LOG.debug("initializing player for: " + file);
initializationWorker = new PlayerInitalizationWorker(file, time, isFullScreen, autoStart, isLaunchOnFailure, new PlayerCompleteCallback() {
@Override
public void complete(Player newPlayer, Time time, boolean autoStart) {
assert(SwingUtilities.isEventDispatchThread());
if(newPlayer == null) {
//New player creation failed. The video was launched natively.
displayDirector.close();
return;
}
if (time != null) {
newPlayer.setMediaTime(time);
}
newPlayer.addControllerListener(controllerListener);
player = newPlayer;
if(!isAudioFile(file))
displayDirector.show();
fireMediaChanged(file.getName());
startMediaPlayer(autoStart);
initializationWorker = null;
if(LOG.isDebugEnabled())
LOG.debug("player initialized");
}
});
initializationWorker.execute();
}
public void cancel() {
initializationWorker.cancelInitialization();
initializationWorker = null;
}
private void startMediaPlayer(final boolean autoStart) {
ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(player != EMPTY_PLAYER) {
// start the player regardless of startVideo so that
// control panel is correctly updated
player.start();
// force fire these immediately to make sure
// everything updates
firePlayerStateChanged(PlayerState.PLAYING);
if (isDurationMeasurable()) {
fireProgressUpdated();
}
if (!autoStart) {
player.stop();
}
}
}
};
// Prevents the flash of native video window by delaying
// video playback so that the video can be fully embedded first.
Timer timer = new Timer(100, listener);
timer.setRepeats(false);
timer.start();
}
/**
* SwingWorker that initializes the new player off of the EDT.
*/
private class PlayerInitalizationWorker extends SwingWorker<Player, Void> {
private final File mediaFile;
private final Time time;
private final boolean autoStart;
private final boolean isLaunchOnFailure;
private final PlayerCompleteCallback callback;
private final Container renderPanel;
private volatile boolean canceled = false;
public PlayerInitalizationWorker(File mediaFile, Time time, boolean isFullScreen,
boolean autoStart, boolean isLaunchOnFailure, PlayerCompleteCallback callback) {
this.mediaFile = mediaFile;
this.time = time;
this.autoStart = autoStart;
this.isLaunchOnFailure = isLaunchOnFailure;
this.callback = callback;
this.renderPanel = new JPanel(new BorderLayout());
if(!isAudioFile(mediaFile))
displayDirector.initialize(renderPanel, isFullScreen);
}
/**
* Cancels the player initialization. We don't want to interrupt the
* thread by using cancel(boolean) because we need to properly
* dispose of the player.
*/
public void cancelInitialization() {
canceled = true;
}
@Override
protected Player doInBackground() throws Exception {
try {
Player player = mediaPlayerFactory.get().createMediaPlayer(mediaFile, renderPanel);
if(categoryManager.getCategoryForFile(mediaFile) == Category.AUDIO)
audioPlayedInSession = true;
else
videoPlayedInSessionByLW = true;
return player;
} catch (IncompatibleSourceException e) {
if(LOG.isDebugEnabled())
LOG.debug("failed to obtain a player " + e);
if(isLaunchOnFailure && !canceled) {
NativeLaunchUtils.safeLaunchFile(mediaFile, categoryManager);
} else if(categoryManager.getCategoryForFile(mediaFile) == Category.AUDIO) {
SwingUtilities.invokeLater(new Runnable(){
public void run() {
if(!canceled)
nextSong();
}
});
} else {
if(!canceled)
NativeLaunchUtils.safeLaunchFile(mediaFile, categoryManager);
}
return null;
}
}
@Override
protected void done() {
GuiUtils.getMainFrame().setCursor(Cursor.getDefaultCursor());
Player player = null;
try {
player = get();
} catch (InterruptedException e) {
// we're already finished so this can't happen
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
if (canceled || player == null) {
if (player != null) {
player.close();
player.deallocate();
}
return;
}
// if no renderer has been added yet or its OSX and a video file, add the renderer
// these are weird work arounds for the DS player on Windows 7 and audio playback on OSX
if(renderPanel.getComponentCount() == 0 &&
(!OSUtils.isMacOSX() || OSUtils.isMacOSX() && categoryManager.getCategoryForFile(mediaFile) == Category.VIDEO))
renderPanel.add(player.getVisualComponent());
callback.complete(player, time, autoStart);
}
}
}
/**
* Wrapper for Player to avoid null checks when the Player has not been initialized.
*/
static class EmptyPlayer implements Player {
@Override public void addController(Controller newController) throws IncompatibleTimeBaseException {}
@Override public Component getControlPanelComponent() {return null;}
@Override public GainControl getGainControl() {return null;}
@Override public Component getVisualComponent() {return null;}
@Override public void removeController(Controller oldController) {}
@Override public void start() {}
@Override public void setSource(DataSource source) throws IOException, IncompatibleSourceException {}
@Override public void addControllerListener(ControllerListener listener) {}
@Override public void close() {}
@Override public void deallocate() {}
@Override public Control getControl(String forName) {return null;}
@Override public Control[] getControls() {return new Control[0];}
@Override public Time getStartLatency() {return new Time(0);}
@Override public int getState() {return Controller.Unrealized;}
@Override public int getTargetState() {return 0;}
@Override public void prefetch() {}
@Override public void realize() {}
@Override public void removeControllerListener(ControllerListener listener) {}
@Override public long getMediaNanoseconds() {return 0;}
@Override public Time getMediaTime() {return new Time(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 void setMediaTime(Time now) {}
@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 stop() {}
@Override public void syncStart(Time at) {}
@Override public Time getDuration() {return new Time(1);}
}
}