Skip to content

Custom ffmpeg arguments

André Schild edited this page Aug 20, 2026 · 1 revision

Custom ffmpeg arguments

The typed API covers what most encodings need. ffmpeg has far more than that, and when you need an option this library does not model, there are two ways down a level.

Before reaching for them, check whether the option is already there. EncodingAttributes, AudioAttributes and VideoAttributes between them cover codecs, bit rates, sampling rates, channels, frame rates, sizes, quality, presets, tune, crf, pixel format, frame rate mode and looping, and the filter classes cover scaling, cropping, overlays, text, subtitles and concatenation.

Adding an argument to the global list

Encoder builds every command line from one list of arguments. You can add to it, and what you add applies to every encoding from then on, in the whole JVM.

// -movflags +faststart, so an mp4 can start playing before it has fully downloaded
Encoder.addOptionAtIndex(
    new ValueArgument(ArgType.OUTFILE, "-movflags", ea -> Optional.of("+faststart")),
    10);

The pieces:

  • ArgType decides where the argument lands. GLOBAL before everything, INFILE before -i, OUTFILE after the inputs and before the target file. Getting this wrong is the usual reason an option is ignored: ffmpeg cares where an option sits relative to the file it applies to.
  • ValueArgument emits a flag and a value, and emits nothing when the function returns an empty Optional.
  • PredicateArgument emits fixed text, one word or two, when a condition over the EncodingAttributes holds. SimpleArgument emits fixed text unconditionally.
  • The index is where in the list it goes, which decides the order on the command line.

The list can also be read and changed:

EncodingArgument existing = Encoder.getOptionAtIndex(10);
Encoder.setOptionAtIndex(replacement, 10);
Encoder.removeOptionAtIndex(10);

This list is static, shared by every Encoder in the JVM. Changing it from one thread while another is encoding is safe as of 4.0.0, the list is copy on write, but the change still affects everybody. Set it up once at startup rather than per encoding.

Driving ffmpeg directly

For a one-off, or something the encoder is not shaped for at all, use the process wrapper and build the whole command line yourself.

ProcessWrapper ffmpeg = new DefaultFFMPEGLocator().createExecutor();
ffmpeg.addArgument("-i");
ffmpeg.addArgument(source.getAbsolutePath());
ffmpeg.addArgument("-vf");
ffmpeg.addArgument("select='gt(scene,0.4)'");
ffmpeg.addArgument("-fps_mode");
ffmpeg.addArgument("vfr");
ffmpeg.addArgument(target.getAbsolutePath());

try {
    ffmpeg.execute();
    RBufferedReader reader =
        new RBufferedReader(new InputStreamReader(ffmpeg.getErrorStream()));
    String line;
    while ((line = reader.readLine()) != null) {
        // ffmpeg writes its progress and its complaints to stderr
    }
    if (ffmpeg.getProcessExitCode() != 0) {
        // it failed, and the lines above say why
    }
} finally {
    ffmpeg.destroy();
}

Two things to know about doing it this way:

  • Read the error stream. ffmpeg writes to stderr as it works, and a full pipe will stop the process dead, so a run that produces plenty of output will appear to hang if you do not drain it.
  • Always destroy(). A finally block, or use it as a resource, since ProcessWrapper is AutoCloseable.

Which ffmpeg is being used

The bundled binary is extracted to a temporary directory the first time it is needed. To use your own instead, for a build with codecs the bundled one lacks, supply a ProcessLocator:

Encoder encoder = new Encoder(() -> "/usr/local/bin/ffmpeg");

ProcessLocator has a single method, getExecutablePath(), so a lambda returning the path is enough. It builds the ProcessWrapper for you.

Everything else works unchanged against it. Note that some fixes in 4.0.0 exist precisely because ffmpeg changed its command line over the years: -vol was removed in favour of the volume filter, and -vsync in favour of -fps_mode. The library asks the executable in front of it which of those it accepts, so a custom binary of any vintage is handled.

A word on where the arguments come from

addArgument adds whatever it is given, which is what makes this page possible. It is also why an application must never hand it a string a user controls: that is giving the user the ffmpeg command line. The same goes for URLs, since ffmpeg will fetch them.

SECURITY.md sets out where that boundary sits.

Clone this wiki locally