package com.robonobo.plugin.mplayer;
import static java.lang.Math.*;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.robonobo.common.concurrent.CatchingRunnable;
import com.robonobo.common.io.LinedInputStreamHandler;
import com.robonobo.core.api.AudioPlayer;
import com.robonobo.core.api.AudioPlayerListener;
import com.robonobo.core.api.model.Stream;
import com.robonobo.mina.external.buffer.PageBuffer;
/**
* Plays audio through mplayer by creating an external mplayer process in slave mode For slave mode reference, see
* ftp://ftp2.mplayerhq.hu/MPlayer/DOCS/tech/slave.txt
*
* @author macavity
*
*/
public class MplayerAudioPlayer implements AudioPlayer {
private static final String MPLAYER_ARGS = "-slave -quiet -hr-mp3-seek -cache-min 10";
ScheduledThreadPoolExecutor executor;
File mplayerExe;
MplayerHttpServer server;
MplayerProcHandler handler;
Status status = Status.Starting;
List<AudioPlayerListener> listeners = new ArrayList<AudioPlayerListener>();
Log log = LogFactory.getLog(getClass());
Stream s;
PageBuffer pb;
int serverListenPort;
// TODO starting up mplayer and shutting it down every time here - need to refactor audioplayer to have
// persistent instances for better responsiveness
public MplayerAudioPlayer(ScheduledThreadPoolExecutor executor, Stream s, PageBuffer pb, File mplayerExe)
throws IOException {
this.executor = executor;
this.mplayerExe = mplayerExe;
if (!mplayerExe.canExecute())
throw new IOException("mplayer exe " + mplayerExe.getAbsolutePath()
+ " does not exist or is not executable");
this.s = s;
this.pb = pb;
server = new MplayerHttpServer();
server.start();
serverListenPort = server.getPort();
server.addStream(s, pb);
}
@Override
public void play() throws IOException {
if (status == Status.Playing)
return;
if (status == Status.Paused)
handler.resume();
else
handler = new MplayerProcHandler();
status = Status.Playing;
}
@Override
public void pause() throws IOException {
if (status == Status.Paused)
return;
handler.pause();
status = Status.Paused;
}
@Override
public void stop() {
handler.die();
server.stop();
status = Status.Stopped;
}
@Override
public void seek(long ms) throws IOException {
handler.seek(ms);
}
public void addListener(AudioPlayerListener listener) {
listeners.add(listener);
}
public void removeListener(AudioPlayerListener listener) {
listeners.remove(listener);
}
private String getMplayerUrl(Stream s) {
return "http://localhost:" + serverListenPort + "/" + s.getStreamId() + ".mp3";
}
class MplayerProcHandler {
Thread stdoutRdr, stderrRdr;
PrintWriter stdinWriter;
Process mplayerProc;
boolean waitingForPlayback = true;
Future<?> getPropsTask;
public MplayerProcHandler() throws IOException {
String cmdLine = mplayerExe.getAbsolutePath() + " " + MPLAYER_ARGS + " " + getMplayerUrl(s);
log.debug("DEBUG: run mplayer with cmdline: "+cmdLine);
// mplayerProc = Runtime.getRuntime().exec(cmdLine);
// stdoutRdr = new Thread(new StdOutHandler(mplayerProc.getInputStream()));
// stdoutRdr.start();
// stderrRdr = new Thread(new StdErrHandler(mplayerProc.getErrorStream()));
// stderrRdr.start();
// stdinWriter = new PrintWriter(mplayerProc.getOutputStream());
// getPropsTask = executor.scheduleAtFixedRate(new CatchingRunnable() {
// public void doRun() throws Exception {
// if (status == Status.Playing) {
// stdinWriter.write("pausing_keep get_property time_pos\n");
// // stdinWriter.write("pausing_keep get_property path\n");
// stdinWriter.flush();
// }
// }
// }, 500, 200, TimeUnit.MILLISECONDS);
}
private void pause() {
stdinWriter.write("pause\n");
stdinWriter.flush();
}
// private void play() {
// // TODO If we pause then playback a different stream, and we get snippets of the previous stream, see
// // mplayer slave mode doc for how to workaround
// stdinWriter.write("loadfile " + getMplayerUrl(s) + "\n");
// stdinWriter.flush();
// }
private void resume() {
stdinWriter.write("pause\n");
stdinWriter.flush();
}
private void seek(long ms) {
int secs = round(ms / 1000f);
log.debug("Seeking to: " + secs);
stdinWriter.write("seek " + secs + " 2\n");
}
private void die() {
stdinWriter.write("quit\n");
stdinWriter.flush();
stdoutRdr.interrupt();
stderrRdr.interrupt();
getPropsTask.cancel(true);
stdinWriter.close();
mplayerProc.destroy();
}
class StdErrHandler extends LinedInputStreamHandler {
final String[] ignoreLines = { "nop_streaming_read error : Bad file descriptor" };
public StdErrHandler(InputStream is) {
super(is);
}
@Override
public void handleLine(String line) {
for (String ignoreLine : ignoreLines) {
if (line.trim().equalsIgnoreCase(ignoreLine))
return;
}
log.debug("Mplayer stderr output: " + line);
}
@Override
protected void handleException(IOException e) {
log.debug("Mplayer stderr handler caught IOException: " + e.getMessage());
}
}
class StdOutHandler extends LinedInputStreamHandler {
Pattern playbackStartPattern = Pattern.compile("^Starting playback...$");
Pattern propValPattern = Pattern.compile("^(\\S+)=(\\S+)$");
Pattern endPattern = Pattern.compile("^Exiting... \\((.*)\\)$");
public StdOutHandler(InputStream is) {
super(is);
}
@Override
public void handleLine(String line) {
log.info("Got stdout from mplayer: " + line);
if (waitingForPlayback) {
if (playbackStartPattern.matcher(line).matches()) {
waitingForPlayback = false;
log.debug("MPlayer playback starting...");
}
return;
}
Matcher m;
m = endPattern.matcher(line);
if (m.matches()) {
// We're done here
String reason = m.group(1);
if (reason.equalsIgnoreCase("End of file")) {
for (AudioPlayerListener listener : listeners) {
try {
listener.onCompletion();
} catch (Exception e) {
log.error(
"Caught exception passing end of playback to audioplayerlistener " + listener,
e);
}
}
} else {
for (AudioPlayerListener listener : listeners) {
try {
listener.onError(reason);
} catch (Exception e) {
log.error("Caught exception passing error to audioplayerlistener " + listener, e);
}
}
}
stop();
return;
}
m = propValPattern.matcher(line);
if (m.matches())
readProperty(m.group(1), m.group(2));
else
log.debug("Read unexpected line from mplayer stdout: " + line);
}
private void readProperty(String prop, String val) {
if (prop.equalsIgnoreCase("ans_time_pos")) {
long usPos = (long) (Float.parseFloat(val) * 1000000);
log.debug("Got mplayer progress: " + usPos + "us");
for (AudioPlayerListener listener : listeners) {
try {
listener.onProgress(usPos);
} catch (Exception e) {
log.error("Caught exception passing progress to audioplayerlistener " + listener, e);
}
}
}
}
@Override
protected void handleException(IOException e) {
log.debug("Mplayer stdout handler caught IOException: " + e.getMessage());
}
}
}
}