All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ws.schild.jave.Encoder Maven / Gradle / Ivy

Go to download

The JAVE (Java Audio Video Encoder) library is Java wrapper on the ffmpeg project. Developers can take take advantage of JAVE2 to transcode audio and video files from a format to another. In example you can transcode an AVI file to a MPEG one, you can change a DivX video stream into a (youtube like) Flash FLV one, you can convert a WAV audio file to a MP3 or a Ogg Vorbis one, you can separate and transcode audio and video tracks, you can resize videos, changing their sizes and proportions and so on. Many other formats, containers and operations are supported by JAVE2.

There is a newer version: 3.5.0
Show newest version
/*
 * 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 .
 */
package ws.schild.jave;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ws.schild.jave.encode.ArgType;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingArgument;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.PredicateArgument;
import ws.schild.jave.encode.SimpleArgument;
import ws.schild.jave.encode.ValueArgument;
import ws.schild.jave.encode.VideoAttributes;
import ws.schild.jave.encode.enums.VsyncMethod;
import ws.schild.jave.encode.enums.X264_PROFILE;
import ws.schild.jave.filters.FilterGraph;
import ws.schild.jave.info.MultimediaInfo;
import ws.schild.jave.info.VideoSize;
import ws.schild.jave.process.ProcessLocator;
import ws.schild.jave.process.ProcessWrapper;
import ws.schild.jave.process.ffmpeg.DefaultFFMPEGLocator;
import ws.schild.jave.progress.EncoderProgressListener;
import ws.schild.jave.utils.RBufferedReader;

/**
 * Main class of the package. Instances can encode audio and video streams.
 *
 * @author Carlo Pelliccia
 */
public class Encoder {

  private static final Logger LOG = LoggerFactory.getLogger(Encoder.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*([AVS]).{5}\\s(\\S+).(.+)$", 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 ProcessLocator locator;

  /**
   * The executor used to do the conversion Is saved here, so we can abort the conversion process
   */
  private ProcessWrapper ffmpeg;

  /** List of unhandled messages from ffmpeng run */
  private List unhandledMessages = null;

  /**
   * It builds an encoder using a {@link DefaultFFMPEGLocator} instance to locate the ffmpeg
   * executable to use.
   */
  public Encoder() {
    this.locator = new DefaultFFMPEGLocator();
  }

  /**
   * It builds an encoder with a custom {@link ws.schild.jave.process.ffmpeg.FFMPEGProcess}.
   *
   * @param locator The locator picking up the ffmpeg executable used by the encoder.
   */
  public Encoder(ProcessLocator 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 {
    return getCoders(false, true);
  }

  /**
   * 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 {
    return getCoders(true, true);
  }

  /**
   * Returns a list with the names of all the coders bundled with the ffmpeg distribution in use.
   *
   * @param encoder Do search encoders, else decoders
   * @param audio Do search for audio encodes, else video
   * @return A list with the names of all the included encoders
   * @throws EncoderException If a problem occurs calling the underlying ffmpeg executable.
   */
  protected String[] getCoders(boolean encoder, boolean audio) throws EncoderException {
    ArrayList res = new ArrayList<>();
    ProcessWrapper localFFMPEG = locator.createExecutor();
    localFFMPEG.addArgument(encoder ? "-encoders" : "-decoders");
    try {
      localFFMPEG.execute();
      RBufferedReader reader =
          new RBufferedReader(new InputStreamReader(localFFMPEG.getInputStream()));
      String line;
      String format = audio ? "A" : "V";
      boolean headerFound = false;
      boolean evaluateLine = false;
      while ((line = reader.readLine()) != null) {
        if (line.trim().length() == 0) {
          continue;
        }
        if (headerFound) {
          if (evaluateLine) {
            Matcher matcher = ENCODER_DECODER_PATTERN.matcher(line);
            if (matcher.matches()) {
              // String encoderFlag = matcher.group(2);
              String audioVideoFlag = matcher.group(1);
              if (format.equals(audioVideoFlag)) {
                String name = matcher.group(2);
                res.add(name);
              }
            } else {
              break;
            }
          } else {
            evaluateLine = line.trim().equals("------");
          }
        } else if (line.trim().equals(encoder ? "Encoders:" : "Decoders:")) {
          headerFound = true;
        }
      }
    } catch (IOException e) {
      throw new EncoderException(e);
    } finally {
      localFFMPEG.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 {
    return getCoders(false, false);
  }

  /**
   * 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 {
    return getCoders(true, false);
  }

  /**
   * 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 {
    return getSupportedCodingFormats(true);
  }

  /**
   * Returns a list with the names of all the file formats supported at en/de-coding time by the
   * underlying ffmpeg distribution.A multimedia file could be encoded and generated only if the
   * specified format is in this list.
   *
   * @param encoding True for encoding job, false to decode a file
   * @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.
   */
  /*
   * TODO: Refactor this out to a parsing utility. This will enable us to support multiple ffmpeg
   * versions if the structure changes.
   */
  protected String[] getSupportedCodingFormats(boolean encoding) throws EncoderException {
    ArrayList res = new ArrayList<>();

    try (ProcessWrapper localFFMPEG = locator.createExecutor()) {
      localFFMPEG.addArgument("-formats");

      localFFMPEG.execute();
      RBufferedReader reader =
          new RBufferedReader(new InputStreamReader(localFFMPEG.getInputStream()));
      String line;
      String ed = encoding ? "E" : "D";
      boolean headerFound = false;
      boolean evaluateLine = false;
      while ((line = reader.readLine()) != null) {
        if (line.trim().length() == 0) {
          continue;
        }
        if (headerFound) {
          if (evaluateLine) {
            Matcher matcher = FORMAT_PATTERN.matcher(line);
            if (matcher.matches()) {
              String encoderFlag = matcher.group(encoding ? 2 : 1);
              if (ed.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 {
            evaluateLine = line.trim().equals("--");
          }
        } else if (line.trim().equals("File formats:")) {
          headerFound = true;
        }
      }
    } catch (IOException e) {
      throw new EncoderException(e);
    }

    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 {
    return getSupportedCodingFormats(false);
  }

  /**
   * Re-encode a multimedia file(s).
   *
   * 

This method is not reentrant, instead create multiple object instances * * @param multimediaObject The source multimedia file. It cannot be null. Be sure this file can be * decoded (see null null null null {@link Encoder#getSupportedDecodingFormats()}, {@link * Encoder#getAudioDecoders()} and {@link Encoder#getVideoDecoders()}). When passing multiple * sources, make sure that they are compatible in the way that ffmpeg can concat them. We * don't use the complex filter at the moment Perhaps you will need to first transcode/resize * them https://trac.ffmpeg.org/wiki/Concatenate @see "Concat protocol" * @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); } public void encode( List multimediaObjects, File target, EncodingAttributes attributes) throws IllegalArgumentException, InputFormatException, EncoderException { encode(multimediaObjects, target, attributes, null); } /** * Re-encode a multimedia file. * *

This method is not reentrant, instead create multiple object instances * * @param multimediaObject The source multimedia file. It cannot be null. Be sure this file can be * decoded (see null null null null {@link Encoder#getSupportedDecodingFormats()}, {@link * Encoder#getAudioDecoders()} and {@link Encoder#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 { List src = new ArrayList<>(); src.add(multimediaObject); encode(src, target, attributes, listener); } private static List globalOptions = new ArrayList(Arrays.asList( new ValueArgument(ArgType.GLOBAL, "--filter_thread", ea -> ea.getFilterThreads().map(Object::toString)), new ValueArgument(ArgType.GLOBAL, "-ss", ea -> ea.getOffset().map(Object::toString)), new ValueArgument(ArgType.INFILE, "-threads", ea -> ea.getDecodingThreads().map(Object::toString)), new PredicateArgument(ArgType.INFILE, "-loop", "1", ea -> ea.getLoop() && ea.getDuration().isPresent()), new ValueArgument(ArgType.INFILE, "-f", ea -> ea.getInputFormat()), new ValueArgument(ArgType.INFILE, "-safe", ea -> ea.getSafe().map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-t", ea -> ea.getDuration().map(Object::toString)), // Video Options new PredicateArgument(ArgType.OUTFILE, "-vn", ea -> !ea.getVideoAttributes().isPresent()), new ValueArgument(ArgType.OUTFILE, "-vcodec", ea -> ea.getVideoAttributes().flatMap(VideoAttributes::getCodec)), new ValueArgument(ArgType.OUTFILE, "-vtag", ea -> ea.getVideoAttributes().flatMap(VideoAttributes::getTag)), new ValueArgument(ArgType.OUTFILE, "-vb", ea -> ea.getVideoAttributes() .flatMap(VideoAttributes::getBitRate) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-r", ea -> ea.getVideoAttributes() .flatMap(VideoAttributes::getFrameRate) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-s", ea -> ea.getVideoAttributes() .flatMap(VideoAttributes::getSize) .map(VideoSize::asEncoderArgument)), new PredicateArgument(ArgType.OUTFILE, "-movflags", "faststart", ea -> ea.getVideoAttributes().isPresent()), new ValueArgument(ArgType.OUTFILE, "-profile:v", ea -> ea.getVideoAttributes() .flatMap(VideoAttributes::getX264Profile) .map(X264_PROFILE::getModeName)), new SimpleArgument(ArgType.OUTFILE, ea -> ea.getVideoAttributes() .map(VideoAttributes::getVideoFilters) .map(Collection::stream) .map(s -> s.flatMap(vf -> Stream.of("-vf", vf.getExpression()))) .orElseGet(Stream::empty)), new ValueArgument(ArgType.OUTFILE, "-filter_complex", ea -> ea.getVideoAttributes() .flatMap(VideoAttributes::getComplexFiltergraph) .map(FilterGraph::getExpression)), new ValueArgument(ArgType.OUTFILE, "-qscale:v", ea -> ea.getVideoAttributes() .flatMap(VideoAttributes::getQuality) .map(Object::toString)), // Audio Options new PredicateArgument(ArgType.OUTFILE, "-an", ea -> !ea.getAudioAttributes().isPresent()), new ValueArgument(ArgType.OUTFILE, "-acodec", ea -> ea.getAudioAttributes().flatMap(AudioAttributes::getCodec)), new ValueArgument(ArgType.OUTFILE, "-ab", ea -> ea.getAudioAttributes() .flatMap(AudioAttributes::getBitRate) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-ac", ea -> ea.getAudioAttributes() .flatMap(AudioAttributes::getChannels) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-ar", ea -> ea.getAudioAttributes() .flatMap(AudioAttributes::getSamplingRate) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-vol", ea -> ea.getAudioAttributes() .flatMap(AudioAttributes::getVolume) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-qscale:a", ea -> ea.getAudioAttributes() .flatMap(AudioAttributes::getQuality) .map(Object::toString)), new ValueArgument(ArgType.OUTFILE, "-f", ea -> ea.getOutputFormat()), new ValueArgument(ArgType.OUTFILE, "-threads", ea -> ea.getEncodingThreads().map(Object::toString)), new PredicateArgument(ArgType.OUTFILE, "-map_metadata", "0", ea -> ea.isMapMetaData()), new ValueArgument(ArgType.OUTFILE, "-pix_fmt", ea -> ea.getVideoAttributes().flatMap(VideoAttributes::getPixelFormat)), new ValueArgument(ArgType.OUTFILE, "-vsync", ea -> ea.getVideoAttributes().flatMap(VideoAttributes::getVsync).map(VsyncMethod::getMethodName)) ) ); public static void addOptionAtIndex(EncodingArgument arg, Integer index) { globalOptions.add(index, arg); } /** * Re-encode a multimedia file(s). * *

This method is not reentrant, instead create multiple object instances * * @param multimediaObjects The source multimedia files. It cannot be null. Be sure this file can * be decoded (see null null null null {@link Encoder#getSupportedDecodingFormats()}, {@link * Encoder#getAudioDecoders()} and* {@link Encoder#getVideoDecoders()}) When passing multiple * sources, make sure that they are compatible in the way that ffmpeg can concat them. We * don't use the complex filter at the moment Perhaps you will need to first transcode/resize * them https://trac.ffmpeg.org/wiki/Concatenate @see "Concat protocol" * @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( List multimediaObjects, File target, EncodingAttributes attributes, EncoderProgressListener listener) throws IllegalArgumentException, InputFormatException, EncoderException { attributes.validate(); target = target.getAbsoluteFile(); target.getParentFile().mkdirs(); ffmpeg = locator.createExecutor(); // Set global options globalOptions .stream() .filter(ea -> ArgType.GLOBAL.equals(ea.getArgType())) .flatMap(eArg -> eArg.getArguments(attributes)) .forEach(ffmpeg::addArgument); // Set input options, must be before -i argument globalOptions .stream() .filter(ea -> ArgType.INFILE.equals(ea.getArgType())) .flatMap(eArg -> eArg.getArguments(attributes)) .forEach(ffmpeg::addArgument); multimediaObjects .stream() .map(Object::toString) .flatMap(mmo -> Stream.of("-i", mmo)) .forEach(ffmpeg::addArgument); // Set output options. Must be after the -i and before the outfile target globalOptions .stream() .filter(ea -> ArgType.OUTFILE.equals(ea.getArgType())) .flatMap(eArg -> eArg.getArguments(attributes)) .forEach(ffmpeg::addArgument); ffmpeg.addArgument("-y"); ffmpeg.addArgument(target.getAbsolutePath()); try { ffmpeg.execute(); } catch (IOException e) { throw new EncoderException(e); } try { String lastWarning = null; long duration = 0; MultimediaInfo info = null; /* * TODO: This is an awkward way of determining duration of input videos. This calls a separate * FFMPEG process to getInfo when the output of running FFMPEG just above will list the info * of the input videos as "Input #0" -> "Input #N". Capture _that_ output instead of calling * *back* into FFMPEG. Furthermore, expressing the percentage of the transcoding job as a * simple "what percentage of the input duration have we output" feels too naive given all of * the interesting video filters that can be applied. It feels like the user would know the * duration of the output video as: * 1. The duration of the input video (as we have expressed here) * 2. The sum of the durations of the input videos * 3. A particular duration calculated with the context of all the inputs/encoding attributes. * So, if the calling method tells this method the expected duration, then we can express * progress as a percentage. I would like to make #1 and #2 very simple to do, however. * Perhaps a method that would take the input MultimediaInfo objects that are generated from * this FFMPEG invocation, the EncodingAttributes, and would output a duration. Then we could * have named methods that would calculate durations as in #1 and #2. */ if (multimediaObjects.size() == 1 && (!multimediaObjects.get(0).isURL() || !multimediaObjects.get(0).isReadURLOnce())) { info = multimediaObjects.get(0).getInfo(); } Float offsetAttribute = attributes.getOffset().orElse(null); Float durationAttribute = attributes.getDuration().orElse(null); if (durationAttribute != null) { duration = (long) Math.round((durationAttribute * 1000L)); } else { if (info != null) { duration = info.getDuration(); if (offsetAttribute != null) { duration -= (long) Math.round((offsetAttribute * 1000L)); } } } if (listener != null) { listener.sourceInfo(info); } String line; ConversionOutputAnalyzer outputAnalyzer = new ConversionOutputAnalyzer(duration, listener); RBufferedReader reader = new RBufferedReader(new InputStreamReader(ffmpeg.getErrorStream())); while ((line = reader.readLine()) != null) { outputAnalyzer.analyzeNewLine(line); } if (outputAnalyzer.getLastWarning() != null) { if (!SUCCESS_PATTERN.matcher(lastWarning).matches()) { throw new EncoderException("No match for: " + SUCCESS_PATTERN + " in " + lastWarning); } } /* * TODO: This is not thread safe. This needs to be a resulting value from the call to the * Encoder. We can create a separate EncoderResult, but not a stateful variable. */ unhandledMessages = outputAnalyzer.getUnhandledMessages(); int exitCode = ffmpeg.getProcessExitCode(); if (exitCode != 0) { LOG.error("Process exit code: {} to {}", exitCode, target.getName()); throw new EncoderException("Exit code of ffmpeg encoding run is " + exitCode); } } catch (IOException e) { throw new EncoderException(e); } finally { if (ffmpeg != null) { ffmpeg.destroy(); } ffmpeg = null; } } /** * Return the list of unhandled output messages of the ffmpeng encoder run * * @return the unhandledMessages list of unhandled messages, can be null or empty */ public List getUnhandledMessages() { return unhandledMessages; } /** Force the encoding process to stop */ public void abortEncoding() { if (ffmpeg != null) { ffmpeg.destroy(); ffmpeg = null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy