
mtons.media.core.encoder.impl.FFMPEGEncoderImpl Maven / Gradle / Ivy
The newest version!
package mtons.media.core.encoder.impl;
import mtons.media.attribute.*;
import mtons.media.core.DefaultFFMPEGLocator;
import mtons.media.core.FFMPEGExecutor;
import mtons.media.core.FFMPEGLocator;
import mtons.media.core.RBufferedReader;
import mtons.media.core.encoder.Encoder;
import mtons.media.exception.EncoderException;
import mtons.media.exception.InputFormatException;
import mtons.media.progress.EncoderProgressListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Main class of the package. Instances can encode audio and video streams.
*/
public class FFMPEGEncoderImpl implements Encoder {
private static Log _log = LogFactory.getLog(FFMPEGEncoderImpl.class);
/**
* This regexp is used to parse the ffmpeg output about the supported formats.
*/
private static final Pattern FORMAT_PATTERN = Pattern.compile("^\\s*([D ])([E ])\\s+([\\w,]+)\\s+.+$");
/**
* This regexp is used to parse the ffmpeg output about the included encoders/decoders.
*/
private static final Pattern ENCODER_DECODER_PATTERN = Pattern.compile("^\\s*([D ])([E ])([AVS]).{3}\\s+(.+)$", Pattern.CASE_INSENSITIVE);
/**
* This regexp is used to parse the ffmpeg output about the ongoing encoding process.
*/
private static final Pattern PROGRESS_INFO_PATTERN = Pattern.compile("\\s*(\\w+)\\s*=\\s*(\\S+)\\s*", Pattern.CASE_INSENSITIVE);
/**
* This regexp is used to parse the ffmpeg output about the size of a video stream.
*/
private static final Pattern SIZE_PATTERN = Pattern.compile("(\\d+)x(\\d+)", Pattern.CASE_INSENSITIVE);
/**
* This regexp is used to parse the ffmpeg output about the frame rate value of a video stream.
*/
private static final Pattern FRAME_RATE_PATTERN = Pattern.compile("([\\d.]+)\\s+(?:fps|tbr)", Pattern.CASE_INSENSITIVE);
/**
* 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)", Pattern.CASE_INSENSITIVE);
/**
* This regexp is used to parse the ffmpeg output about the success of an encoding operation.
*/
private static final Pattern SUCCESS_PATTERN = Pattern.compile("^\\s*video\\:\\S+\\s+audio\\:\\S+\\s+subtitle\\:\\S+\\s+global headers\\:\\S+.*$",
Pattern.CASE_INSENSITIVE);
/**
* The locator of the ffmpeg executable used by this encoder.
*/
private final FFMPEGLocator locator;
/**
* It builds an encoder using a {@link DefaultFFMPEGLocator} instance to
* locate the ffmpeg executable to use.
*/
public FFMPEGEncoderImpl() {
this.locator = new DefaultFFMPEGLocator();
}
/**
* It builds an encoder with a custom {@link FFMPEGLocator}.
*
* @param locator The locator picking up the ffmpeg executable used by the encoder.
*/
public FFMPEGEncoderImpl(FFMPEGLocator locator) {
this.locator = locator;
}
/**
* Returns a list with the names of all the audio decoders bundled with the
* ffmpeg distribution in use. An audio stream can be decoded only if a
* decoder for its format is available.
*
* @return A list with the names of all the included audio decoders.
* @throws EncoderException If a problem occurs calling the underlying
* ffmpeg executable.
*/
public String[] getAudioDecoders() throws EncoderException {
ArrayList res = new ArrayList<>();
FFMPEGExecutor ffmpeg = locator.createExecutor();
ffmpeg.addArgument("-formats");
try {
ffmpeg.execute();
RBufferedReader reader = null;
reader = new RBufferedReader(new InputStreamReader(ffmpeg.getInputStream()));
String line;
boolean evaluate = false;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (evaluate) {
Matcher matcher = ENCODER_DECODER_PATTERN.matcher(line);
if (matcher.matches()) {
String decoderFlag = matcher.group(1);
String audioVideoFlag = matcher.group(3);
if ("D".equals(decoderFlag) && "A".equals(audioVideoFlag)) {
String name = matcher.group(4);
res.add(name);
}
} else {
break;
}
} else if (line.trim().equals("Codecs:")) {
evaluate = true;
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
int size = res.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = res.get(i);
}
return ret;
}
/**
* Returns a list with the names of all the audio encoders bundled with the
* ffmpeg distribution in use. An audio stream can be encoded using one of
* these encoders.
*
* @return A list with the names of all the included audio encoders.
* @throws EncoderException If a problem occurs calling the underlying
* ffmpeg executable.
*/
public String[] getAudioEncoders() throws EncoderException {
ArrayList res = new ArrayList<>();
FFMPEGExecutor ffmpeg = locator.createExecutor();
ffmpeg.addArgument("-formats");
try {
ffmpeg.execute();
RBufferedReader reader = null;
reader = new RBufferedReader(new InputStreamReader(ffmpeg.getInputStream()));
String line;
boolean evaluate = false;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (evaluate) {
Matcher matcher = ENCODER_DECODER_PATTERN.matcher(line);
if (matcher.matches()) {
String encoderFlag = matcher.group(2);
String audioVideoFlag = matcher.group(3);
if ("E".equals(encoderFlag) && "A".equals(audioVideoFlag)) {
String name = matcher.group(4);
res.add(name);
}
} else {
break;
}
} else if (line.trim().equals("Codecs:")) {
evaluate = true;
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
int size = res.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = res.get(i);
}
return ret;
}
/**
* Returns a list with the names of all the video decoders bundled with the
* ffmpeg distribution in use. A video stream can be decoded only if a
* decoder for its format is available.
*
* @return A list with the names of all the included video decoders.
* @throws EncoderException If a problem occurs calling the underlying
* ffmpeg executable.
*/
public String[] getVideoDecoders() throws EncoderException {
ArrayList res = new ArrayList<>();
FFMPEGExecutor ffmpeg = locator.createExecutor();
ffmpeg.addArgument("-formats");
try {
ffmpeg.execute();
RBufferedReader reader = null;
reader = new RBufferedReader(new InputStreamReader(ffmpeg.getInputStream()));
String line;
boolean evaluate = false;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (evaluate) {
Matcher matcher = ENCODER_DECODER_PATTERN.matcher(line);
if (matcher.matches()) {
String decoderFlag = matcher.group(1);
String audioVideoFlag = matcher.group(3);
if ("D".equals(decoderFlag) && "V".equals(audioVideoFlag)) {
String name = matcher.group(4);
res.add(name);
}
} else {
break;
}
} else if (line.trim().equals("Codecs:")) {
evaluate = true;
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
int size = res.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = res.get(i);
}
return ret;
}
/**
* Returns a list with the names of all the video encoders bundled with the
* ffmpeg distribution in use. A video stream can be encoded using one of
* these encoders.
*
* @return A list with the names of all the included video encoders.
* @throws EncoderException If a problem occurs calling the underlying
* ffmpeg executable.
*/
public String[] getVideoEncoders() throws EncoderException {
ArrayList res = new ArrayList<>();
FFMPEGExecutor ffmpeg = locator.createExecutor();
ffmpeg.addArgument("-formats");
try {
ffmpeg.execute();
RBufferedReader reader = null;
reader = new RBufferedReader(new InputStreamReader(ffmpeg.getInputStream()));
String line;
boolean evaluate = false;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (evaluate) {
Matcher matcher = ENCODER_DECODER_PATTERN.matcher(line);
if (matcher.matches()) {
String encoderFlag = matcher.group(2);
String audioVideoFlag = matcher.group(3);
if ("E".equals(encoderFlag) && "V".equals(audioVideoFlag)) {
String name = matcher.group(4);
res.add(name);
}
} else {
break;
}
} else if (line.trim().equals("Codecs:")) {
evaluate = true;
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
int size = res.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = res.get(i);
}
return ret;
}
/**
* Returns a list with the names of all the file formats supported at
* encoding time by the underlying ffmpeg distribution. A multimedia file
* could be encoded and generated only if the specified format is in this
* list.
*
* @return A list with the names of all the supported file formats at
* encoding time.
* @throws EncoderException If a problem occurs calling the underlying
* ffmpeg executable.
*/
public String[] getSupportedEncodingFormats() throws EncoderException {
ArrayList res = new ArrayList<>();
FFMPEGExecutor ffmpeg = locator.createExecutor();
ffmpeg.addArgument("-formats");
try {
ffmpeg.execute();
RBufferedReader reader = null;
reader = new RBufferedReader(new InputStreamReader(ffmpeg.getInputStream()));
String line;
boolean evaluate = false;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (evaluate) {
Matcher matcher = FORMAT_PATTERN.matcher(line);
if (matcher.matches()) {
String encoderFlag = matcher.group(2);
if ("E".equals(encoderFlag)) {
String aux = matcher.group(3);
StringTokenizer st = new StringTokenizer(aux, ",");
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
if (!res.contains(token)) {
res.add(token);
}
}
}
} else {
break;
}
} else if (line.trim().equals("File formats:")) {
evaluate = true;
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
int size = res.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = res.get(i);
}
return ret;
}
/**
* Returns a list with the names of all the file formats supported at
* decoding time by the underlying ffmpeg distribution. A multimedia file
* could be open and decoded only if its format is in this list.
*
* @return A list with the names of all the supported file formats at
* decoding time.
* @throws EncoderException If a problem occurs calling the underlying
* ffmpeg executable.
*/
public String[] getSupportedDecodingFormats() throws EncoderException {
ArrayList res = new ArrayList<>();
FFMPEGExecutor ffmpeg = locator.createExecutor();
ffmpeg.addArgument("-formats");
try {
ffmpeg.execute();
RBufferedReader reader = null;
reader = new RBufferedReader(new InputStreamReader(ffmpeg.getInputStream()));
String line;
boolean evaluate = false;
while ((line = reader.readLine()) != null) {
if (line.trim().length() == 0) {
continue;
}
if (evaluate) {
Matcher matcher = FORMAT_PATTERN.matcher(line);
if (matcher.matches()) {
String decoderFlag = matcher.group(1);
if ("D".equals(decoderFlag)) {
String aux = matcher.group(3);
StringTokenizer st = new StringTokenizer(aux, ",");
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
if (!res.contains(token)) {
res.add(token);
}
}
}
} else {
break;
}
} else if (line.trim().equals("File formats:")) {
evaluate = true;
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
int size = res.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = res.get(i);
}
return ret;
}
/**
* Private utility. Parse a line and try to match its contents against the
* {@link FFMPEGEncoderImpl#PROGRESS_INFO_PATTERN} pattern. It the line can be parsed,
* it returns a hashtable with progress informations, otherwise it returns
* null.
*
* @param line The line from the ffmpeg output.
* @return A hashtable with the value reported in the line, or null if the
* given line can not be parsed.
*/
private HashMap parseProgressInfoLine(String line) {
HashMap table = null;
Matcher m = PROGRESS_INFO_PATTERN.matcher(line);
while (m.find()) {
if (table == null) {
table = new HashMap<>();
}
String key = m.group(1);
String value = m.group(2);
table.put(key, value);
}
return table;
}
/**
* Re-encode a multimedia file.
*
* @param multimediaObject The source multimedia file. It cannot be null. Be sure this
* file can be decoded (see {@link FFMPEGEncoderImpl#getSupportedDecodingFormats()},
* {@link FFMPEGEncoderImpl#getAudioDecoders()} and
* {@link FFMPEGEncoderImpl#getVideoDecoders()}).
* @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 encoding process.
* @throws IllegalArgumentException If both audio and video parameters are
* null.
* @throws InputFormatException If the source multimedia file cannot be
* decoded.
* @throws EncoderException If a problems occurs during the encoding
* process.
*/
public void encode(MultimediaObject multimediaObject, File target, EncodingAttributes attributes)
throws IllegalArgumentException, InputFormatException,
EncoderException {
encode(multimediaObject, target, attributes, null);
}
/**
* Re-encode a multimedia file.
*
* @param multimediaObject The source multimedia file. It cannot be null. Be sure this
* file can be decoded (see {@link FFMPEGEncoderImpl#getSupportedDecodingFormats()},
* {@link FFMPEGEncoderImpl#getAudioDecoders()} and
* {@link FFMPEGEncoderImpl#getVideoDecoders()}).
* @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 encoding process.
* @param listener An optional progress listener for the encoding process.
* It can be null.
* @throws IllegalArgumentException If both audio and video parameters are
* null.
* @throws InputFormatException If the source multimedia file cannot be
* decoded.
* @throws EncoderException If a problems occurs during the encoding
* process.
*/
public void encode(MultimediaObject multimediaObject, File target, EncodingAttributes attributes,
EncoderProgressListener listener) throws IllegalArgumentException,
InputFormatException, EncoderException {
String formatAttribute = attributes.getFormat();
Float offsetAttribute = attributes.getOffset();
Float durationAttribute = attributes.getDuration();
AudioAttributes audioAttributes = attributes.getAudioAttributes();
VideoAttributes videoAttributes = attributes.getVideoAttributes();
if (audioAttributes == null && videoAttributes == null) {
throw new IllegalArgumentException(
"Both audio and video attributes are null");
}
target = target.getAbsoluteFile();
target.getParentFile().mkdirs();
FFMPEGExecutor ffmpeg = locator.createExecutor();
if (offsetAttribute != null) {
ffmpeg.addArgument("-ss");
ffmpeg.addArgument(String.valueOf(offsetAttribute.floatValue()));
}
ffmpeg.addArgument("-i");
ffmpeg.addArgument(multimediaObject.getFile().getAbsolutePath());
if (durationAttribute != null) {
ffmpeg.addArgument("-t");
ffmpeg.addArgument(String.valueOf(durationAttribute.floatValue()));
}
if (videoAttributes == null) {
ffmpeg.addArgument("-vn");
} else {
String codec = videoAttributes.getCodec();
if (codec != null) {
ffmpeg.addArgument("-vcodec");
ffmpeg.addArgument(codec);
}
String tag = videoAttributes.getTag();
if (tag != null) {
ffmpeg.addArgument("-vtag");
ffmpeg.addArgument(tag);
}
Integer bitRate = videoAttributes.getBitRate();
if (bitRate != null) {
ffmpeg.addArgument("-vb");
ffmpeg.addArgument(String.valueOf(bitRate.intValue()));
}
Integer frameRate = videoAttributes.getFrameRate();
if (frameRate != null) {
ffmpeg.addArgument("-r");
ffmpeg.addArgument(String.valueOf(frameRate.intValue()));
}
VideoSize size = videoAttributes.getSize();
if (size != null) {
ffmpeg.addArgument("-s");
ffmpeg.addArgument(String.valueOf(size.getWidth()) + "x"
+ String.valueOf(size.getHeight()));
}
if (videoAttributes.isFaststart()) {
ffmpeg.addArgument("-movflags");
ffmpeg.addArgument("faststart");
}
if (videoAttributes.getX264Profile() != null) {
ffmpeg.addArgument("-profile:v");
ffmpeg.addArgument(videoAttributes.getX264Profile().getModeName());
}
if (videoAttributes.getVideoFilters().size() > 0) {
for (VideoFilter videoFilter : videoAttributes.getVideoFilters()) {
ffmpeg.addArgument("-vf");
ffmpeg.addArgument(videoFilter.getExpression());
}
}
}
if (audioAttributes == null) {
ffmpeg.addArgument("-an");
} else {
String codec = audioAttributes.getCodec();
if (codec != null) {
if (codec.equals("aac")) {
codec = "libvo_aacenc";
}
ffmpeg.addArgument("-acodec");
ffmpeg.addArgument(codec);
}
Integer bitRate = audioAttributes.getBitRate();
if (bitRate != null) {
ffmpeg.addArgument("-ab");
ffmpeg.addArgument(String.valueOf(bitRate.intValue()));
}
Integer channels = audioAttributes.getChannels();
if (channels != null) {
ffmpeg.addArgument("-ac");
ffmpeg.addArgument(String.valueOf(channels.intValue()));
}
Integer samplingRate = audioAttributes.getSamplingRate();
if (samplingRate != null) {
ffmpeg.addArgument("-ar");
ffmpeg.addArgument(String.valueOf(samplingRate.intValue()));
}
Integer volume = audioAttributes.getVolume();
if (volume != null) {
ffmpeg.addArgument("-vol");
ffmpeg.addArgument(String.valueOf(volume.intValue()));
}
}
if (formatAttribute != null) {
ffmpeg.addArgument("-f");
ffmpeg.addArgument(formatAttribute);
}
ffmpeg.addArgument("-y");
ffmpeg.addArgument(target.getAbsolutePath());
try {
ffmpeg.execute();
} catch (IOException e) {
throw new EncoderException(e);
}
try {
String lastWarning = null;
long duration;
long progress = 0;
RBufferedReader reader = new RBufferedReader(
new InputStreamReader(ffmpeg.getErrorStream()));
MultimediaInfo info = multimediaObject.getInfo();
if (durationAttribute != null) {
duration = (long) Math
.round((durationAttribute * 1000L));
} else {
duration = info.getDuration();
if (offsetAttribute != null) {
duration -= (long) Math
.round((offsetAttribute * 1000L));
}
}
if (listener != null) {
listener.sourceInfo(info);
}
int step = 0;
int lineNR = 0;
String line;
while ((line = reader.readLine()) != null) {
lineNR++;
if (_log.isDebugEnabled()) {
_log.debug("Input Line (" + lineNR + "): " + line);
}
if (step == 0) {
if (line.startsWith("WARNING: ")) {
if (listener != null) {
listener.message(line);
}
} else if (!line.startsWith("Output #0")) {
// throw new EncoderException(line);
} else {
step++;
}
}
if (step == 1) {
if (line.startsWith("WARNING: ")) {
if (listener != null) {
listener.message(line);
}
} else if (!line.startsWith("Output #0")) {
// throw new EncoderException(line);
} else {
step++;
}
} else if (step == 2) {
if (!line.startsWith(" ")) {
step++;
}
}
if (step == 3) {
if (!line.startsWith("Stream mapping:")) {
throw new EncoderException(line);
} else {
step++;
}
} else if (step == 4) {
if (!line.startsWith(" ")) {
step++;
}
}
if (line.startsWith("frame=")) {
try {
line = line.trim();
if (line.length() > 0) {
HashMap table = parseProgressInfoLine(line);
if (table == null) {
if (listener != null) {
listener.message(line);
}
lastWarning = line;
} else {
if (listener != null) {
String time = table.get("time");
if (time != null) {
String dParts[] = time.split(":");
// HH:MM:SS.xx
Double seconds = Double.parseDouble(dParts[dParts.length - 1]);
if (dParts.length > 1) {
seconds += Double.parseDouble(dParts[dParts.length - 2]) * 60;
if (dParts.length > 2) {
seconds += Double.parseDouble(dParts[dParts.length - 3]) * 60 * 60;
}
}
int perm = (int) Math.round((seconds * 1000L * 1000L)
/ (double) duration);
if (perm > 1000) {
perm = 1000;
}
listener.progress(perm);
}
}
lastWarning = null;
}
}
} catch (Exception ex) {
_log.warn("Error in progress parsing for line: " + line);
}
}
}
if (lastWarning != null) {
if (!SUCCESS_PATTERN.matcher(lastWarning).matches()) {
throw new EncoderException("No match for: " + SUCCESS_PATTERN + " in " + lastWarning);
}
}
} catch (IOException e) {
throw new EncoderException(e);
} finally {
ffmpeg.destroy();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy