/* * 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.File; import java.io.IOException; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.StringTokenizer; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import be.tarsos.transcoder.Attributes; /** * Main class of the package. Instances can encode audio and video streams. * * @author Carlo Pelliccia * @author Joren Six */ public class Encoder { private static final Logger LOG = Logger.getLogger(Encoder.class.getName()); private static ArrayList<FFMPEGLocator> locators = new ArrayList<FFMPEGLocator>(); public static void addFFMPEGLocator(FFMPEGLocator locator) { locators.add(locator); } public static boolean hasLocators() { return locators.size() > 0; } /** * This regexp is used to parse the ffmpeg output about the bit rate value * of a stream. */ private static final Pattern BIT_RATE_PATTERN = Pattern.compile("(\\d+)\\s+kb/s", Pattern.CASE_INSENSITIVE); /** * This regexp is used to parse the ffmpeg output about the sampling rate of * an audio stream. */ private static final Pattern SAMPLING_RATE_PATTERN = Pattern.compile("(\\d+)\\s+Hz", Pattern.CASE_INSENSITIVE); /** * This regexp is used to parse the ffmpeg output about the channels number * of an audio stream. */ private static final Pattern CHANNELS_PATTERN = Pattern.compile("(mono|stereo|.*(\\d+).*channels)", Pattern.CASE_INSENSITIVE); /** * The locator of the ffmpeg executable used by this encoder. */ private FFMPEGLocator locator; /** * It builds an encoder using a locator instance to * locate the ffmpeg executable to use. */ public Encoder() { for (FFMPEGLocator loc : locators) { if (loc.pickMe()) { this.locator = loc; } } if (this.locator == null) { throw new Error("Could not find an ffmpeg locator for this operating system."); } } /** * Returns a set informations about a multimedia file, if its format is * supported for decoding. * * @param source * The source multimedia file. * @return A set of informations about the file and its contents. * @throws InputFormatException * If the format of the source file cannot be recognized and * decoded. * @throws EncoderException * If a problem occurs calling the underlying ffmpeg executable. */ public Attributes getInfo(File source) throws InputFormatException, EncoderException { FFMPEGExecutor ffmpeg = locator.createExecutor(); ffmpeg.addArgument("-i"); ffmpeg.addFileArgument(source.getAbsolutePath()); try { String out = ffmpeg.execute(); return parseAudioAttributes(source, out); } catch (IOException e) { throw new EncoderException(e); } } /** * Private utility. It parses the ffmpeg output, extracting informations * about a source multimedia file. * * @param source * The source multimedia file. * @param reader * The ffmpeg output channel. * @return A set of informations about the source multimedia file and its * contents. * @throws InputFormatException * If the format of the source file cannot be recognized and * decoded. * @throws EncoderException * If a problem occurs calling the underlying ffmpeg executable. */ private Attributes parseAudioAttributes(File source,String contents) throws InputFormatException, EncoderException { Pattern p1 = Pattern.compile(".*\\s*Input #0, (\\w+).+$\\s*.*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.UNIX_LINES); Pattern p2 = Pattern.compile(".*\\s*Duration: (\\d\\d):(\\d\\d):(\\d\\d).(\\d\\d),", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.UNIX_LINES); Pattern p3 = Pattern.compile(".*\\s*Stream #\\S+: ((?:Audio)|(?:Video)|(?:Data)): (.*)\\s*.*", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.UNIX_LINES); boolean noMatch = true; Attributes info = new Attributes(); Matcher m = p1.matcher(contents); if (m.find()) { noMatch = false; String format = m.group(1); info.setFormat(format); } m = p2.matcher(contents); if (m.find()) { noMatch = false; long hours = Integer.parseInt(m.group(1)); long minutes = Integer.parseInt(m.group(2)); long seconds = Integer.parseInt(m.group(3)); long centiSeconds = Integer.parseInt(m.group(4)); long duration = centiSeconds * 10L + (seconds + minutes * 60L + hours * 60L * 60L) * 1000L; info.setDuration(duration); } m = p3.matcher(contents); if (m.find()) { noMatch = false; String type = m.group(1); String specs = m.group(2); if ("Audio".equalsIgnoreCase(type)) { StringTokenizer st = new StringTokenizer(specs, ","); for (int i = 0; st.hasMoreTokens(); i++) { String token = st.nextToken().trim(); if (i == 0) { info.setFormat(token); } else { boolean parsed = false; // Sampling rate. Matcher m2 = SAMPLING_RATE_PATTERN.matcher(token); if (!parsed && m2.find()) { int samplingRate = Integer.parseInt(m2.group(1)); info.setSamplingRate(samplingRate); parsed = true; } // Channels. m2 = CHANNELS_PATTERN.matcher(token); if (!parsed && m2.find()) { String ms = m2.group(1); if ("mono".equalsIgnoreCase(ms)) { info.setChannels(1); } else if ("stereo".equalsIgnoreCase(ms)) { info.setChannels(2); } else { info.setChannels(Integer.valueOf(m2.group(2))); } parsed = true; } // Bit rate. m2 = BIT_RATE_PATTERN.matcher(token); if (!parsed && m2.find()) { int bitRate = Integer.parseInt(m2.group(1)); info.setBitRate(bitRate); parsed = true; } } } } } if (noMatch) { throw new InputFormatException(); } return info; } /** * Re-encode a multimedia file. * * @param source * The source multimedia file. It cannot be null. Be sure this * file can be decoded. * @param target * The target multimedia re-encoded file. It cannot be null. If * this file already exists, it will be overwrited. * @param attributes * A set of attributes for the attributes process. * @throws IllegalArgumentException * If both audio and video parameters are null. * * @throws EncoderException * If a problems occurs during the attributes process. */ public void encode(File source, File target, Attributes attributes) throws EncoderException { if (attributes == null) { throw new IllegalArgumentException("Audio attributes are null"); } target = target.getAbsoluteFile(); target.getParentFile().mkdirs(); FFMPEGExecutor ffmpeg = construcExecutor(attributes, source.getAbsolutePath()); //add output file ffmpeg.addArgument("-y"); ffmpeg.addFileArgument(target.getAbsolutePath()); try { String out = ffmpeg.execute(); LOG.fine(out); } catch (IOException e) { throw new EncoderException(e); } if (target.length() == 0) { throw new EncoderException(String.format( "The size of the target (%s) is zero bytes, something went wrong.", target.getAbsolutePath())); } else { long sourceDuration = getInfo(source).getDuration(); long targetDuration = getInfo(target).getDuration(); if (targetDuration > 0 && sourceDuration > 0 && Math.abs(sourceDuration - targetDuration) > 3000) { throw new EncoderException( String.format( "Source and target should have similar duration (source %s duration: %s ms, target %s duration: %s ms).", source.getAbsolutePath(), sourceDuration, target.getAbsolutePath(), targetDuration)); } } } public AudioInputStream stream(String source, Attributes attributes) throws EncoderException { if (attributes == null) { throw new IllegalArgumentException("Audio attributes are null"); } if(!attributes.getFormat().equalsIgnoreCase("wav")){ throw new IllegalArgumentException("Streaming only supports the wav format, not " + attributes.getFormat()); } //Create an ffmpeg executor FFMPEGExecutor ffmpeg = construcExecutor(attributes, source); //Pipe the output to stdout ffmpeg.addArgument("pipe:1"); //The command to execute is this: LOG.fine("Will pipe stream output using the following command:"); LOG.fine(ffmpeg.toString()); return ffmpeg.pipe(attributes); } private FFMPEGExecutor construcExecutor(Attributes attributes,String source){ FFMPEGExecutor ffmpeg = locator.createExecutor(); Integer seekTime = attributes.getSeekTime(); if (seekTime != null) { ffmpeg.addArgument("-ss"); int S = seekTime; //ms int s = S/1000; //sec int m = s/60; //min int h = m/60; //hr ffmpeg.addArgument(String .format("%2d:%2d:%2d.%3d", h%100, m%60, s%60, S%1000) .replaceAll(" ","0")); } ffmpeg.addArgument("-i"); ffmpeg.addArgument(source); // no video ffmpeg.addArgument("-vn"); String codec = attributes.getCodec(); if (codec != null) { ffmpeg.addArgument("-acodec"); ffmpeg.addArgument(codec); } Integer bitRate = attributes.getBitRate(); if (bitRate != null) { ffmpeg.addArgument("-ab"); ffmpeg.addArgument(String.valueOf(bitRate.intValue())); } Integer channels = attributes.getChannels(); if (channels != null) { ffmpeg.addArgument("-ac"); ffmpeg.addArgument(String.valueOf(channels.intValue())); } Integer samplingRate = attributes.getSamplingRate(); if (samplingRate != null) { ffmpeg.addArgument("-ar"); ffmpeg.addArgument(String.valueOf(samplingRate.intValue())); } Integer volume = attributes.getVolume(); if (volume != null) { ffmpeg.addArgument("-vol"); ffmpeg.addArgument(String.valueOf(volume.intValue())); } ffmpeg.addArgument("-f"); ffmpeg.addArgument(attributes.getFormat()); return ffmpeg; } /** * Constructs the target audio format. The audio format is one channel * signed PCM of a given sample rate. * * @param attributes * The audio format (sample rate, format, bit depth, channels,...) to convert to. * @return The audio format after conversion. */ public static AudioFormat getTargetAudioFormat(Attributes attributes) { AudioFormat audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, attributes.getSamplingRate(), 16, attributes.getChannels(), 2 * attributes.getChannels(), attributes.getSamplingRate(), ByteOrder.BIG_ENDIAN.equals(ByteOrder.nativeOrder())); return audioFormat; } }