/* * Jajuk * Copyright (C) The Jajuk Team * http://jajuk.info * * 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 2 * of the License, or 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ package org.jajuk.services.players; import java.io.PrintStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.StringUtils; import org.jajuk.base.File; import org.jajuk.services.webradio.WebRadio; import org.jajuk.util.Conf; import org.jajuk.util.Const; import org.jajuk.util.UtilSystem; import org.jajuk.util.log.Log; /** * Mplayer player implementation. */ public abstract class AbstractMPlayerImpl implements IPlayerImpl, Const { /** Stored Volume. */ float fVolume; /** Mplayer process. */ volatile Process proc; /** End of file flag *. */ volatile boolean bEOF = false; /** File is opened flag *. */ volatile boolean bOpening = false; /** Stop position thread flag. */ volatile boolean bStop = false; /** Fading state. */ volatile boolean bFading = false; /** pause flag *. */ protected volatile boolean bPaused = false; /** Whether the track has been started in bitperfect mode **/ boolean bitPerfect = false; /* * * Kill abruptly the mplayer process (this way, killing is synchronous, and * easier than sending a quit command). Do not try to send a 'quit' command to * mplayer because then, it's not possible to differentiate end of file from * forced quit and the fifo will comes out of control */ /* (non-Javadoc) * @see org.jajuk.services.players.IPlayerImpl#stop() */ @Override public void stop() throws Exception { bFading = false; this.bStop = true; Log.debug("Stop"); if (proc != null) { if (UtilSystem.isUnderLinux()) { /* * Under linux (not sure if it may happen on others Unix and never * reproduced under Windows), mplayer process can "zombified" after * destroy() method call for unknown reason (linked with the mplayer * slave mode ?). Even worse, these processes block the dsp audio line * and then all new mplayer processes fail. To avoid this, we force a * kill on every process call under Linux. * * Note also that mplayer slave mode opens two processes with different * pids. When we try to kill them with -9 (abruptly) only the parent * process dies and the second process is left hanging in the * background. The solution is to just use kill (without -9) to let both * mplayer processes die gracefully. I guess the destroy() method * internally also tries to use -9 and so both pids are never killed. */ Field field = proc.getClass().getDeclaredField("pid"); field.setAccessible(true); int pid = field.getInt(proc); try { ProcessBuilder pb = new ProcessBuilder("kill", Integer.toString(pid)); pb.start(); } catch (Error error) { Log.error(error); } } else { proc.destroy(); } } } /* * (non-Javadoc) * * @see org.jajuk.base.IPlayerImpl#setVolume(float) */ @Override public void setVolume(float fVolume) { this.fVolume = fVolume; // Fix for a issue under Linux (at least with pulseaudio) : if a track is started in bitperfect mode (no volume specified), then // the mode is unset when the same track is playing. When the fade out occurs, the volume commands sent to mplayer are propagated for some reasons // directly to the pulsaudio mixer and the next track sound volume is affected (muted most of times). if (bitPerfect) { Log.warn("This track was started in bit-perfect mode, even if the mode has been disabled, it can apply only to next track"); return; } sendCommand("volume " + (int) (100 * fVolume) + " 2"); // Not not log this when fading, generates too much logs if (!bFading) { Log.debug("Set Volume= " + (int) (100 * fVolume) + " %"); } } /** * Send a command to mplayer slave. * * @param command */ protected void sendCommand(String command) { if (proc != null) { PrintStream out = new PrintStream(proc.getOutputStream()); // Do not use println() : it doesn't work under windows out.print(command + '\n'); out.flush(); // don't close out here otherwise the output stream of the Process // will be closed as well and subsequent sendCommand() calls will silently // fail!! } } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#getCurrentVolume() */ @Override public float getCurrentVolume() { return fVolume; } /** * Build the mplayer command line. * * @param url the url to play * @param startPositionSec the position in the track when starting in secs (0 means we plat from the begining) * * @return command line as a String array */ List<String> buildCommand(String url, int startPositionSec) { String sCommand = "mplayer"; // Use any forced mplayer path String forced = Conf.getString(Const.CONF_MPLAYER_PATH_FORCED); if (!StringUtils.isBlank(forced)) { sCommand = forced; } else { if (UtilSystem.isUnderWindows()) { sCommand = UtilSystem.getMPlayerWindowsPath().getAbsolutePath(); } else if (UtilSystem.isUnderOSX()) { sCommand = UtilSystem.getMPlayerOSXPath().getAbsolutePath(); } } String sAdditionalArgs = Conf.getString(Const.CONF_MPLAYER_ARGS); // Build command List<String> cmd = new ArrayList<String>(10); cmd.add(sCommand); // Start at given position cmd.add("-ss"); cmd.add(Integer.toString(startPositionSec)); // quiet: less traces cmd.add("-quiet"); // slave: slave mode (control with stdin) cmd.add("-slave"); // No af options if bit perfect is enabled if (!Conf.getBoolean(CONF_BIT_PERFECT)) { // -af volume: Use volnorm to limit gain to max // If mute, use -200db otherwise, use a linear scale cmd.add("-af"); cmd.add(buildAudioFilters()); // -softvol : use soft mixer, allows to set volume only to this mplayer // instance, not others programs cmd.add("-softvol"); } // Define a cache. It is useful to avoid sound gliches but also to // overide a local mplayer large cache configuration in // ~/.mplayer/config file. User can set a large cache for video for ie. String cacheSize = "500"; // 500Kb, mplayer starts before the cache is filled up if (!Conf.getString(Const.CONF_MPLAYER_ARGS).matches(".*-cache.*")) { // If user already forced a cache value, do not overwrite it cmd.add("-cache"); cmd.add(cacheSize); } if (!StringUtils.isBlank(sAdditionalArgs)) { // Add any additional arguments provided by user String[] sArgs = sAdditionalArgs.split(" "); for (String element : sArgs) { cmd.add(element); } } // If it is a playlist, add the -playlist option, must be the last option // because options after -playlist are ignored (see mplayer man page). // Moreover, we only use this option if we are about to play line-based stream like m3u or the playback will fail. if (url.matches(".*://.*") && (url.toLowerCase().endsWith(".m3u") || url.toLowerCase().endsWith(".asx") || url .toLowerCase().endsWith(".pls"))) { cmd.add("-playlist"); } cmd.add(url); return cmd; } /** * Build the -af audio filters command part. * * @return the string */ private String buildAudioFilters() { // Audio filters syntax : -af // <filter1[=parameter1:parameter2:...],filter2,...> // Add -volnorm (audio normalization) if option is set StringBuilder audiofilters = new StringBuilder(); if (Conf.getBoolean(CONF_USE_VOLNORM)) { audiofilters.append("volnorm,"); } // gain = -200 = mute int volume = -200; if (fVolume != 0) { // Gain = 10 * log(fVolume) volume = (int) (10 * Math.log(fVolume)); } audiofilters.append("volume=" + volume); // Add karaoke state if required if (Conf.getBoolean(CONF_STATE_KARAOKE)) { audiofilters.append(",karaoke"); } return audiofilters.toString(); } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#getDurationSec() */ @Override public long getDurationSec() { return 0; } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#getCurrentPosition() */ @Override public float getCurrentPosition() { return 0; } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#getElapsedTimeMillis() */ @Override public long getElapsedTimeMillis() { return 0; } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#play(org.jajuk.base.File, float, long, * float) */ @Override public abstract void play(File file, float fPosition, long length, float fVolume) throws Exception; /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#play(org.jajuk.base.WebRadio, float) */ @Override public abstract void play(WebRadio radio, float fVolume) throws Exception; /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#seek(float) */ @Override public void seek(float fPosition) { // required by interface, but nothing to do here... } /** * Gets the state. * * @return player state, -1 if player is null. */ @Override public int getState() { return -1; } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#pause() */ @Override public void pause() throws Exception { bPaused = true; sendCommand("pause"); } /* * (non-Javadoc) * * @see org.jajuk.players.IPlayerImpl#resume() */ @Override public void resume() throws Exception { // This test is required because in case of volume change, mplayer is // already resumed and we don't want to send another pause command if (bPaused) { bPaused = false; sendCommand("pause"); } } }