/* * JAVE - A Java Audio/Video Encoder (based on FFMPEG) * * Copyright (C) 2008-2009 Carlo Pelliccia (www.sauronsoftware.it) * * 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, see <http://www.gnu.org/licenses/>. */ package be.tarsos.transcoder.ffmpeg; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ProcessBuilder.Redirect; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteWatchdog; import org.apache.commons.exec.PumpStreamHandler; import be.tarsos.transcoder.Attributes; /** * A ffmpeg process wrapper. * * @author Carlo Pelliccia */ class FFMPEGExecutor { /** * Log messages. */ private static final Logger LOG = Logger.getLogger(FFMPEGExecutor.class.getName()); /** * The path of the ffmpeg executable. */ private final String ffmpegExecutablePath; /** * Arguments for the executable. */ private final ArrayList<String> args = new ArrayList<String>(); private final ArrayList<Boolean> argIsFile = new ArrayList<Boolean>(); /** * It build the executor. * * @param ffmpegExecutablePath * The path of the ffmpeg executable. */ public FFMPEGExecutor(String ffmpegExecutablePath) { this.ffmpegExecutablePath = ffmpegExecutablePath; } /** * Adds an argument to the ffmpeg executable call. * * @param arg * The argument. */ public void addArgument(String arg) { args.add(arg); argIsFile.add(false); } /** * Add a file to the ffmpeg executable call. * @param arg */ public void addFileArgument(String arg){ args.add(arg); argIsFile.add(true); } /** * Executes the ffmpeg process with the previous given arguments. * * @return The standard output of the child process. * * @throws IOException * If the process call fails. */ public String execute() throws IOException { CommandLine cmdLine = new CommandLine(ffmpegExecutablePath); int fileNumber=0; Map<String,File> map = new HashMap<String,File>(); for (int i = 0 ;i<args.size();i++) { final String arg = args.get(i); final Boolean isFile = argIsFile.get(i); if(isFile){ String key = "file" + fileNumber; map.put(key, new File(arg)); cmdLine.addArgument("'${" + key + "}'",false); fileNumber++; } else { cmdLine.addArgument(arg); } } cmdLine.setSubstitutionMap(map); LOG.fine("Execute: " + cmdLine); DefaultExecutor executor = new DefaultExecutor(); //5minutes wait ExecuteWatchdog watchdog = new ExecuteWatchdog(60 * 1000 * 5); executor.setWatchdog(watchdog); ByteArrayOutputStream out = new ByteArrayOutputStream(); executor.setStreamHandler(new PumpStreamHandler(out)); int[] exitValues = {0,1}; executor.setExitValues(exitValues); executor.execute(cmdLine); return out.toString(); } public AudioInputStream pipe(Attributes attributes) throws EncoderException { String pipeEnvironment; String pipeArgument; File pipeLogFile; int pipeBuffer; if(System.getProperty("os.name").indexOf("indows") > 0 ){ pipeEnvironment = "cmd.exe"; pipeArgument = "/C"; }else{ pipeEnvironment = "/bin/bash"; pipeArgument = "-c"; } pipeLogFile = new File("decoder_log.txt"); //buffer 1/4 second of audio. pipeBuffer = attributes.getSamplingRate()/4; AudioFormat audioFormat = Encoder.getTargetAudioFormat(attributes); String command = toString(); ProcessBuilder pb = new ProcessBuilder(pipeEnvironment, pipeArgument , command); pb.redirectError(Redirect.appendTo(pipeLogFile)); LOG.fine("Starting piped decoding process" ); final Process process; try { process = pb.start(); } catch (IOException e1) { throw new EncoderException("Problem starting piped sub process: " + e1.getMessage()); } InputStream stdOut = new BufferedInputStream(process.getInputStream(), pipeBuffer); //read and ignore the 46 byte wav header, only pipe the pcm samples to the audioinputstream byte[] header = new byte[46]; double sleepSeconds = 0; double timeoutLimit = 20; //seconds try { while(stdOut.available() < header.length){ try { Thread.sleep(100); sleepSeconds += 0.1; } catch (InterruptedException e) { e.printStackTrace(); } if(sleepSeconds > timeoutLimit){ throw new Error("Could not read from pipe within " + timeoutLimit + " seconds: timeout!"); } } int bytesRead = stdOut.read(header); if(bytesRead != header.length){ throw new EncoderException("Could not read complete WAV-header from pipe. This could result in mis-aligned frames!"); } } catch (IOException e1) { throw new EncoderException("Problem reading from piped sub process: " + e1.getMessage()); } final AudioInputStream audioStream = new AudioInputStream(stdOut, audioFormat, AudioSystem.NOT_SPECIFIED); //This thread waits for the end of the subprocess. new Thread(new Runnable(){ public void run() { try { process.waitFor(); LOG.fine("Finished piped decoding process"); } catch (InterruptedException e) { LOG.severe("Interrupted while waiting for sub process exit."); e.printStackTrace(); } }},"Decoding Pipe Reader").start(); return audioStream; } public String toString(){ CommandLine cmdLine = new CommandLine(ffmpegExecutablePath); int fileNumber=0; Map<String,File> map = new HashMap<String,File>(); for (int i = 0 ;i<args.size();i++) { final String arg = args.get(i); final Boolean isFile = argIsFile.get(i); if(isFile){ String key = "file" + fileNumber; map.put(key, new File(arg)); cmdLine.addArgument("${" + key + "}",false); fileNumber++; } else { cmdLine.addArgument(arg); } } cmdLine.setSubstitutionMap(map); return cmdLine.toString(); } }