/* * Copyright (C) 2012-2016 Julien Bonjean <julien@bonjean.info> * * This file is part of Beluga Player. * * This program 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 (at your option) any later * version. * * This program 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 * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package info.bonjean.beluga.gui.pivot; import info.bonjean.beluga.bus.InternalBus; import info.bonjean.beluga.bus.PlaybackEvent; import info.bonjean.beluga.client.BelugaState; import info.bonjean.beluga.client.PandoraPlaylist; import info.bonjean.beluga.configuration.BelugaConfiguration; import info.bonjean.beluga.player.MP3Player; import info.bonjean.beluga.response.Song; import info.bonjean.beluga.util.ResourcesUtil; import java.io.IOException; import java.net.URL; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.apache.pivot.beans.BXML; import org.apache.pivot.beans.Bindable; import org.apache.pivot.collections.Map; import org.apache.pivot.util.Resources; import org.apache.pivot.wtk.ApplicationContext; import org.apache.pivot.wtk.Label; import org.apache.pivot.wtk.LinkButton; import org.apache.pivot.wtk.Meter; import org.apache.pivot.wtk.TablePane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author Julien Bonjean <julien@bonjean.info> * */ public class PlayerUI extends TablePane implements Bindable { private static Logger log = LoggerFactory.getLogger(PlayerUI.class); @BXML private MainWindow mainWindow; @BXML private Label stationName; @BXML private Label currentTime; @BXML private Label totalTime; @BXML private Meter progress; @BXML private Meter progressCache; @BXML protected LinkButton nextButton; @BXML protected LinkButton pauseButton; private final BelugaState state = BelugaState.getInstance(); private final BelugaConfiguration configuration = BelugaConfiguration.getInstance(); private final MP3Player mp3Player = new MP3Player(); private static final int UI_REFRESH_INTERVAL = 200; private Future<?> playerUISyncFuture; private Future<?> playbackThreadFuture; private volatile long duration; private volatile boolean closed; @Override public void initialize(Map<String, Object> namespace, URL location, Resources resources) { currentTime.setText("00:00"); totalTime.setText("00:00"); progress.setPercentage(0); progressCache.setPercentage(0); } private String formatTime(long ms) { return String.format( "%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(ms), TimeUnit.MILLISECONDS.toSeconds(ms) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(ms))); } public void open() { closed = false; // ensure the thread is running if (playbackThreadFuture == null || playbackThreadFuture.isDone() || closed) playbackThreadFuture = ThreadPools.playbackPool.submit(new Playback()); // playback will start automatically } public void playPause() { mp3Player.pause(); InternalBus.publish(new PlaybackEvent(mp3Player.isPaused() ? PlaybackEvent.Type.SONG_PAUSE : PlaybackEvent.Type.SONG_RESUME, null)); // replace play/pause button ApplicationContext.queueCallback(new Runnable() { @Override public void run() { try { pauseButton.setButtonData(ResourcesUtil.getSVGImage(mp3Player.isPaused() ? "/img/play.svg" : "/img/pause.svg")); } catch (IOException e) { log.debug(e.getMessage()); } } }, false); } public void skip() { // stop the player to skip the song mp3Player.stop(); } public void close() { if (playbackThreadFuture == null || playbackThreadFuture.isDone() || closed) return; closed = true; // stop the player mp3Player.stop(); // stop the playback thread try { if (playbackThreadFuture != null) playbackThreadFuture.get(5, TimeUnit.SECONDS); } catch (Exception e) { log.error(e.getMessage(), e); playbackThreadFuture.cancel(true); } } public boolean isPaused() { return mp3Player.isPaused(); } public boolean isClosed() { return closed; } private Runnable syncUI = new Runnable() { @Override public void run() { if (!mp3Player.isActive()) return; final long position = mp3Player.getPosition(); final float progressValue = position / (float) duration; final float cacheProgressValue = mp3Player.getCachePosition() / (float) duration; // update song position (playback can stop anytime) state.getSong().setPosition(position); ApplicationContext.queueCallback(new Runnable() { @Override public void run() { // update progress bar currentTime.setText(formatTime(position)); progress.setPercentage(progressValue); progressCache.setPercentage(cacheProgressValue); } }, true); } }; private class Playback implements Runnable { @Override public void run() { // init failure counter int successiveFailures = 0; // start the UI synchronization thread playerUISyncFuture = ThreadPools.playerUIScheduler.scheduleAtFixedRate(syncUI, 0, UI_REFRESH_INTERVAL, TimeUnit.MILLISECONDS); Song song; while (true) { song = null; if (closed) break; try { if (successiveFailures == 0 || state.getSong() == null) song = PandoraPlaylist.getInstance().getNext(); else // do not skip to next song if we failed before song = state.getSong(); if (song == null) { Thread.sleep(500); continue; } log.debug("New song: " + song.getAdditionalAudioUrl()); // initialize the player try { log.info("openingAudioStream"); mp3Player.loadSong(song.getAdditionalAudioUrl()); successiveFailures = 0; } catch (Exception e) { successiveFailures++; if (successiveFailures >= 3) { log.error(e.getMessage(), e); break; } else { log.info(e.getMessage(), e); Thread.sleep(2000); } continue; } duration = mp3Player.getDuration(); song.setDuration(duration); // is there a better way to detect the Pandora skip // protection (42sec length mp3)? if (duration == 42569 && mp3Player.getBitrate() == 64000) { log.error("pandoraSkipProtection"); break; } // guess if it's an ad (not very reliable) if (configuration.getAdsDetectionEnabled() && mp3Player.getBitrate() == 128000 && duration < 45000) { log.debug("Ad detected"); // set the ad flag on the song, for the display and to // skip scrobbling song.setAd(true); } // notify song started InternalBus.publish(new PlaybackEvent(PlaybackEvent.Type.SONG_START, song)); // initialize controls ApplicationContext.queueCallback(new Runnable() { @Override public void run() { // update song duration totalTime.setText(formatTime(duration)); // update station name stationName.setText(state.getStation().getStationName()); } }, false); // increase thread priority before starting playback Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // start playback mp3Player.play(configuration.getAdsSilenceEnabled() && song.isAd()); // restore thread priority to normal Thread.currentThread().setPriority(Thread.NORM_PRIORITY); // disable controls ApplicationContext.queueCallback(new Runnable() { @Override public void run() { // set progress bar to full currentTime.setText(formatTime(duration)); progress.setPercentage(1); } }, false); log.debug("Playback finished"); } catch (Exception e) { log.error(e.getMessage(), e); break; } finally { if (song != null && song.getDuration() > 0) // notify song finished InternalBus .publish(new PlaybackEvent(PlaybackEvent.Type.SONG_FINISH, song)); } } log.debug("Exiting playback thread"); // if closed has not been requested, we are disconnected if (!closed) InternalBus .publish(new PlaybackEvent(PlaybackEvent.Type.PANDORA_DISCONNECTED, null)); closed = true; // stop the UI thread playerUISyncFuture.cancel(false); } } }