package org.limewire.ui.swing.player;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.limewire.core.api.Category;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.inspection.Inspectable;
import org.limewire.inspection.InspectionHistogram;
import org.limewire.inspection.InspectionPoint;
import org.limewire.player.api.AudioPlayer;
import org.limewire.player.api.AudioPlayerEvent;
import org.limewire.player.api.AudioPlayerListener;
import org.limewire.player.api.AudioSource;
import org.limewire.player.api.PlayerState;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.library.LibraryPanel;
import org.limewire.ui.swing.library.navigator.LibraryNavItem;
import org.limewire.ui.swing.library.navigator.LibraryNavItem.NavType;
import org.limewire.ui.swing.settings.MediaPlayerSettings;
import org.limewire.ui.swing.util.I18n;
import ca.odell.glazedlists.EventList;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
/**
* Mediator that controls the interaction between the player view, the current
* playlist, and the audio player.
*/
@Singleton
public class PlayerMediator {
public static final String AUDIO_LENGTH_BYTES = "audio.length.bytes";
public 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 final LibraryMediator libraryMediator;
private final List<PlayerMediatorListener> listenerList;
/** Current list of songs. */
private final List<LocalFileItem> playList;
/** Randomized list of songs to be played. */
private final List<LocalFileItem> shuffleList;
/** Audio player component. */
private AudioPlayer audioPlayer;
/** Identifier for current playlist. */
private PlaylistId playlistId;
/** File item for the last opened song. */
private LocalFileItem fileItem = null;
/** Map containing properties for the last opened song. */
private Map audioProperties = null;
/** Progress of current song from 0.0 to 1.0. */
private float progress;
/** Indicator for shuffle mode. */
private boolean shuffle = false;
@InspectionPoint(value = "media-player")
private final PlayerInspector inspectable;
/**
* Constructs a PlayerMediator using the specified services.
*/
@Inject
public PlayerMediator(Provider<AudioPlayer> audioPlayerProvider,
LibraryMediator libraryMediator) {
this.audioPlayerProvider = audioPlayerProvider;
this.libraryMediator = libraryMediator;
this.listenerList = new ArrayList<PlayerMediatorListener>();
this.playList = new ArrayList<LocalFileItem>();
this.shuffleList = new ArrayList<LocalFileItem>();
this.inspectable = new PlayerInspector();
}
/**
* 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();
audioPlayer.addAudioPlayerListener(new PlayerListener());
}
return audioPlayer;
}
/**
* Adds the specified listener to the list that is notified about
* mediator events.
*/
public void addMediatorListener(PlayerMediatorListener listener) {
listenerList.add(listener);
}
/**
* Removes the specified listener from the list that is notified about
* mediator events.
*/
public void removeMediatorListener(PlayerMediatorListener listener) {
listenerList.remove(listener);
}
/**
* Notifies registered listeners that the progress is updated to the
* specified value.
*/
private void fireProgressUpdated(float progress) {
for (int i = 0, size = listenerList.size(); i < size; i++) {
listenerList.get(i).progressUpdated(progress);
}
}
/**
* Notifies registered listeners that the song is changed to the specified
* song name.
*/
private void fireSongChanged(String name) {
for (int i = 0, size = listenerList.size(); i < size; i++) {
listenerList.get(i).songChanged(name);
}
}
/**
* Notifies registered listeners that the player state is changed to the
* specified state.
*/
private void fireStateChanged(PlayerState state) {
for (int i = 0, size = listenerList.size(); i < size; i++) {
listenerList.get(i).stateChanged(state);
}
}
/**
* Returns the current status of the audio player.
*/
public PlayerState getStatus() {
return getPlayer().getStatus();
}
/**
* Returns true if the specified library item is the active playlist.
*/
public boolean isActivePlaylist(LibraryNavItem navItem) {
if (navItem != null) {
PlaylistId newId = new PlaylistId(navItem);
return newId.equals(playlistId);
}
return false;
}
/**
* Sets the active playlist using the specified library item.
*/
public void setActivePlaylist(LibraryNavItem navItem) {
if (navItem != null) {
PlaylistId oldPlaylist = playlistId;
playlistId = new PlaylistId(navItem);
if (!playlistId.equals(oldPlaylist)) {
inspectable.newListStarted();
}
} else {
playlistId = null;
playList.clear();
}
}
/**
* Returns the current playlist. The method may return an empty list, but
* never a null list.
*/
private List<LocalFileItem> getPlaylist() {
// Get selected list and category.
LibraryNavItem selectedNavItem = libraryMediator.getSelectedNavItem();
Category selectedCategory = libraryMediator.getSelectedCategory();
// No selected list so return internal playlist.
if (selectedNavItem == null) {
return playList;
}
// Compare selected list to playlist.
PlaylistId selectedId = new PlaylistId(selectedNavItem);
if (selectedId.equals(playlistId) &&
LibraryPanel.isPlayable(selectedCategory)) {
// Selected list is same so return list from library.
List<LocalFileItem> libraryList = libraryMediator.getPlayableList();
if (libraryList == null) libraryList = Collections.emptyList();
return libraryList;
} else {
// Selected list is different so return internal playlist.
return playList;
}
}
/**
* Sets the internal playlist using the specified list of file items. If
* the specified list is empty or null, then the playlist is cleared.
*/
public void setPlaylist(EventList<LocalFileItem> fileList) {
// Clear current playlist.
playList.clear();
if (fileList == null) return;
// Copy files into playlist.
for (int i = 0, size = fileList.size(); i < size; i++) {
playList.add(fileList.get(i));
}
}
/**
* Returns true if shuffle mode is enabled.
*/
public boolean isShuffle() {
return shuffle;
}
/**
* Sets an indicator to enable shuffle mode.
*/
public void setShuffle(boolean shuffle) {
this.shuffle = shuffle;
// Update shuffle list.
if (shuffle) {
updateShuffleList();
} else {
shuffleList.clear();
}
}
/**
* Sets the volume (gain) value on a linear scale from 0.0 to 1.0.
*/
public void setVolume(double value) {
getPlayer().setVolume(value);
}
/**
* Pauses the current song in the audio player.
*/
public void pause() {
getPlayer().pause();
}
/**
* Resumes playing the current song in the audio player. If the player is
* stopped, then attempt to play the first selected item in the library.
*/
public void resume() {
PlayerState status = getPlayer().getStatus();
if ((status == PlayerState.STOPPED) || (status == PlayerState.UNKNOWN)) {
// Get first selected item.
List<LocalFileItem> selectedItems = libraryMediator.getSelectedItems();
if (selectedItems.size() > 0) {
LocalFileItem selectedItem = selectedItems.get(0);
if (PlayerUtils.isPlayableFile(selectedItem.getFile())) {
// Set active playlist and play file item.
setActivePlaylist(libraryMediator.getSelectedNavItem());
play(selectedItem);
}
}
} else {
getPlayer().unpause();
}
}
/**
* Starts playing the specified file in the audio player. The playlist
* is automatically cleared so the player will stop when the song finishes.
*/
public void play(File file) {
// Stop current song.
stop();
// Play new song.
this.fileItem = null;
loadAndPlay(file);
// Clear play and shuffle lists.
setActivePlaylist(null);
shuffleList.clear();
}
/**
* Starts playing the specified file item in the audio player.
*/
public void play(LocalFileItem localFileItem) {
// Stop current song.
stop();
// Play new song.
this.fileItem = localFileItem;
loadAndPlay(localFileItem.getFile());
// Update shuffle list when enabled.
if (shuffle) {
updateShuffleList();
}
}
private void loadAndPlay(File fileToPlay) {
AudioPlayer player = getPlayer();
player.loadSong(fileToPlay);
player.playSong();
inspectable.started(fileToPlay);
}
/**
* Skips the current song to a new position in the song. If the song's
* length is unknown (streaming audio), then ignore the skip.
*
* @param percent of the song frames to skip from begining of file
*/
public void skip(double percent) {
// need to know something about the audio type to be able to skip
if (audioProperties != null && audioProperties.containsKey(AUDIO_TYPE)) {
String songType = (String) audioProperties.get(AUDIO_TYPE);
// currently, only mp3 and wav files can be seeked upon
if (isSeekable(songType) && audioProperties.containsKey(AUDIO_LENGTH_BYTES)) {
final long skipBytes = Math.round((Integer) audioProperties.get(AUDIO_LENGTH_BYTES)* percent);
getPlayer().seekLocation(skipBytes);
}
}
}
/**
* Stops playing the current song in the audio player.
*/
public void stop() {
getPlayer().stop();
}
/**
* Plays the next song in the playlist.
*/
public void nextSong() {
// Stop current song.
stop();
// Get next file item.
fileItem = getNextFileItem();
// Play song.
if (fileItem != null) {
loadAndPlay(fileItem.getFile());
}
}
/**
* Plays the previous song in the playlist.
*/
public void prevSong() {
// Stop current song.
stop();
// If near beginning of current song, then get previous song.
// Otherwise, restart current song.
if (progress < 0.1f) {
fileItem = getPrevFileItem();
}
// Play song.
if (fileItem != null) {
loadAndPlay(fileItem.getFile());
}
}
/**
* Returns the current playing file.
*/
public File getCurrentSongFile() {
AudioSource source = getPlayer().getCurrentSong();
return (source != null) ? source.getFile() : null;
}
/**
* Returns true if this file is currently playing, false otherwise
*/
public boolean isPlaying(File file) {
return getPlayer().isPlaying(file);
}
/**
* Returns true if this file is currently loaded and paused, false otherwise.
*/
public boolean isPaused(File file) {
return getPlayer().isPaused(file);
}
/**
* Returns true if the currently playing song is seekable.
*/
public boolean isSeekable() {
if (audioProperties != null) {
return isSeekable((String) audioProperties.get(AUDIO_TYPE));
}
return false;
}
/**
* Returns true if the specified song type is seekable, which means that
* the progress position can be set. At present, only MP3 and Wave files
* are seekable.
*/
private boolean isSeekable(String songType) {
if (songType == null) {
return false;
}
return songType.equalsIgnoreCase(MP3) || songType.equalsIgnoreCase(WAVE);
}
/**
* Updates the shuffle list of items to be played.
*/
private void updateShuffleList() {
// Clear shuffle list.
shuffleList.clear();
// Get current playlist.
List<LocalFileItem> playlist = getPlaylist();
if (playlist.size() == 0) return;
// Set shuffle list elements and randomize.
for (int i = 0; i < playlist.size(); i++) {
shuffleList.add(playlist.get(i));
}
Collections.shuffle(shuffleList);
// Move currently playing song to the beginning.
int index = shuffleList.indexOf(fileItem);
if (index > 0) {
shuffleList.remove(index);
shuffleList.add(0, fileItem);
}
}
/**
* Returns the next file item in the current playlist.
*/
private LocalFileItem getNextFileItem() {
// Get file list.
List<LocalFileItem> fileList = shuffle ? shuffleList : getPlaylist();
if ((fileItem != null) && (fileList.size() > 0)) {
int index = fileList.indexOf(fileItem);
if (index < (fileList.size() - 1)) {
return fileList.get(index + 1);
}
}
return null;
}
/**
* Returns the previous file item in the current playlist.
*/
private LocalFileItem getPrevFileItem() {
// Get file list.
List<LocalFileItem> fileList = shuffle ? shuffleList : getPlaylist();
if ((fileItem != null) && (fileList.size() > 0)) {
int index = fileList.indexOf(fileItem);
if (index > 0) {
return fileList.get(index - 1);
}
}
return null;
}
/**
* Returns the name of the current song.
*/
private String getSongName() {
// Use audio properties if available.
if (audioProperties != null) {
Object author = audioProperties.get("author");
Object title = audioProperties.get("title");
if ((author != null) && (title != null)) {
return author + " - " + title;
}
}
// Use file item if available.
if (fileItem != null) {
return fileItem.getFile().getName();
} else {
return I18n.tr("Unknown");
}
}
/**
* Listener to handle audio player events.
*/
private class PlayerListener implements AudioPlayerListener {
@Override
public void progressChange(int bytesread) {
// If we know the length of the song, update progress value.
if ((audioProperties != null) && audioProperties.containsKey(AUDIO_LENGTH_BYTES)) {
float byteslength = ((Integer) audioProperties.get(AUDIO_LENGTH_BYTES)).floatValue();
progress = bytesread / byteslength;
// Notify UI about progress.
fireProgressUpdated(progress);
}
}
@Override
public void songOpened(Map<String, Object> properties) {
// Save properties.
audioProperties = properties;
// Notify UI about new song.
fireSongChanged(getSongName());
}
@Override
public void stateChange(AudioPlayerEvent event) {
// Go to next song when finished.
if (event.getState() == PlayerState.EOM) {
nextSong();
} else if (event.getState() == PlayerState.STOPPED) {
inspectable.stopped();
}
// Notify UI about state change.
fireStateChanged(event.getState());
}
}
/**
* An identifier for a playlist.
*/
private static class PlaylistId {
private final NavType type;
private final int id;
public PlaylistId(LibraryNavItem navItem) {
this.type = navItem.getType();
this.id = navItem.getId();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PlaylistId) {
return (type == ((PlaylistId) obj).type) && (id == ((PlaylistId) obj).id);
}
return false;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + type.hashCode();
result = 31 * result + id;
return result;
}
}
/**
* Media Player inspections are cumulative, spanning all limewire sessions.
* Statistics are updated and stored via PropertiesSetting objects. During inspection,
* statistics are extracted from the properties.
*/
private class PlayerInspector implements Inspectable {
// key == percent, value == num times user stopped playing at percent
private final Properties percentPlayedProp = MediaPlayerSettings.MEDIA_PLAYER_PERCENT_PLAYED.get();
// key == file name, value = num times file played
private final Properties filesPlayed = MediaPlayerSettings.MEDIA_PLAYER_NUM_PLAYS.get();
// key == playlist size, value == num times playlist of this size was played
private final Properties playListSizeProp = MediaPlayerSettings.MEDIA_PLAYER_LIST_SIZE.get();
/**
* Called when media player stops playing a file.
*/
void stopped() {
incrementIntProperty(percentPlayedProp, Integer.toString(Math.round(progress*100)));
MediaPlayerSettings.MEDIA_PLAYER_PERCENT_PLAYED.set(percentPlayedProp);
}
/**
* Called when media player plays a file.
* @param filePlayed File that is played
*/
void started(File filePlayed) {
incrementIntProperty(filesPlayed, filePlayed.getName());
MediaPlayerSettings.MEDIA_PLAYER_NUM_PLAYS.set(filesPlayed);
}
/**
* Called when media player plays a playlist.
*/
void newListStarted() {
List<LocalFileItem> fileList = shuffle ? shuffleList : getPlaylist();
incrementIntProperty(playListSizeProp, Integer.toString(fileList.size()));
MediaPlayerSettings.MEDIA_PLAYER_LIST_SIZE.set(playListSizeProp);
}
private void incrementIntProperty(Properties properties, String key) {
String value = properties.getProperty(key);
Integer currentNum = (value == null) ? 0 : Integer.valueOf(value);
properties.setProperty(key, Integer.toString(currentNum+1));
}
/**
* Convert the PropertiesSetting data into inspectable data
*/
@Override
public Object inspect() {
int repeats = 0;
int numPlayStarts = 0;
for (String fileName : filesPlayed.stringPropertyNames()) {
int numOfPlaysForFile = Integer.parseInt(filesPlayed.getProperty(fileName));
numPlayStarts += numOfPlaysForFile;
if (numOfPlaysForFile > 1) {
repeats += numOfPlaysForFile - 1;
}
}
int numPlayStops = 0;
InspectionHistogram<Integer> percentPlayed = new InspectionHistogram<Integer>();
for (String percentPlayedStr : percentPlayedProp.stringPropertyNames()) {
Integer percent = Integer.valueOf(percentPlayedStr);
int numPlayedAtPercent = Integer.parseInt(percentPlayedProp.getProperty(percentPlayedStr));
numPlayStops += numPlayedAtPercent;
percentPlayed.count(percent, numPlayedAtPercent);
}
int numberOfTimesListPlayed = 0;
InspectionHistogram<Integer> playListsize = new InspectionHistogram<Integer>();
for (String numFilesInPlayListAsStr : playListSizeProp.stringPropertyNames()) {
Integer numFilesInPlayList = Integer.valueOf(numFilesInPlayListAsStr);
int numPlayListPlays = Integer.parseInt(playListSizeProp.getProperty(numFilesInPlayListAsStr));
numberOfTimesListPlayed += numPlayListPlays;
playListsize.count(numFilesInPlayList, numPlayListPlays);
}
Map<String,Object> ret = new HashMap<String,Object>();
ret.put("play_starts", numPlayStarts);
ret.put("repeats", repeats);
ret.put("play_stops", numPlayStops);
ret.put("percent_played", percentPlayed.inspect());
ret.put("total_list_plays", numberOfTimesListPlayed);
ret.put("list_size", playListsize.inspect());
return ret;
}
}
}