/*
* Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner,
* Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain,
* Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter,
* Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann,
* Samuel Zweifel
*
* This file is part of Jukefox.
*
* Jukefox 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 any later version. Jukefox 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
* Jukefox. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.ethz.dcg.jukefox.controller.player.playbackcontroller;
import java.util.Timer;
import java.util.TimerTask;
import ch.ethz.dcg.jukefox.commons.Constants;
import ch.ethz.dcg.jukefox.commons.DataUnavailableException;
import ch.ethz.dcg.jukefox.commons.utils.Log;
import ch.ethz.dcg.jukefox.controller.player.IPlaybackInfoBroadcaster;
import ch.ethz.dcg.jukefox.controller.player.mediaplayer.IMediaPlayerWrapper;
import ch.ethz.dcg.jukefox.controller.player.mediaplayer.OnMediaPlayerEventListener;
import ch.ethz.dcg.jukefox.controller.player.playlistmanager.IPlaylistManager;
import ch.ethz.dcg.jukefox.model.AbstractCollectionModelManager;
import ch.ethz.dcg.jukefox.model.AbstractPlayerModelManager;
import ch.ethz.dcg.jukefox.model.collection.BaseAlbum;
import ch.ethz.dcg.jukefox.model.collection.BaseArtist;
import ch.ethz.dcg.jukefox.model.collection.PlaylistSong;
import ch.ethz.dcg.jukefox.model.commons.EmptyPlaylistException;
import ch.ethz.dcg.jukefox.model.commons.NoNextSongException;
import ch.ethz.dcg.jukefox.model.commons.PlaylistPositionOutOfRangeException;
import ch.ethz.dcg.jukefox.model.player.PlayerAction;
import ch.ethz.dcg.jukefox.model.player.PlayerState;
import ch.ethz.dcg.jukefox.playmode.PlayerControllerCommand;
import ch.ethz.dcg.jukefox.playmode.PlayerControllerCommands;
public class GaplessPlaybackController extends BasePlaybackController {
private static final String TAG = GaplessPlaybackController.class.getSimpleName();
protected IMediaPlayerWrapper mediaPlayer2;
protected int currentMediaPlayerId = 1;
protected boolean isNextSongPrepared = false;
protected int lastPreloadedSongId = -1;
protected int lastPlayedSongId = -1;
protected Timer playTimer;
protected Timer prepareTimer;
protected Timer measureTimer;
protected int gaplessSongPrepareOffset;
protected int manualGapRemoveTime;
protected GaplessTimeMeasures gaplessTimeMeasures;
protected PlayerControllerCommands lastCommands;
public final OnMediaPlayerEventListener mediaPlayerEventListener = new OnMediaPlayerEventListener() {
@Override
public boolean onError(IMediaPlayerWrapper mp, int what, int extra) {
return GaplessPlaybackController.this.onError(mp, what, extra);
}
@Override
public boolean onInfo(IMediaPlayerWrapper mp, int what, int extra) {
return GaplessPlaybackController.this.onInfo(mp, what, extra);
}
@Override
public void onSongCompleted(IMediaPlayerWrapper mediaPlayer) {
GaplessPlaybackController.this.onSongCompleted(mediaPlayer.getCurrentSong());
}
};
public GaplessPlaybackController(IPlaybackInfoBroadcaster listenerInformer,
AbstractCollectionModelManager collectionModel, AbstractPlayerModelManager playerModel,
IPlaylistManager currentPlaylistManager, int autoGapRemoveTime, int manualGapRemoveTime,
IMediaPlayerWrapper mediaPlayer1, IMediaPlayerWrapper mediaPlayer2) {
super(listenerInformer, collectionModel, playerModel, currentPlaylistManager, mediaPlayer1);
this.mediaPlayer2 = mediaPlayer2;
this.manualGapRemoveTime = manualGapRemoveTime;
mediaPlayer2.setOnMediaPlayerEventListener(mediaPlayerEventListener);
gaplessTimeMeasures = new GaplessTimeMeasures(autoGapRemoveTime, manualGapRemoveTime);
gaplessSongPrepareOffset = manualGapRemoveTime + gaplessTimeMeasures.getGapTime() + 8000;
}
@Override
public synchronized int getDuration() {
return getDuration(getCurrentMediaPlayer());
}
@Override
public synchronized int getPlaybackPosition() {
return getCurrentPosition(getCurrentMediaPlayer());
}
@Override
public synchronized boolean jumpToPlaylistPosition(int position) {
try {
currentPlaylistManager.setCurrentSongIndex(position);
} catch (PlaylistPositionOutOfRangeException e) {
Log.w(TAG, e);
return false;
}
PlaylistSong<BaseArtist, BaseAlbum> song;
try {
song = currentPlaylistManager.getCurrentSong();
} catch (EmptyPlaylistException e) {
Log.w(TAG, e);
return false;
}
if (song == null) {
// This song is no longer in the database!
return false;
}
String path;
try {
path = collectionModel.getOtherDataProvider().getSongPath(song);
Log.v(TAG, "loadSong() " + song.getArtist().getName() + " - " + song.getName());
isNextSongPrepared = false;
lastPreloadedSongId = -1;
cancelTimers();
if (loadSongIntoPlayer(song, path, getCurrentMediaPlayer())) {
// Log.v(TAG, "cancelTimers() in loadSong1");
return true;
} else {
// Log.v(TAG, "cancelTimers() in loadSong2");
return false;
}
} catch (DataUnavailableException e) {
Log.w(TAG, e);
}
return false;
}
protected synchronized void cancelTimers() {
cancelPrepareTimer();
cancelPlayTimer();
}
@Override
public synchronized void pause() {
// Log.v(TAG, "cancelTimers() in pause()");
cancelTimers();
pause(getCurrentMediaPlayer());
}
@Override
public synchronized void play() {
// Log.v(TAG, "cancelTimers() in play()");
cancelTimers();
play(getCurrentMediaPlayer());
setNewPrepareTimer();
}
@Override
public synchronized void stop() {
// Log.v(TAG, "cancelTimers() in stop()");
cancelTimers();
stop(getCurrentMediaPlayer());
}
@Override
public synchronized void seekTo(int position) {
// Log.v(TAG, "cancelTimers() in seekTo()");
cancelTimers();
seekTo(getCurrentMediaPlayer(), position);
setNewPrepareTimer();
}
private synchronized void prepareNextSongToPlay() {
preloadNextSong();
setNewPlayTimer();
// Log.v(TAG, "Play timer set");
}
protected synchronized IMediaPlayerWrapper getCurrentMediaPlayer() {
if (currentMediaPlayerId == 2) {
return mediaPlayer2;
}
return mediaPlayer;
}
protected synchronized IMediaPlayerWrapper getNextMediaPlayer() {
if (currentMediaPlayerId == 1) {
return mediaPlayer2;
}
return mediaPlayer;
}
protected synchronized IMediaPlayerWrapper switchMediaPlayer() {
if (currentMediaPlayerId == 1) {
currentMediaPlayerId = 2;
return mediaPlayer2;
}
currentMediaPlayerId = 1;
return mediaPlayer;
}
@Override
public void onDestroy() {
stop();
if (mediaPlayer2 != null) {
stop(mediaPlayer2);
mediaPlayer2.onDestroy();
}
super.onDestroy();
}
protected synchronized void playPreloadedSong() {
if (!isNextSongPrepared) {
try {
lastCommands = currentPlaylistManager.getPlayMode().next(currentPlaylistManager.getCurrentPlaylist());
PlaylistSong<BaseArtist, BaseAlbum> songToLoad = applyPreloadControlCommands(lastCommands);
Log.v(TAG, "preloadNextSong() " + songToLoad.getArtist().getName() + " - " + songToLoad.getName());
try {
isNextSongPrepared = loadSongIntoPlayer(songToLoad, collectionModel.getOtherDataProvider()
.getSongPath(songToLoad), getCurrentMediaPlayer());
lastPreloadedSongId = songToLoad.getId();
} catch (DataUnavailableException e) {
Log.w(TAG, e);
}
applyPlayPreloadedControlCommands(lastCommands);
} catch (NoNextSongException e) {
Log.w(TAG, e);
stop();
}
return;
}
// IMediaPlayerWrapper currentMP = switchMediaPlayer();
switchMediaPlayer();
// play(currentMP);
try {
applyPlayPreloadedControlCommands(lastCommands);
} catch (Exception e) {
}
setMeasureTimer();
lastPlayedSongId = lastPreloadedSongId;
// Log.i(TAG, "started " + System.currentTimeMillis() + " " +
// currentMediaPlayerId);
isNextSongPrepared = false;
setNewPrepareTimer();
}
protected void setMeasureTimer() {
cancelMeasureTimer();
measureTimer = new Timer();
measureTimer.schedule(new TimerTask() {
@Override
public void run() {
if (lastLoadedSongId == lastPlayedSongId) {
gaplessTimeMeasures.setSong2Times(System.currentTimeMillis(), getPlaybackPosition(),
manualGapRemoveTime + gaplessTimeMeasures.getGapTime());
}
}
}, 1000);
}
private void cancelMeasureTimer() {
if (measureTimer != null) {
measureTimer.cancel();
measureTimer = null;
}
}
protected synchronized void preloadNextSong() {
gaplessTimeMeasures.setSong1Times(System.currentTimeMillis(), getPlaybackPosition(), getDuration());
if (isNextSongPrepared) {
return;
}
// Log.v(TAG, "preloadNextSong");
IMediaPlayerWrapper nextMP = getNextMediaPlayer();
PlaylistSong<BaseArtist, BaseAlbum> songToLoad;
try {
PlayerControllerCommands commands = currentPlaylistManager.getPlayMode().next(
currentPlaylistManager.getCurrentPlaylist());
songToLoad = applyPreloadControlCommands(commands);
lastCommands = commands;
Log.v(TAG, "preloadNextSong() " + songToLoad.getArtist().getName() + " - " + songToLoad.getName());
try {
isNextSongPrepared = loadSongIntoPlayer(songToLoad, collectionModel.getOtherDataProvider().getSongPath(
songToLoad), nextMP);
lastPreloadedSongId = songToLoad.getId();
} catch (DataUnavailableException e) {
Log.w(TAG, e);
}
} catch (NoNextSongException e) {
// Log.w(TAG, e);
}
}
@Override
public void onSongCompleted(PlaylistSong<BaseArtist, BaseAlbum> song) {
listenerInformer.informSongCompletedListeners(song);
}
private synchronized void cancelPlayTimer() {
if (playTimer != null) {
playTimer.cancel();
// Log.v(TAG, "Cancelled play timer");
}
}
private synchronized void cancelPrepareTimer() {
if (prepareTimer != null) {
prepareTimer.cancel();
}
// Log.v(TAG, "Cancelled prepare timer");
}
protected synchronized void setNewPlayTimer() {
// Log.v(TAG, "cancelTimers() in setNewPlayTimers()");
cancelTimers();
if (getState() != PlayerState.PLAY) {
// Log.v(TAG, "Player not in play mode. Not setting play timer.");
return;
}
playTimer = new Timer();
TimerTask playPreparedSongTask = new TimerTask() {
@Override
public void run() {
playPreloadedSong();
}
};
IMediaPlayerWrapper currentMediaPlayer = getCurrentMediaPlayer();
int playIn = getDuration(currentMediaPlayer) - getCurrentPosition(currentMediaPlayer) - manualGapRemoveTime
- gaplessTimeMeasures.getGapTime();
if (playIn < 0) {
playPreloadedSong();
return;
}
playTimer.schedule(playPreparedSongTask, playIn);
// Log.v(TAG, "Set play timer");
}
protected synchronized void setNewPrepareTimer() {
// Log.v(TAG, "cancelTimers() setNewPrepareTimer()");
cancelTimers();
if (getState() != PlayerState.PLAY) {
// Log.v(TAG,
// "Player not in play mode. Not setting prepare timer.");
return;
}
IMediaPlayerWrapper currentMediaPlayer = getCurrentMediaPlayer();
prepareTimer = new Timer();
int prepareIn = getDuration(currentMediaPlayer) - getCurrentPosition(currentMediaPlayer)
- gaplessSongPrepareOffset;
// System.out
// .println("Set song-prepare-timer to load the song in " + prepareIn
// + " msec. Duration: " + getDuration(currentMediaPlayer) + " position: " + getCurrentPosition(currentMediaPlayer));
if (prepareIn <= 0) {
prepareNextSongToPlay();
return;
}
prepareTimer.schedule(new TimerTask() {
@Override
public void run() {
prepareNextSongToPlay();
}
}, prepareIn);
// Log.v(TAG, "Set prepare timer");
}
@Override
public void reloadSettings() {
super.reloadSettings();
}
@Override
public void mute() {
super.mute(getCurrentMediaPlayer());
}
@Override
public void unmute() {
super.unmute(getCurrentMediaPlayer());
}
/**
* Applies all commands until it reaches a playerAction PLAY command (not including the play command)
*
* @param commands
* @return the song that should be preloaded in the next media player
*/
protected PlaylistSong<BaseArtist, BaseAlbum> applyPreloadControlCommands(PlayerControllerCommands commands) {
int position = currentPlaylistManager.getCurrentPlaylist().getPositionInList();
for (PlayerControllerCommand command : commands.getAllCommands()) {
switch (command.getType()) {
case ADD_SONG:
try {
currentPlaylistManager.insertSongAtPosition(command.getSong(), command.getPosition());
} catch (PlaylistPositionOutOfRangeException e) {
Log.w(TAG, e);
}
break;
case REMOVE_SONG:
try {
currentPlaylistManager.removeSongFromPlaylist(command.getPosition());
} catch (EmptyPlaylistException e) {
Log.w(TAG, e);
} catch (PlaylistPositionOutOfRangeException e) {
Log.w(TAG, e);
}
break;
case PLAYER_ACTION:
// Don't execute player commands when preloading a song
break;
case SET_POS_IN_LIST:
// Don't set a playlist position when preloading a song,
// just remember the position
position = command.getPosition();
break;
case SET_POS_IN_SONG:
// Don't seek when preloading a song
break;
case SET_PLAY_MODE:
setPlayMode(command.getPlayMode(), 0, Constants.SAME_SONG_AVOIDANCE_NUM); // TODO: use real parameters
break;
}
}
PlaylistSong<BaseArtist, BaseAlbum> song = currentPlaylistManager.getCurrentPlaylist().getSongList().get(
position);
return song;
}
/**
* Applies just playerAction PLAY commands (including the play command)
*
* @param commands
*/
protected void applyPlayPreloadedControlCommands(PlayerControllerCommands commands) {
for (PlayerControllerCommand command : commands.getAllCommands()) {
switch (command.getType()) {
case ADD_SONG:
// Ignore when playing a preloaded song
break;
case REMOVE_SONG:
// Ignore when playing a preloaded song
break;
case PLAYER_ACTION:
if (command.getPlayerAction() == PlayerAction.PLAY) {
play(getCurrentMediaPlayer());
} else if (command.getPlayerAction() == PlayerAction.PAUSE) {
pause(getCurrentMediaPlayer());
} else if (command.getPlayerAction() == PlayerAction.STOP) {
stop(getCurrentMediaPlayer());
}
break;
case SET_POS_IN_LIST:
try {
currentPlaylistManager.setCurrentSongIndex(command.getPosition());
} catch (PlaylistPositionOutOfRangeException e) {
Log.w(TAG, e);
}
break;
case SET_POS_IN_SONG:
// Ignore when playing a preloaded song
break;
case SET_PLAY_MODE:
setPlayMode(command.getPlayMode(), 0, Constants.SAME_SONG_AVOIDANCE_NUM); // TODO: use real parameters
break;
}
}
}
}